
本文详解如何构建一个符合逻辑的 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 游戏核心状态管理范式。











