24 KiB
构建太空游戏第6部分:结束与重启
journey
title 你的游戏完成旅程
section 结束条件
定义胜负状态: 3: Student
实现条件检测: 4: Student
处理状态转换: 4: Student
section 玩家体验
设计反馈系统: 4: Student
创建重启机制: 5: Student
优化用户界面: 5: Student
section 系统集成
管理游戏生命周期: 5: Student
处理内存清理: 5: Student
创建完整体验: 5: Student
每个伟大的游戏都需要清晰的结束条件和流畅的重启机制。你已经打造了一个具有移动、战斗和计分功能的令人印象深刻的太空游戏——现在是时候添加最终的部分,让它感觉完整了。
你的游戏当前无限运行,就像美国宇航局1977年发射的旅行者探测器——几十年后仍在太空中旅行。虽然这对于太空探索很好,但游戏需要定义好的终点来创造令人满意的体验。
今天,我们将实现合适的胜负条件和重启系统。课程结束时,你将拥有一个抛光完善的游戏,玩家可以完成并重玩,就像定义了媒介的经典街机游戏一样。
mindmap
root((游戏完成))
End Conditions
Victory States[胜利状态]
Defeat Conditions[失败条件]
Progress Tracking[进度跟踪]
State Validation[状态验证]
Player Feedback
Visual Messages[视觉信息]
Color Psychology[色彩心理学]
Clear Communication[清晰传达]
Emotional Response[情感反应]
State Management
Game Loop Control[游戏循环控制]
Memory Cleanup[内存清理]
Object Lifecycle[对象生命周期]
Event Handling[事件处理]
Restart Systems
Input Handling[输入处理]
State Reset[状态重置]
Fresh Initialization[全新初始化]
User Experience[用户体验]
Polish Elements
Message Display[消息显示]
Smooth Transitions[平滑过渡]
Error Prevention[错误预防]
Accessibility[无障碍]
课前测验
理解游戏结束条件
你的游戏应该何时结束?这个基础问题自早期街机时代以来影响了游戏设计。吃豆人被鬼抓住或吃掉所有豆子即结束,太空入侵者则是在外星人到达底部或全部被击毁时结束。
作为游戏创作者,你定义胜利和失败条件。对于我们的太空游戏,以下是创造引人入胜玩法的常见方式:
flowchart TD
A["🎮 游戏开始"] --> B{"检查条件"}
B --> C["敌人数"]
B --> D["英雄生命"]
B --> E["得分阈值"]
B --> F["关卡进度"]
C --> C1{"敌人 = 0?"}
D --> D1{"生命 = 0?"}
E --> E1{"得分 ≥ 目标?"}
F --> F1{"目标完成?"}
C1 -->|是| G["🏆 胜利"]
D1 -->|是| H["💀 失败"]
E1 -->|是| G
F1 -->|是| G
C1 -->|否| B
D1 -->|否| B
E1 -->|否| B
F1 -->|否| B
G --> I["🔄 重新开始选项"]
H --> I
style G fill:#e8f5e8
style H fill:#ffebee
style I fill:#e3f2fd
- 已击毁
N艘敌舰:如果你将游戏分为不同关卡,通常需要击毁N艘敌舰以完成关卡 - 你的飞船被击毁:有些游戏中,如果你的船被毁你就会输。一种常见方式是生命值机制。每当你的船被毁损失一条命,命数耗尽时游戏失败。
- 你获得了
N分:另一种常见的结束条件是你收集到足够的分数。分数来源由你决定,通常分数赋予消灭敌舰或收集敌舰被击毁后掉落的物品。 - 完成一个关卡:这可能涉及多个条件,如击毁
X艘敌舰、收集Y分数,或收集特定道具。
实现游戏重启功能
好的游戏通过流畅的重启机制提高重玩价值。当玩家完成游戏(或失败)时,通常希望立即再试一次——无论是为了超越分数还是提升表现。
stateDiagram-v2
[*] --> Playing: 游戏开始
Playing --> Victory: 消灭所有敌人
Playing --> Defeat: 生命值 = 0
Victory --> MessageDisplay: 显示胜利信息
Defeat --> MessageDisplay: 显示失败信息
MessageDisplay --> WaitingRestart: 按回车提示
WaitingRestart --> Resetting: 按下回车键
Resetting --> CleanupMemory: 清除定时器
CleanupMemory --> ClearEvents: 移除监听器
ClearEvents --> InitializeGame: 全新开始
InitializeGame --> Playing: 新游戏开始
note right of MessageDisplay
颜色编码反馈:
绿色 = 胜利
红色 = 失败
end note
note right of Resetting
完成状态重置
防止内存泄漏
end note
俄罗斯方块是这方面的典范:当方块堆满顶部时,你可以立即开始新游戏,无需复杂菜单操作。我们将构建类似的重启系统,清晰地重置游戏状态,让玩家迅速回归游戏。
✅ 思考:想想你玩过的游戏。它们在什么情况下结束?如何提示你重启?什么样的重启体验让人感觉顺畅而非烦躁?
你将构建的内容
你将实现最终的功能,把项目打造成完整的游戏体验。这些元素让精致游戏区别于基础原型。
今天我们添加的内容:
- 胜利条件:消灭全部敌人并庆祝胜利(你应得的!)
- 失败条件:耗尽所有生命,面对失败画面
- 重启机制:按回车键即可重新开始——因为一场游戏永远不够
- 状态管理:每次都是崭新开始——无遗留敌人或奇怪故障
准备工作
让我们准备开发环境。你应该已经有上节课的所有太空游戏文件。
你的项目文件结构应大致如下:
-| assets
-| enemyShip.png
-| player.png
-| laserRed.png
-| life.png
-| index.html
-| app.js
-| package.json
启动你的开发服务器:
cd your-work
npm start
该命令执行:
- 在
http://localhost:5000运行本地服务器 - 正确提供你的文件服务
- 当你修改时自动刷新浏览器
在浏览器打开 http://localhost:5000 并验证游戏是否能运行。你应该可以移动,发射并与敌人互动。确认后,我们即可开始实现功能。
💡 专业建议:为避免Visual Studio Code中警告,请在文件顶部声明
let gameLoopId;,而不是在window.onload函数内声明。这符合现代JavaScript变量声明最佳实践。
flowchart TD
A["1. 条件追踪"] --> B["2. 事件处理器"]
B --> C["3. 消息常量"]
C --> D["4. 重启控制"]
D --> E["5. 消息显示"]
E --> F["6. 重置系统"]
G["isHeroDead()\nisEnemiesDead()"] --> A
H["碰撞事件\n结束游戏事件"] --> B
I["游戏结束_胜利\n游戏结束_失败"] --> C
J["回车键\n重启触发"] --> D
K["胜利/失败\n颜色编码文本"] --> E
L["状态清理\n全新初始化"] --> F
F --> M["🎮 完整游戏"]
style A fill:#e3f2fd
style B fill:#e8f5e8
style C fill:#fff3e0
style D fill:#f3e5f5
style E fill:#e0f2f1
style F fill:#fce4ec
style M fill:#e1f5fe
实现步骤
步骤1:创建结束条件追踪函数
我们需要函数来监测游戏何时结束。就像国际空间站上的传感器不断监控关键系统一样,这些函数将持续检查游戏状态。
function isHeroDead() {
return hero.life <= 0;
}
function isEnemiesDead() {
const enemies = gameObjects.filter((go) => go.type === "Enemy" && !go.dead);
return enemies.length === 0;
}
幕后发生的事情:
- 检查 主角是否已无生命(糟糕!)
- 计算 当前还活着并在战斗的敌人数量
- 返回 当战场清空敌人时返回
true - 使用 简单的真假逻辑保持清晰明了
- 过滤 所有游戏对象,找出存活者
步骤2:更新事件处理器以检测结束条件
现在我们将把这些条件检测连接到游戏事件系统中。每当发生碰撞时,游戏会评估是否触发结束条件。这为关键游戏事件创建即时反馈。
sequenceDiagram
participant Collision
participant GameLogic
participant Conditions
participant EventSystem
participant Display
Collision->>GameLogic: 激光击中敌人
GameLogic->>GameLogic: 摧毁对象
GameLogic->>Conditions: 检查 isEnemiesDead()
alt 全部敌人被击败
Conditions->>EventSystem: 发送 GAME_END_WIN
EventSystem->>Display: 显示胜利消息
else 敌人仍然存在
Conditions->>GameLogic: 继续游戏
end
Collision->>GameLogic: 敌人攻击英雄
GameLogic->>GameLogic: 减少生命值
GameLogic->>Conditions: 检查 isHeroDead()
alt 生命值 = 0
Conditions->>EventSystem: 发送 GAME_END_LOSS
EventSystem->>Display: 显示失败消息
else 生命值仍然存在
GameLogic->>Conditions: 检查 isEnemiesDead()
alt 全部敌人被击败
Conditions->>EventSystem: 发送 GAME_END_WIN
end
end
eventEmitter.on(Messages.COLLISION_ENEMY_LASER, (_, { first, second }) => {
first.dead = true;
second.dead = true;
hero.incrementPoints();
if (isEnemiesDead()) {
eventEmitter.emit(Messages.GAME_END_WIN);
}
});
eventEmitter.on(Messages.COLLISION_ENEMY_HERO, (_, { enemy }) => {
enemy.dead = true;
hero.decrementLife();
if (isHeroDead()) {
eventEmitter.emit(Messages.GAME_END_LOSS);
return; // 胜利之前的代价
}
if (isEnemiesDead()) {
eventEmitter.emit(Messages.GAME_END_WIN);
}
});
eventEmitter.on(Messages.GAME_END_WIN, () => {
endGame(true);
});
eventEmitter.on(Messages.GAME_END_LOSS, () => {
endGame(false);
});
这里发生的事情:
- 激光击中敌人:双方消失,你得分,我们检查你是否获胜
- 敌人撞击你:你失去一条命,我们检查你是否还活着
- 合理顺序:先检查失败(没人想同时赢又输!)
- 即时反应:重要事件发生时游戏立刻感知
步骤3:添加新消息常量
你需要为 Messages 常量对象添加新的消息类型。这些常量帮助保持一致性,避免事件系统中的拼写错误。
GAME_END_LOSS: "GAME_END_LOSS",
GAME_END_WIN: "GAME_END_WIN",
上面内容:
- 添加 代表游戏结束事件的常量以保持一致性
- 使用 描述性名称清晰标识事件目的
- 遵循 现有消息类型命名规范
步骤4:实现重启控制
接下来添加键盘控制,让玩家能重启游戏。回车键是自然选择,因其常用作确认与开始新游戏的按键。
在现有keydown事件监听器中添加对回车键的检测:
else if(evt.key === "Enter") {
eventEmitter.emit(Messages.KEY_EVENT_ENTER);
}
添加新的消息常量:
KEY_EVENT_ENTER: "KEY_EVENT_ENTER",
你需要知道的:
- 扩展 你现有的键盘事件处理系统
- 使用 回车键作为重启触发器,提升用户体验直观性
- 发布 自定义事件,供游戏的其他部分监听
- 保持 与其他键盘控制相同模式
步骤5:创建消息显示系统
游戏需要清楚地向玩家传达结果。我们将创建一个消息系统,使用颜色编码的文本显示胜利和失败状态,类似早期计算机终端界面绿色表示成功,红色表示错误。
创建 displayMessage() 函数:
function displayMessage(message, color = "red") {
ctx.font = "30px Arial";
ctx.fillStyle = color;
ctx.textAlign = "center";
ctx.fillText(message, canvas.width / 2, canvas.height / 2);
}
一步步,发生了什么:
- 设置 字号和字体,确保文字清晰易读
- 应用 颜色参数,默认为“红色”表示警告
- 让 文字在画布水平和垂直居中
- 利用 现代JavaScript默认参数支持灵活颜色选择
- 借助 canvas 2D上下文直接渲染文本
创建 endGame() 函数:
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)
}
此函数做了什么:
- 冻结 所有移动——不再有舰船或激光动作
- 暂停 200毫秒,让最后一帧完成绘制
- 清屏 并将背景涂成黑色,效果更戏剧化
- 显示 不同消息给胜利者和失败者
- 用色彩区分消息——绿代表好消息,红代表……不太好
- 告诉 玩家如何重新开始游戏
🔄 教学反馈点
游戏状态管理:在实现重置功能前确保理解:
- ✅ 结束条件如何创造清晰的游戏目标
- ✅ 为什么视觉反馈对玩家理解至关重要
- ✅ 合适清理对防止内存泄漏的重要性
- ✅ 事件驱动架构如何实现干净的状态切换
自测速答:如果重置时不清理事件监听器会怎样? 答案:导致内存泄漏和重复事件处理器,引发不可预测的行为
游戏设计原则:你正在实现的是:
- 清晰目标:玩家明确什么是胜利、什么是失败
- 即时反馈:游戏状态变化立即被传达
- 用户控制:玩家准备好了就能重启
- 系统可靠性:适当清理避免bug和性能损耗
步骤6:实现游戏重置功能
重置系统需彻底清理当前游戏状态,初始化清新的游戏会话。保证玩家得到干净的开局,无前一局遗留数据。
创建 resetGame() 函数:
function resetGame() {
if (gameLoopId) {
clearInterval(gameLoopId);
eventEmitter.clear();
initGame();
gameLoopId = setInterval(() => {
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);
}
}
逐部分理解:
- 检查 当前是否有游戏循环正在运行
- 清除 现有游戏循环以停止所有当前游戏活动
- 移除 所有事件监听器防止内存泄漏
- 重新初始化 游戏状态,生成全新对象和变量
- 启动 新的游戏循环,包含必需的游戏逻辑
- 保持 100毫秒间隔,确保游戏性能一致
将回车键事件处理器添加到 initGame() 函数:
eventEmitter.on(Messages.KEY_EVENT_ENTER, () => {
resetGame();
});
为你的 EventEmitter 类添加 clear() 方法:
clear() {
this.listeners = {};
}
关键点总结:
- 将 回车键按下连接到重置游戏功能
- 在 游戏初始化时注册此事件监听器
- 提供 一种清理所有事件监听器的干净方式,用于重置
- 通过 清空事件处理器避免内存泄漏
- 将 监听器对象重置为空以便重新初始化
恭喜!🎉
👽 💥 🚀 你成功从零构建了完整的游戏。像1970年代首批电子游戏程序员一样,你把代码变成了带有合适游戏机制和用户反馈的互动体验。🚀 💥 👽
你完成了:
- 实现 完整的胜负条件并向用户反馈
- 创建 无缝重启系统,支持连续游戏
- 设计 明确的游戏状态视觉传达
- 管理 复杂的游戏状态切换和清理
- 整合 所有组件成一个连贯、可玩的游戏
🔄 教学反馈点
完整游戏开发系统:庆祝你掌握了完整开发流程:
- ✅ 结束条件如何创造满足感玩家体验?
- ✅ 为什么正确的状态管理对游戏稳定性关键?
- ✅ 视觉反馈如何增强玩家理解?
- ✅ 重启系统在玩家留存中扮演什么角色?
系统掌控力:你的完整游戏展现了:
- 全栈游戏开发:涵盖图形、输入和状态管理
- 专业架构:事件驱动系统加上适当清理
- 用户体验设计:清晰反馈与直观控制
- 性能优化:高效渲染和内存管理
- 打磨完善:所有细节让游戏感觉完美
行业级技能:你实现了:
- 游戏循环架构:实时系统性能稳定
- 事件驱动编程:解耦系统便于扩展
- 状态管理:复杂数据处理与生命周期管控
- 用户界面设计:清晰沟通与响应式操作
- 测试与调试:迭代开发和问题解决
⚡ 下5分钟你可以做什么
- 玩你的完整游戏,测试所有胜负条件
- 尝试修改不同结束条件参数
- 加入 console.log 语句追踪游戏状态变化
- 与朋友分享游戏并收集反馈
🎯 这小时你可以完成什么
- 完成课后测验,反思你的开发历程
- 为胜负状态添加音效效果
- 实现额外的结束条件,如限时或奖励目标
- 制作不同难度,调整敌人数量
- 美化视觉效果,使用更好字体和颜色
📅 你的周度游戏开发精通计划
- 完成增强版太空游戏,多关卡和进度系统
- 增加高级功能,如增强道具、多样敌人类型和特殊武器
- 创建保存的高分榜系统
- 设计菜单、设置和选项的用户界面
- 优化性能,适配不同设备和浏览器
- 在线部署你的游戏,与社区分享
🌟 您的一个月游戏开发职业规划
- 制作多个完整游戏,探索不同的类型和机制
- 学习高级游戏开发框架,如 Phaser 或 Three.js
- 参与开源游戏开发项目的贡献
- 研究游戏设计原则和玩家心理
- 创建展示您游戏开发技能的作品集
- 与游戏开发社区连接,持续学习
🎯 您的完整游戏开发精通时间表
timeline
title 完整游戏开发学习进度
section 基础(课程1-2)
游戏架构:项目结构
:资源管理
:画布基础
:事件系统
section 交互系统(课程3-4)
玩家控制:输入处理
:移动机制
:碰撞检测
:物理模拟
section 游戏机制(课程5)
反馈系统:得分机制
:生命管理
:视觉传达
:玩家激励
section 游戏完成(课程6)
精细化与流程:结束条件
:状态管理
:重启系统
:用户体验
section 高级功能(一周)
增强技能:音频整合
:视觉特效
:关卡进度
:性能优化
section 职业发展(一月)
行业准备:框架精通
:团队协作
:作品集开发
:社区参与
section 职业晋升(三月)
专业化:高级游戏引擎
:平台部署
:盈利策略
:行业网络
🛠️ 您的完整游戏开发工具包总结
完成整个太空游戏系列后,您现已掌握:
- 游戏架构:事件驱动系统、游戏循环和状态管理
- 图形编程:Canvas API,精灵渲染和视觉效果
- 输入系统:键盘处理、碰撞检测和响应式控制
- 游戏设计:玩家反馈、进度系统和参与机制
- 性能优化:高效渲染、内存管理和帧率控制
- 用户体验:清晰沟通、直观控制和细节打磨
- 专业模式:整洁代码、调试技术和项目组织
现实应用:您的游戏开发技能直接适用于:
- 交互式网络应用:动态界面和实时系统
- 数据可视化:动画图表和交互图形
- 教育技术:游戏化和吸引人的学习体验
- 移动开发:基于触摸的交互和性能优化
- 模拟软件:物理引擎和实时建模
- 创意产业:互动艺术、娱乐和数字体验
获得的专业技能:您现在可以:
- 架构复杂的交互系统从零开始
- 调试使用系统化方法的实时应用
- 优化性能以流畅用户体验
- 设计吸引人的用户界面和交互模式
- 高效协作处理技术项目并合理组织代码
掌握的游戏开发概念:
- 实时系统:游戏循环、帧率管理和性能
- 事件驱动架构:解耦系统和消息传递
- 状态管理:复杂数据处理和生命周期管理
- 用户界面编程:Canvas 图形和响应式设计
- 游戏设计理论:玩家心理和参与机制
下一阶段:您已准备好探索高级游戏框架、3D 图形、多玩家系统,或转向专业游戏开发职位!
🌟 成就解锁:您已完成完整的游戏开发旅程,从零打造出专业品质的互动体验!
欢迎加入游戏开发社区! 🎮✨
GitHub Copilot Agent 挑战 🚀
使用 Agent 模式完成以下挑战:
描述: 通过实现一个关卡进度系统,增加难度和奖励功能,增强太空游戏。
提示: 创建一个多关卡太空游戏系统,每个关卡中敌舰数量增加,速度和生命值也提高。增加随关卡增长的得分倍增器,并实现当敌人被摧毁时随机出现的强化道具(如速射或护盾)。包括关卡完成奖励,并在屏幕上显示当前关卡与现有的得分和生命值。
了解更多关于agent mode的信息。
🚀 可选增强挑战
给您的游戏添加音效:通过实现音效来增强游戏体验!考虑添加以下音效:
- 激光射击,玩家开火时
- 敌舰摧毁,敌舰被击中时
- 英雄受伤,玩家受击时
- 胜利音乐,游戏胜利时
- 失败音效,游戏失败时
音频实现示例:
// 创建音频对象
const laserSound = new Audio('assets/laser.wav');
const explosionSound = new Audio('assets/explosion.wav');
// 在游戏事件中播放声音
function playLaserSound() {
laserSound.currentTime = 0; // 重置到开始处
laserSound.play();
}
您需要了解:
- 创建用于不同音效的 Audio 对象
- 重置
currentTime以支持快速连续音效播放 - 处理浏览器自动播放策略,通过用户交互触发声音
- 管理音量和时序以提升游戏体验
💡 学习资源:探索这个audio sandbox,了解更多 JavaScript 游戏中音频实现。
课后测验
复习与自学
您的作业是创建一个新的示例游戏,您可以探索一些有趣的游戏,看看想制作哪种类型的游戏。
任务
免责声明:
本文件由 AI 翻译服务 Co-op Translator 翻译而成。尽管我们努力确保准确性,但请注意自动翻译可能包含错误或不准确之处。原始文本的原文版本应视为权威来源。对于重要信息,建议采用专业人工翻译。对于因使用本翻译而产生的任何误解或曲解,我们概不负责。