Canvas 射击游戏的循环、碰撞与对象生命周期

Canvas 原型只要能移动、射击和生成敌人,就可以录出一段看似完整的演示;真正运行时却可能在高刷新率设备上整体加速,高速子弹跨过碰撞体,粒子和敌人数组不断增长。本文只解决这些可验证的工程问题:用固定时间步把逻辑更新与渲染频率分开,用运动线段检测高速投射物,用生命周期和数量边界清理对象,并通过切换标签页、密集生成和持续运行检查异常。成长、音效与反馈仅保留实现边界,不对玩法平衡作未经测试的结论。
工程验收关注:
模型是否理解实时游戏的循环、性能、判定与反馈。1. 先定义需要闭合的游戏循环
战斗循环
移动 -> 瞄准 -> 射击 -> 命中 -> 反馈 -> 躲避成长循环
击败敌人 -> 获得经验 -> 升级 -> 三选一强化 -> 改变打法风险循环
生命下降 -> 护盾恢复或资源掉落 -> 决定进攻还是撤退局外循环
开始 -> 逐步加压 -> Boss -> 胜负结算 -> 重开只有敌人和子弹,没有成长与节奏,只是射击演示。
只有升级选项,没有真正改变武器或移动策略,也不算 Roguelike 构筑。
2. 游戏循环为什么要固定时间步
浏览器渲染通常使用 requestAnimationFrame,但不同设备帧率不同。
如果每渲染一帧就固定移动 5 像素:
60Hz 显示器每秒移动 300 像素。
144Hz 显示器每秒移动 720 像素。同一个游戏会在高刷屏上加速。
更稳的方式是按时间更新,或使用固定时间步累积器:
const FIXED_DT = 1 / 60;
let previous = performance.now();
let accumulator = 0;
function frame(now) {
const elapsed = Math.min((now - previous) / 1000, 0.1);
previous = now;
accumulator += elapsed;
while (accumulator >= FIXED_DT) {
update(FIXED_DT);
accumulator -= FIXED_DT;
}
render(accumulator / FIXED_DT);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);Math.min 限制长时间切后台后一次补算过多逻辑。
3. 武器差异必须改变决策
武器区别不能只是伤害数字:
| 武器 | 核心节奏 | 玩家决策 |
|---|---|---|
| 步枪 | 稳定连续输出 | 保持距离和准星追踪 |
| 霰弹 | 近距离爆发 | 冒险靠近,再快速撤离 |
| 轨道炮 | 蓄力贯穿 | 预判敌人路线和释放时机 |
冷却、伤害、弹速、扩散、蓄力和穿透可以数据化,但具体数值必须由目标节奏和实玩记录调整,不能因为结构完整就认为平衡完成。
4. 不同敌人要有不同状态机
只改变颜色而复用同一种追击逻辑,不会产生不同的战斗决策。
可以设计:
追击者:直接靠近玩家。
射手:保持中距离并周期射击。
冲锋者:预警、锁定、蓄力、冲刺、恢复。
狙击者:拉远距离、显示瞄准线、延迟开火。
召唤者:躲在后方并生成低级单位。冲锋者状态机:
CHASE
↓ 距离合适
TELEGRAPH
↓ 预警结束
CHARGE
↓ 撞击或超时
RECOVER
↓ 恢复结束
CHASE预警非常重要。
如果高伤害攻击没有可读提示,难度会变成随机惩罚。
5. 高速子弹为什么会穿过敌人
假设子弹上一帧在敌人左边,下一帧已经到了敌人右边。
如果只检测当前点是否位于敌人圆形范围内,两帧都可能没有重叠。
这叫隧穿。
连续碰撞检测会检查子弹从上一位置到当前位置的线段,是否与敌人碰撞体相交。
简化思路:
保存 projectile.prevX / prevY。
计算运动线段到敌人圆心的最近点。
最近距离小于碰撞半径时判定命中。示例:
function segmentHitsCircle(x1, y1, x2, y2, cx, cy, radius) {
const dx = x2 - x1;
const dy = y2 - y1;
const lengthSq = dx * dx + dy * dy;
if (lengthSq === 0) {
return Math.hypot(cx - x1, cy - y1) <= radius;
}
const t = Math.max(0, Math.min(1,
((cx - x1) * dx + (cy - y1) * dy) / lengthSq
));
const nearestX = x1 + t * dx;
const nearestY = y1 + t * dy;
return Math.hypot(cx - nearestX, cy - nearestY) <= radius;
}轨道炮等高速武器尤其需要这类检查。
6. 对象数量为什么必须设上限
粒子、子弹、敌人和掉落物如果只创建不回收,浏览器会越来越慢。
至少设置:
最大敌人数。
最大玩家子弹数。
最大敌方子弹数。
最大粒子数。
最大掉落物数。对象离开地图、生命周期结束或命中后应标记删除。
数组清理:
projectiles = projectiles.filter(projectile => projectile.alive);
particles = particles.filter(particle => particle.life > 0);规模更大时可以使用对象池,复用已有对象,减少频繁分配和垃圾回收抖动。
单文件小游戏不一定需要复杂 ECS,但必须控制生命周期。
7. 动态难度不能惩罚高手
一个常见错误是:
玩家打得越好,系统立刻生成更多敌人,让所有优势被抵消。这会让成长失去意义。
更合理的难度变量:
基础强度随时间缓慢增加。
等级只小幅修正敌人强度。
低生命时减少短时间刷怪或提高恢复掉落。
高连击可以增加精英概率,但同步增加奖励。
Boss 阶段使用明确规则,不被随机动态难度破坏。动态难度应该调节节奏,不是实时取消玩家优势。
8. Boss 阶段怎么做
阶段变化要让玩家感知到:
新动作。
新节奏。
新空间压力。
清楚的转阶段反馈。示例:
| 阶段 | 触发条件 | 新机制 |
|---|---|---|
| 初始阶段 | 战斗开始 | 基础弹幕与追击 |
| 中间阶段 | 达到设计阈值 | 召唤单位和冲锋 |
| 最终阶段 | 达到设计阈值 | 组合攻击和场地危险 |
转阶段时短暂无敌、清理危险子弹并播放明确反馈,避免玩家在动画中无缘无故受伤。
9. 程序化音效为什么适合单文件
Web Audio API 可以现场合成:
枪声。
爆炸。
受击。
拾取。
升级。
Boss 预警。优点是无需外部音频文件。
但要注意:
浏览器通常要求用户交互后才能启动音频上下文。
高频连续播放需要限制音量和并发。
不同事件要有不同音高、包络和时长。
必须提供静音开关。不要每颗子弹都创建过重的音频节点链。
10. 粒子和屏幕震动要服务于反馈
反馈映射:
普通命中:少量短粒子。
暴击:更亮粒子和短促震动。
玩家受伤:红色闪烁和方向反馈。
爆炸:范围粒子与较强震动。
升级:与战斗颜色不同的正向效果。屏幕震动要衰减,并允许用户关闭。
持续震动只会降低可读性和舒适度。
11. 一份可复制的 Roguelike 任务书
请在一个 HTML 文件中,使用纯原生 HTML、CSS、JavaScript 和 Canvas,从零开发一款可离线运行的俯视角科幻 Roguelike 动作射击游戏。
交互:
- WASD 或方向键移动
- 鼠标瞄准和射击
- 空格冲刺,具有冷却和短暂无伤窗口
- Esc 暂停
内容:
- 多种玩法明显不同的武器
- 多类具有不同状态机的敌人
- 精英变体
- 具有清楚阶段变化的 Boss
- 生命与护盾
- 经验升级和随机三选一强化
- 连击计分、掉落物和动态难度
- 粒子、屏幕震动和程序化音效
- 暂停、重开、胜利和失败结算
工程要求:
1. 使用固定时间步更新逻辑。
2. 高速子弹使用连续碰撞检测。
3. 敌人、子弹、粒子和掉落物有明确上限与生命周期。
4. 页面失焦时自动暂停。
5. Canvas 根据设备像素比清晰缩放。
6. 不使用第三方库、外部接口、图片、字体或音频。
7. 提供音量、屏幕震动和高对比选项。
验证:
- 在目标环境持续游玩并覆盖完整循环。
- 分别使用各类武器完成升级。
- 触发各类敌人、精英和 Boss 阶段。
- 检查高速子弹、暂停恢复、重开和结算。
- 监控对象数量和帧率,修复明显卡顿与无限增长。
最终交付可直接打开的单个 HTML 文件,并说明操作、机制、测试结果和已知限制。12. 怎么验收而不是只试玩一分钟
功能验收
各类武器都能获得并切换。
不同敌人行为确实不同。
升级选项会改变战斗表现。
Boss 阶段按生命触发。
胜负、暂停和重开完整。性能验收
持续运行足以覆盖对象高峰和完整循环。
记录敌人、子弹和粒子峰值。
大量爆炸时观察帧率。
切换标签页后恢复不发生时间爆炸。判定验收
高速武器不会频繁穿透。
冲刺无伤窗口与动画一致。
Boss 转阶段不会产生无提示伤害。
掉落物拾取范围稳定。体验验收
攻击有清楚反馈。
危险攻击有预警。
不同武器改变站位。
升级选择存在取舍。
难度提升但不是突然秒杀。13. 文件大小不能说明什么
文件小可能来自:
没有外部素材。
程序化绘制。
程序化音效。
代码压缩或较少注释。它不能直接证明:
代码可维护。
玩法平衡。
没有内存泄漏。
所有浏览器兼容。
长期游玩有足够内容。文件大小是约束,不是质量评分。
14. 验收检查清单
[ ] 游戏逻辑不依赖显示器刷新率
[ ] 页面失焦时暂停或限制时间补算
[ ] 高速子弹有连续碰撞检测
[ ] 敌人、子弹和粒子数量有上限
[ ] 不同武器改变玩家决策
[ ] 不同敌人具有不同状态机
[ ] 高伤害攻击有可读预警
[ ] 升级选项确实改变属性或机制
[ ] Boss 阶段切换稳定
[ ] 音效需用户交互后启动并可静音
[ ] 屏幕震动可关闭
[ ] 连续运行测试没有明显对象泄漏
[ ] 已明确单文件游戏不是长期生产架构15. 结论与限制
一个 Roguelike 是否“真正能玩”,不是看功能列表有多长。
要看四件事是否形成闭环:
输入是否响应。
战斗是否可读。
成长是否改变策略。
难度与性能是否稳定。固定时间步、连续碰撞和对象生命周期是可以通过源码与运行记录核对的工程机制;武器差异、难度曲线和反馈质量则需要覆盖完整循环的实玩验证。功能清单和文件大小都不能证明游戏已经完成。
本文代码只演示时间步累积器和线段与圆的碰撞思路,没有给出一套完整游戏,也没有声明任何帧率、内存或平衡结果。实际项目还需根据画布缩放、碰撞体形状、暂停策略和目标设备补充测试,并由真实玩家评价可读性与玩法取舍。
Related
相关文章推荐

DeepSeek Harness实测:权限拦截≠结果正确
DeepSeek Harness实测:文件写入、Shell验证与权限拦截。权限只拦动作,验收机制才定结果。

DeepSeek Harness:Agent与插件运行时拆解
DeepSeek Harness是什么?可组合Agent运行时,Cordis管插件,拆解Agent=Model+Harness。

Agent上线后怎么观测?三仪表盘监控成功率/接管率/成本
Agent上线后三仪表盘:任务成功率、人工接管率与成本监控,事件链设计与异常排查流程。

MT-SDPO多教师蒸馏实战:多模型集成降本与路由策略
多教师蒸馏MT-SDPO实战:按样本选教师、验证器把门、缓存降本,附4sapi多模型路由示例。