HTML 赛车游戏:实现动态加速与精准过车计分系统

本文详解如何构建一个符合逻辑的 html 赛车游戏,重点解决速度递增(每5秒+1,起始为5)和精准计分(仅当玩家车尾完全越过敌车尾时+10分)两大核心需求,并修复常见重启异常、误计分与速度失控问题。

在原始代码中,player.score++ 被错误地置于 runGame() 的每一帧循环内,导致分数随帧率狂涨(如60fps下每秒+60),完全违背“仅过车时计分”的设计要求;同时,速度未做时间基准控制,player.speed++ 缺乏定时器封装,造成重启后累积加速、多次重启后指数级失控。此外,“车尾对齐”判定缺失,当前碰撞检测 isCollide() 仅用于防撞,无法支撑过车逻辑。

✅ 正确实现方案

1. 引入时间基准与增量控制

使用 performance.now() 记录上一次加速时间,确保严格每5000ms增加1点速度,且每次游戏初始化重置计时器:

let lastSpeedIncreaseTime = 0;
function updateSpeed(timestamp) {
  if (timestamp - lastSpeedIncreaseTime >= 5000) {
    player.speed++;
    lastSpeedIncreaseTime = timestamp;
  }
}
⚠️ 注意:必须在 runGame() 的动画帧回调中传入 timestamp(requestAnimationFrame(runGame) 自动提供),不可用 setInterval——后者与渲染帧不同步,易导致卡顿或跳帧。

2. 实现精准“车尾过车”计分逻辑

根据题设:“黄车(玩家)尾部 > 绿车(敌人)尾部”才计分。需获取两车 getBoundingClientRect() 的 bottom 值(即元素最下方Y坐标),并确保:

  • 敌车此前未被计过分(防重复)
  • 玩家车尾首次越过敌车尾(上升沿触发)

为此,为每个敌车元素添加自定义属性标记状态:

// 初始化敌车时添加标记
enemyCar.dataset.passed = 'false';

// 在 moveEnemy() 中检查过车
function checkPassing(myCar, enemyCar) {
  const myRect = myCar.getBoundingClientRect();
  const enemyRect = enemyCar.getBoundingClientRect();

  // 黄车尾部(bottom) > 绿车尾部(bottom) → 已完成超越
  if (myRect.bottom > enemyRect.bottom && enemyCar.dataset.passed === 'false') {
    player.score += 10;
    enemyCar.dataset.passed = 'true'; // 标记已计分,防止重复
  }
}

调用位置:在 moveEnemy() 循环内、更新 enemyCar.y 之后立即执行 checkPassing(car, enemyCar)。

3. 彻底移除错误计分与速度硬编码

  • ❌ 删除 runGame() 中的 player.score++;
  • ❌ 删除任何 player.speed++ 的裸调用
  • ✅ 所有状态变更仅通过上述受控函数触发

4. 修复重启状态污染

initializeGame() 必须完全重置所有状态变量

function initializeGame() {
  startScreen.classList.add('hide');
  gameArea.innerHTML = "";

  // 关键:重置全部状态
  player = { speed: 5, score: 0, start: true, x: 0, y: 0 };
  lastSpeedIncreaseTime = performance.now(); // 重置计时起点
  keys = { ArrowUp: false, ArrowDown: false, ArrowLeft: false, ArrowRight: false };

  // ... 后续创建元素逻辑(保持不变)
}

? 为什么原代码重启后速度异常?因 player 对象未被重建,speed 继承上次结束值;而 lastSpeedIncreaseTime 未重置,导致差值极小,瞬间触发加速。

5. 完整 runGame() 重构示例

function runGame(timestamp) {
  if (!player.start) return;

  // 1. 更新速度(基于时间戳)
  updateSpeed(timestamp);

  // 2. 移动道路线
  moveLines();

  // 3. 获取玩家车元素并移动
  const car = document.querySelector('.myCar');
  const road = gameArea.getBoundingClientRect();

  if (keys.ArrowUp && player.y > (road.top + 150)) player.y -= player.speed;
  if (keys.ArrowDown && player.y < (road.bottom - 85)) player.y += player.speed;
  if (keys.ArrowLeft && player.x > 0) player.x -= player.speed;
  if (keys.ArrowRight && player.x < (road.width - 50)) player.x += player.speed;

  car.style.top = player.y + "px";
  car.style.left = player.x + "px";

  // 4. 移动并检测敌车(含过车判定)
  moveEnemy(car);

  // 5. 更新UI
  score.innerText = `Score: ${player.score}\nSpeed: ${player.speed}`;

  // 6. 持续动画
  window.requestAnimationFrame(runGame);
}

? 关键注意事项总结

  • 永远不要在渲染循环中直接修改 score/speed —— 必须通过带条件/时序约束的函数;
  • getBoundingClientRect() 返回的是视口坐标,适合做像素级位置比对,但需确保元素已渲染(offsetTop/Left 在初始化时可能不准确,推荐全程用 getBoundingClientRect());
  • 敌车重置逻辑(y = -300)必须保留,否则新生成的敌车会堆积在屏幕底部;
  • CSS 中 .myCar 和 .enemyCar 的 position: absolute 及初始 top/left 必须由 JS 动态设置,避免样式覆盖 JS 控制;
  • 若需更高精度(如处理高速下帧间跳跃),可引入“扫掠检测(sweep test)”,但本项目中 bottom 比较已满足需求。

通过以上改造,游戏将严格遵循:
✅ 起始速度恒为 5
✅ 每整 5 秒精确 +1 速度(不受重启影响)
✅ 仅当黄车尾部越过绿车尾部时 +10 分(无漏计、无重计、无误计)
✅ 多次重启后状态完全隔离,运行稳定

现在,你已掌握构建可扩展、可维护的 HTML 游戏核心状态管理范式。