Web-Dev-For-Beginners 太空射击课完结篇实战:用多结局条件与重启机制从零构建自己的示例游戏
本篇文章围绕《Web-Dev-For-Beginners》太空射击游戏课程第 6 课(Build a Space Game Part 6: End and Restart)配套的结课实战作业展开。该作业要求学习者在掌握胜利/失败结局判定与重启功能之后,脱离现有代码、独立设计并实现一款全新的示例游戏,用它演示至少两种不同的结局模式与流畅的重启机制。读完本文,你将获得一份可直接照做的作业拆解指南:既清楚作业的硬性需求、可选游戏创意与评估量规,也能对照仓库中的官方参考实现,掌握多结局判定、事件驱动通信、画面反馈与干净重置的核心代码套路。
作业定位:把「结局与重启」从跟随变成创造
本作业对应仓库第 6 课的练习文档(英文原版见 6-space-game/6-end-condition/assignment.md,即本文依据的关联文档为多语言翻译版 translations/bg/6-space-game/6-end-condition/assignment.md)。它出现在你已经用 Canvas 与 JavaScript 完成飞船移动、发射激光、碰撞检测、计分与生命管理的完整课程链条之后(课程正文 同步提供了全套结局与重启的教学实现)。
作业的定位很明确:不再要求你往太空游戏里「打补丁」,而是把之前学会的结局条件与重启机制当作设计工具,迁移到一个你亲手构思的崭新游戏中。它同时考察三件事——对游戏结局模式的理解、对事件驱动代码组织方式的运用、以及对「重开一局要做到状态完全干净」这一工程细节的把握。
作业核心需求:三类必备要素
结局条件多样化:至少实现两种「游戏如何结束」
作业要求游戏至少具备两种不同的结束方式,可从以下四类模式中任选组合:
- 基于得分的胜利(Point-based victory):玩家达到目标分数,或收集到指定数量的物品;
- 基于生命的失败(Life-based defeat):玩家耗尽全部生命值或生命点数;
- 目标达成(Objective completion):全部敌人被消灭、指定物品被集齐、特定目标达成;
- 基于时间(Time-based):游戏在设定时长后结束,或倒计时归零。
这正是第 6 课正文所归纳的「什么时候游戏应该结束」这一设计命题的落地:Pac-Man 在被幽灵抓住或清空所有豆子时结束,Space Invaders 在外星人抵达底部或被全灭时结束,而作为创作者的你,需要自己为胜利与失败下定义。
重启功能:干净、彻底、可感知
重启不是简单地把画面刷新一遍,作业从三个层面给出了硬性要求:
- 清空游戏状态:移除上一局遗留的全部游戏对象并重置所有变量;
- 重新初始化系统:以全新的玩家属性、敌人与目标从头开始;
- 友好的控制:向玩家提供清晰明确的重启操作指引。
玩家反馈:让玩家始终知道「现在发生了什么」
- 胜利消息:用积极的反馈庆祝玩家的成就;
- 失败消息:用鼓励性的文案推动玩家再来一局;
- 进度指示器:实时展示当前分数、剩余生命或目标达成状态。
备选游戏创意:四选一或自创
作业给出了四个可直接选用的游戏概念,也可自行构思:
1. 控制台冒险游戏(Console Adventure Game)
基于文本的冒险游戏,带回合制战斗机制,作业甚至给出了示范对局输出:
Hero> Strikes with broadsword - orc takes 3p damage
Orc> Hits with club - hero takes 2p damage
Hero> Kicks - orc takes 1p damage
Game> Orc is defeated - Hero collects 2 coins
Game> ****No more monsters, you have conquered the evil fortress****
需要实现的要点包括:提供多种攻击选项的回合制战斗、玩家与敌人各自的生命值、用于收集金币或物品的背包系统、难度不一的多种敌人类型,以及「全部敌人被击败即胜利」的胜利条件。
2. 收集类游戏(Collection Game)
- 目标:在躲避障碍物的同时收集指定物品;
- 结局条件:达成目标收集数量,或耗尽全部生命;
- 进阶设计:随着游戏推进,物品会越来越难以到达。
3. 解谜游戏(Puzzle Game)
- 目标:依次解决难度递增的谜题;
- 结局条件:通关全部关卡,或步数/时间耗尽;
- 重启:回到第一关,进度完全清空。
4. 防守类游戏(Defense Game)
- 目标:保护基地免受一波波敌人的攻击;
- 结局条件:撑过全部波次即胜利,基地被摧毁则失败;
- 进阶设计:敌人波次的难度与数量逐步提升。
对照官方参考实现:把需求翻译成代码
太空游戏的官方参考实现位于 6-space-game/6-end-condition/solution/app.js,配套的 HTML 载体在 6-space-game/6-end-condition/solution/index.html(一个 1024×768 的画布节点)。它是你完成本作业最直接的「语法词典」,以下逐一拆解其中与本作业需求一一对应的实现。
结局判定函数:把条件收敛成布尔值
无论采用哪类结局模式,最终都需要以集中、可读的方式判定「是否结束」。参考实现将判定收敛为两个纯函数:
isHeroDead()(app.js):判断英雄生命是否归零;isEnemiesDead()(app.js):通过filter统计仍存活(go.type === "Enemy" && !go.dead)的敌人数量是否为 0。
function isHeroDead() {
return hero.life <= 0;
}
function isEnemiesDead() {
const enemies = gameObjects.filter((go) => go.type === "Enemy" && !go.dead);
return enemies.length === 0;
}
这种「返回布尔值」的纯函数风格让结局检查可以在任何时机被重复调用,也正是作业要求中「清晰定义结局条件」的最小代码形态。
事件驱动架构:结局如何被触发
仓库参考实现里有一个极简的 EventEmitter(app.js),所有结局相关逻辑都以「消息」解耦:
Messages常量集中定义了GAME_END_LOSS、GAME_END_WIN、KEY_EVENT_ENTER等消息名(app.js),避免事件名拼写错误;- 碰撞发生后,游戏逻辑调用结局判定并发出结局消息(app.js):
eventEmitter.on(Messages.COLLISION_ENEMY_LASER, (_, { first, second }) => {
first.dead = true;
second.dead = true;
hero.incrementPoints(); // 每次击毁 +100 分
if (isEnemiesDead()) {
eventEmitter.emit(Messages.GAME_END_WIN);
}
});
eventEmitter.on(Messages.COLLISION_ENEMY_HERO, (_, { enemy }) => {
enemy.dead = true;
hero.decrementLife(); // 生命从 3 开始逐次递减
if (isHeroDead()) {
eventEmitter.emit(Messages.GAME_END_LOSS);
return; // 先判失败,避免胜败同时发生
}
if (isEnemiesDead()) {
eventEmitter.emit(Messages.GAME_END_WIN);
}
});
注意这里的两个工程细节,也正是作业「技术性实现」评分维度考察的对象:
- 胜负同帧互斥:当英雄与最后一个敌人同归于尽时,代码刻意先判失败(
return提前退出),保证不会出现「既胜利又失败」的矛盾状态; - 检查时机分散:结局判定既可以在碰撞回调中即时触发(如本例),也可以放入主循环的「更新逻辑 → 检查结局」阶段集中执行,后者更贴合作业主循环模板中的第 4 步。
反馈画面:颜色语义化地呈现结局
endGame(win)(app.js)与 displayMessage()(app.js)共同实现了「消息显示 + 颜色编码」:
function endGame(win) {
clearInterval(gameLoopId); // 冻结游戏循环
setTimeout(() => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "black";
ctx.fillRect(0, 0, canvas.width, canvas.height);
if (win) {
displayMessage("Victory!!! Pew Pew... - Press [Enter] to start a new game Captain Pew Pew", "green");
} else {
displayMessage("You died !!! Press [Enter] to start a new game Captain Pew Pew");
}
}, 200); // 等待最后一帧绘制完成
}
function displayMessage(message, color = "red") {
ctx.font = "30px Arial";
ctx.fillStyle = color; // 默认红色=失败,胜利传 green
ctx.textAlign = "center";
ctx.fillText(message, canvas.width / 2, canvas.height / 2);
}
这套做法对应作业「玩家反馈」需求的三点:胜利消息用绿色、失败消息用默认红色、提示文案直接把「按 Enter 重开」写给了玩家。函数默认参数 color = "red" 是典型 ES6+ 语法,与作业「现代 JavaScript」要求呼应。游戏界面同时提供持续的进度指示——drawPoints() 与 drawLife()(app.js)在每帧渲染实时分数与剩余生命图标,这正是「进度指示器」需求的现成范例。
重启闭环:清空、重注册、重开新循环
作业把重启拆成「清空游戏状态 + 重新初始化 + 用户友好控制」三步,参考实现一一对应:
- Enter 键触发:键盘事件监听中拦截
Enter并发出KEY_EVENT_ENTER消息(app.js); - 事件监听与初始化:
initGame()重设gameObjects数组、重建英雄与敌人,并注册全部事件监听(app.js),其中包含KEY_EVENT_ENTER → resetGame()的重启入口; - 完整重置:
resetGame()(app.js)先清除旧循环,再清空事件监听,随后重新初始化并启动新循环:
function resetGame() {
if (gameLoopId) { // 仅当游戏循环确实存在时重置
clearInterval(gameLoopId); // 1. 停止旧循环
eventEmitter.clear(); // 2. 移除所有旧事件监听
initGame(); // 3. 重置状态、重建对象、重挂监听
gameLoopId = setInterval(() => { // 4. 重启 100ms 主循环
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "black";
ctx.fillRect(0, 0, canvas.width, canvas.height);
drawPoints();
drawLife();
updateGameObjects();
drawGameObjects(ctx);
}, 100);
}
}
配套的 EventEmitter.clear()(app.js)把 listeners 重置为空对象,这正是防止「内存泄漏与重复事件处理器」的关键。第 6 课正文特别用一个自测题点明:若重置时不清理事件监听,多次重启会导致事件处理器重复叠加,游戏行为变得不可预测——这正是作业评分表中「Developing(发展期):重启可能有小毛病」与「Exemplary(卓越):重启顺滑无残留」的分水岭。
游戏主循环:作业模板与代码的对应关系
作业给出了标准主循环五步模板,与参考实现的 window.onload 驱动结构(app.js)逐一对得上:
| 作业主循环步骤 | 参考实现对应 |
|---|---|
| 初始化游戏状态 | initGame():清空 gameObjects,创建敌人与英雄 |
| 处理用户输入 | keyup 监听把方向键、空格、Enter 转成事件消息 |
| 更新游戏逻辑 | updateGameObjects():碰撞检测、扣血、加分、清理死亡对象 |
| 检查结局条件 | 碰撞回调中调用 isHeroDead() / isEnemiesDead() 并发出结局消息 |
| 渲染当前状态 | drawGameObjects() + drawPoints() + drawLife(),每 100ms 一帧 |
这里有一个值得照做的编码细节:课程建议把 let gameLoopId; 声明在文件顶层(app.js)而不是 window.onload 内部,这样 resetGame() 才能访问到它——类似「循环句柄需要在整个游戏生命周期内可访问」的问题,在你自己的新游戏里同样会出现。
实施路线:从设计稿到可玩原型
第 1 步:先规划,再写码
- 画出基本的游戏循环草图;
- 清晰定义结局条件(胜利/失败分别何时成立);
- 明确重启时哪些数据必须重置(分数、生命、游戏对象数组、倒计时、关卡进度等)。
第 2 步:搭建项目结构
my-game/
├── index.html
├── style.css
├── game.js
└── README.md
如需本地预览,可将该目录交给任意静态服务器托管(第 6 课正文中启动开发服务器的方式为在 your-work 目录执行 npm start,详见 6-space-game/6-end-condition/README.md)。
第 3 步:实现核心主循环
按「初始化状态 → 处理输入 → 更新逻辑 → 检查结局 → 渲染状态」五步组织代码。可直接参考 6-space-game/6-end-condition/solution/app.js 中 initGame 与主循环的写法,也可沿用仓库前几课提供的 your-work 起步代码(6-space-game/6-end-condition/your-work)中的结构习惯。
技术验收标准:代码质量如何自检
作业为代码质量规定了三条硬标准:
现代 JavaScript:变量声明使用 const / let;适当使用箭头函数;落地 ES6+ 特性(如模板字符串、解构赋值)。
事件驱动架构:为用户交互编写事件处理函数;通过事件推动游戏状态变化;用事件监听器实现重启功能。
整洁代码实践:函数只做一件事(单一职责);变量与函数采用描述性命名;用注释说明游戏逻辑与规则;把代码组织为逻辑分明的段落。
提交物与测试清单
交付材料
- 完整的游戏文件:运行游戏所需的全部 HTML、CSS、JavaScript 文件;
- README.md:说明游戏玩法、实现了哪些结局条件、重启操作指引,以及任何特殊功能或机制;
- 代码注释:对游戏逻辑与算法给出清晰解释。
提交前逐项自检
- [ ] 浏览器控制台无报错运行;
- [ ] 按规格实现了多个结局条件;
- [ ] 能正确重启,状态完全干净;
- [ ] 向玩家提供关于游戏状态的清晰反馈;
- [ ] 使用现代 JavaScript 语法与良好实践;
- [ ] README.md 包含完整文档。
评分量规与得分解读
作业的评估由 5 个维度构成,每个维度满分 4 分,总分 20 分:
| 维度 | 卓越(4) | 熟练(3) | 发展期(2) | 起步(1) |
|---|---|---|---|---|
| 游戏功能 | 完整游戏:多种结局条件、顺滑重启、打磨成熟的体验 | 完整游戏:基础结局条件 + 可用的重启机制 | 部分完成:仅实现部分结局条件,重启可能存在小问题 | 不完整:功能有限、bug 明显 |
| 代码质量 | 干净有条理,现代 JS 实践,注释完整、结构出色 | 组织良好,语法现代,注释充分、结构清晰 | 基础组织,部分现代实践,注释极少 | 组织混乱、语法过时、缺乏注释与结构 |
| 用户体验 | 直觉化玩法、明确指引、反馈出色、结局/重启体验有吸引力 | 玩法良好、指引与反馈够用、结局/重启功能正常 | 基础玩法、指引极少、对游戏状态反馈有限 | 玩法令人困惑、指引不清、反馈差 |
| 技术实现 | 展现对游戏开发、事件处理与状态管理的掌握 | 对游戏概念理解扎实、实现良好 | 基础理解、实现尚可接受 | 理解有限、实现欠佳 |
| 文档 | README 完整清晰、代码注释良好、测试证据充分 | 文档良好、指引清晰、注释适当 | 基础文档、指引极少 | 文档差或缺失 |
总分档位
- 卓越(16–20 分):以创意功能与打磨到位的实现超出预期;
- 熟练(12–15 分):满足全部要求且执行扎实;
- 发展期(8–11 分):满足大部分要求,存在小问题;
- 起步(4–7 分):仅满足部分要求,仍需大幅改进。
实战建议:先做对,再做好
作业原文强调一句方法论:从简单的东西开始,增量地叠加功能——一个打磨到位的简单游戏,永远好过一个充满 bug 的复杂游戏。
结合参考实现可以再提炼三条落地建议:第一,先把「结局判定 + 重启」这条最小闭环跑通(对应参考实现的 resetGame 与结局消息),再扩充玩法;第二,凡涉及定时器(setInterval)、事件监听等资源,一律在重启路径上显式清理,这决定了你的游戏能否经得起连续重开;第三,把胜利与失败文案、颜色、以及重启指引都当成产品的一部分来打磨——它们直接决定玩家是否愿意「再来一局」。
若想进一步挑战,第 6 课正文(6-space-game/6-end-condition/README.md)还提供了加音频特效、多关卡难度、道具系统与排行榜等进阶方向,可作为本作业通过后的延伸练习。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00