真实射击游戏必须用 requestAnimationFrame 控制主循环,基于 deltaTime 归一化位移计算,状态更新与渲染分离,子弹用 Canvas 绘制并局部擦除,碰撞检测用距离平方或 AABB,按键状态需全局管理并防默认行为。

用 requestAnimationFrame 控制游戏主循环,别用 setInterval
浏览器重绘节奏和 JS 执行节奏不一致,setInterval 容易导致画面卡顿、子弹位移跳变或输入响应延迟。真实射击游戏必须靠 requestAnimationFrame 对齐屏幕刷新率。
- 每次回调拿到的是上一帧到当前帧的耗时(
deltaTime),所有位移计算要乘以它,否则在不同刷新率设备上速度不一致 - 不要在回调里直接写渲染逻辑——先更新状态(玩家位置、子弹坐标、敌人血量),再统一渲染,避免状态和画面不同步
- 如果帧率掉到 30fps 以下,
deltaTime可能超过 33ms,需加阈值限制(如Math.min(deltaTime, 16))防单帧过长导致子弹“瞬移”
子弹用 canvas 绘制比 DOM 元素快一个数量级
每个子弹都建一个 <div> 并用 <code>transform: translate() 移动?50 发子弹同时存在时,DOM 重排+重绘开销会立刻拖垮 60fps。Canvas 2D 上画圆或小矩形,CPU 占用低且可控。
- 子弹数据存为数组:
bullets = [{x: 100, y: 200, vx: 5, vy: 0, life: 100}],每帧遍历更新x += vx * dt,超出画布或life 就 <code>splice掉 - 绘制时用
ctx.beginPath(); ctx.arc(x, y, 3, 0, Math.PI * 2); ctx.fill();,比drawImage贴图更轻量(除非你要带尾迹或旋转效果) - 别在每帧清空整个 canvas 后重画所有元素——只清子弹区域(
ctx.clearRect(x-5,y-5,10,10))再重绘,对少量高速移动对象更省
碰撞检测用距离平方比较,别调 Math.sqrt
玩家打中敌人、子弹击中目标,本质是点与圆(或矩形)的距离判断。常见错误是算欧氏距离再比半径:Math.sqrt(dx*dx + dy*dy) ——<code>sqrt 是高开销操作,每帧上百次调用明显掉帧。
- 改成比较距离平方:
dx*dx + dy*dy ,完全等价但零函数调用 - 敌人若为矩形,用轴对齐包围盒(AABB):检查
bullet.x > enemy.x && bullet.x enemy.y && bullet.y ,比圆形检测还快 - 批量检测时先做粗筛:把屏幕分格子(grid-based spatial partition),只检测子弹所在格子及邻近格子里的敌人,避免 O(n×m) 全量遍历
keydown 和 keyup 必须配对管理按键状态,别在事件里直接移动
按住方向键时,keydown 会连续触发,但你不能在每次事件里执行一次位移——这样会导致加速跑、松手后还滑行,或者连发子弹间隔失控。
立即学习“前端免费学习笔记(深入)”;
- 声明一个状态对象:
const keys = {ArrowUp: false, ArrowDown: false, Space: false},keydown设true,keyup设false - 主循环里读
keys.ArrowUp决定是否加速度,再积分得速度和位置;keys.Space控制是否生成新子弹(并加冷却计时器) - 注意浏览器默认行为:空格键会触发页面滚动,记得在
keydown里e.preventDefault(),尤其移动端软键盘弹出时容易误触
实际运行中,最常被忽略的是时间步长(deltaTime)未归一化带来的跨设备体验差异,以及 DOM 操作混入 Canvas 渲染路径导致的隐式重排。这两点不处理,游戏在低端机或 Chrome 外其他浏览器里基本不可玩。



















