首页
/ Web-Dev-For-Beginners 太空射击课完结篇实战:用多结局条件与重启机制从零构建自己的示例游戏

Web-Dev-For-Beginners 太空射击课完结篇实战:用多结局条件与重启机制从零构建自己的示例游戏

2026-09-07 23:48:10作者:柏廷章Berta

本篇文章围绕《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;
}

这种「返回布尔值」的纯函数风格让结局检查可以在任何时机被重复调用,也正是作业要求中「清晰定义结局条件」的最小代码形态。

事件驱动架构:结局如何被触发

仓库参考实现里有一个极简的 EventEmitterapp.js),所有结局相关逻辑都以「消息」解耦:

  • Messages 常量集中定义了 GAME_END_LOSSGAME_END_WINKEY_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);
    }
});

注意这里的两个工程细节,也正是作业「技术性实现」评分维度考察的对象:

  1. 胜负同帧互斥:当英雄与最后一个敌人同归于尽时,代码刻意先判失败(return 提前退出),保证不会出现「既胜利又失败」的矛盾状态;
  2. 检查时机分散:结局判定既可以在碰撞回调中即时触发(如本例),也可以放入主循环的「更新逻辑 → 检查结局」阶段集中执行,后者更贴合作业主循环模板中的第 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.jsinitGame 与主循环的写法,也可沿用仓库前几课提供的 your-work 起步代码(6-space-game/6-end-condition/your-work)中的结构习惯。

技术验收标准:代码质量如何自检

作业为代码质量规定了三条硬标准:

现代 JavaScript:变量声明使用 const / let;适当使用箭头函数;落地 ES6+ 特性(如模板字符串、解构赋值)。

事件驱动架构:为用户交互编写事件处理函数;通过事件推动游戏状态变化;用事件监听器实现重启功能。

整洁代码实践:函数只做一件事(单一职责);变量与函数采用描述性命名;用注释说明游戏逻辑与规则;把代码组织为逻辑分明的段落。

提交物与测试清单

交付材料

  1. 完整的游戏文件:运行游戏所需的全部 HTML、CSS、JavaScript 文件;
  2. README.md:说明游戏玩法、实现了哪些结局条件、重启操作指引,以及任何特殊功能或机制;
  3. 代码注释:对游戏逻辑与算法给出清晰解释。

提交前逐项自检

  • [ ] 浏览器控制台无报错运行
  • [ ] 按规格实现了多个结局条件
  • [ ] 能正确重启,状态完全干净;
  • [ ] 向玩家提供关于游戏状态的清晰反馈
  • [ ] 使用现代 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)还提供了加音频特效、多关卡难度、道具系统与排行榜等进阶方向,可作为本作业通过后的延伸练习。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388