# 构建一个太空游戏 第一部分: 介绍 ```mermaid journey title 你的游戏开发旅程 section 基础 Learn game architecture: 3: Student Understand inheritance: 4: Student Explore composition: 4: Student section 通信 Build pub/sub system: 4: Student Design event flow: 5: Student Connect components: 5: Student section 应用 Create game objects: 5: Student Implement patterns: 5: Student Plan game structure: 5: Student ``` ![太空游戏动画显示游戏玩法](../../../../6-space-game/images/pewpew.gif) 就像美国宇航局的任务控制中心在发射过程中协调多个系统一样,我们将构建一个太空游戏,展示程序的不同部分如何无缝协作。在创建一个你可以实际玩的项目时,你将学习适用于任何软件项目的基本编程概念。 我们将探索组织代码的两种基本方法:继承和组合。这不仅仅是学术概念——它们是驱动从视频游戏到银行系统所有内容的相同模式。我们还将实现一个名为发布/订阅(pub/sub)的通信系统,它类似于航天器中使用的通信网络,允许不同组件共享信息而不产生依赖关系。 通过本系列课程的结束,你将理解如何构建可扩展和演进的应用程序——无论你是开发游戏、Web应用程序,还是任何其他软件系统。 ```mermaid mindmap root((游戏架构)) Object Organization 继承 组合 类层次结构 行为混合 Communication Patterns 发布/订阅系统 事件发射器 消息传递 松耦合 Game Objects 属性 (x, y) 行为 (移动, 碰撞) 生命周期管理 状态管理 Design Patterns 工厂函数 观察者模式 组件系统 事件驱动架构 Scalability 模块化设计 可维护代码 测试策略 性能优化 ``` ## 课前测验 [课前测验](https://ff-quizzes.netlify.app/web/quiz/29) ## 游戏开发中的继承与组合 随着项目复杂度增大,代码组织变得尤为关键。最初简单的脚本如果没有良好的结构,维护会变得困难——就像阿波罗任务需要对上千个组件进行细致协作一样。 我们将探索两种组织代码的基本方式:继承和组合。每种都有独特的优点,理解它们有助于你在不同情况下选择合适的方式。我们将通过太空游戏进行演示,其中英雄、敌人、道具以及其他对象必须高效互动。 ✅ 一本最著名的编程书籍就讲述了[设计模式](https://en.wikipedia.org/wiki/Design_Patterns)。 在任何游戏中,你都有 `游戏对象`——填充游戏世界的交互元素。英雄、敌人、道具和视觉特效都是游戏对象。它们都存在于特定屏幕坐标,用 `x` 和 `y` 值表示,类似于在坐标平面上绘点。 尽管外观不同,这些对象通常共享基本行为: - **它们存在于某处** —— 每个对象都有 x 和 y 坐标,游戏才能知道在哪里绘制它 - **许多对象可以移动** —— 英雄奔跑,敌人追逐,子弹飞过屏幕 - **它们有生命周期** —— 有些永久存在,另一些(比如爆炸)短暂出现后消失 - **它们对事件做出反应** —— 碰撞发生时,道具被拾取,血条更新 ✅ 想想像吃豆人这样的游戏。你能在游戏中识别上面提到的四种对象类型吗? ```mermaid classDiagram class GameObject { +x: number +y: number +type: string +exists_somewhere() } class MovableObject { +moveTo(x, y) +can_move_around() } class TemporaryObject { +lifespan: number +has_lifespan() } class InteractiveObject { +onCollision() +reacts_to_stuff() } GameObject <|-- MovableObject GameObject <|-- TemporaryObject GameObject <|-- InteractiveObject MovableObject <|-- Hero MovableObject <|-- Enemy MovableObject <|-- Bullet TemporaryObject <|-- PowerUp TemporaryObject <|-- Explosion InteractiveObject <|-- Collectible InteractiveObject <|-- Obstacle ``` ### 通过代码表达行为 现在你理解了游戏对象共有的行为,让我们探索如何用 JavaScript 实现这些行为。你可以通过添加到类或单个对象的方法来表达对象行为,且有多种实现方式。 **基于类的方法** 类和继承为组织游戏对象提供了一种结构化的方式。就像卡尔·林奈所构建的生物分类系统一样,你从一个含有共有属性的基础类开始,然后创建派生类继承这些基本功能并添加特定能力。 ✅ 继承是一个重要概念,更多内容请见[MDN关于继承的文章](https://developer.mozilla.org/docs/Web/JavaScript/Inheritance_and_the_prototype_chain)。 下面是如何使用类和继承实现游戏对象的示例: ```javascript // 第一步:创建基础的 GameObject 类 class GameObject { constructor(x, y, type) { this.x = x; this.y = y; this.type = type; } } ``` **我们逐步拆解理解:** - 我们创建了一个基础模板,所有游戏对象都能使用它 - 构造函数保存对象的位置(`x`, `y`)和类型 - 这成为所有游戏对象构建的基础 ```javascript // 第2步:通过继承添加移动功能 class Movable extends GameObject { constructor(x, y, type) { super(x, y, type); // 调用父类构造函数 } // 添加移动到新位置的能力 moveTo(x, y) { this.x = x; this.y = y; } } ``` **在上例中,我们:** - **扩展**了 GameObject 类以添加移动功能 - **使用** `super()` 调用父类构造函数初始化继承的属性 - **添加**了 `moveTo()` 方法来更新对象位置 ```javascript // 第3步:创建特定的游戏对象类型 class Hero extends Movable { constructor(x, y) { super(x, y, 'Hero'); // 自动设置类型 } } class Tree extends GameObject { constructor(x, y) { super(x, y, 'Tree'); // 树木不需要移动 } } // 第4步:使用您的游戏对象 const hero = new Hero(0, 0); hero.moveTo(5, 5); // 英雄可以移动! const tree = new Tree(10, 15); // tree.moveTo() 会导致错误——树木不能移动 ``` **理解这些概念:** - **创建**继承适当行为的专门对象类型 - **展示**继承如何选择性包含功能 - **说明**英雄可以移动,而树木保持静止 - **演示**类层级防止不恰当操作 ✅ 花几分钟重新构思吃豆人中的英雄(比如 Inky、Pinky 或 Blinky)如何用 JavaScript 编写。 **组合方法** 组合遵循模块化设计理念,类似于工程师设计航天器时使用可互换组件。你不是继承父类,而是组合特定行为来创建具有所需功能的对象。这种方法提供了灵活性,不受严格层级限制。 ```javascript // 第一步:创建基础行为对象 const gameObject = { x: 0, y: 0, type: '' }; const movable = { moveTo(x, y) { this.x = x; this.y = y; } }; ``` **这段代码的作用:** - **定义**了一个带有位置和类型的基础 `gameObject` - **创建**了一个独立的 `movable` 行为对象,实现移动功能 - **分离**了位置数据和移动逻辑,使职责独立 ```javascript // 第2步:通过组合行为来组合对象 const movableObject = { ...gameObject, ...movable }; // 第3步:为不同类型的对象创建工厂函数 function createHero(x, y) { return { ...movableObject, x, y, type: 'Hero' }; } function createStatic(x, y, type) { return { ...gameObject, x, y, type }; } ``` **在上面,我们:** - **通过扩展语法**组合基础对象属性和移动行为 - **创建**工厂函数返回定制对象 - **实现**了无需复杂类层级即可灵活创建对象 - **使**对象恰好拥有所需行为 ```javascript // 第4步:创建并使用你组合的对象 const hero = createHero(10, 10); hero.moveTo(5, 5); // 工作完美! const tree = createStatic(0, 0, 'Tree'); // tree.moveTo() 未定义 - 未组合任何移动行为 ``` **记住关键点:** - **通过混合行为组合对象,而非继承** - **提供**比严格继承层级更大的灵活性 - **允许**对象恰好拥有它们需要的特性 - **使用**现代 JavaScript 扩展语法实现干净合并 ``` **Which Pattern Should You Choose?** **Which Pattern Should You Choose?** ```mermaid quadrantChart title Code Organization Patterns x-axis Simple --> Complex y-axis Rigid --> Flexible quadrant-1 Advanced Composition quadrant-2 Hybrid Approaches quadrant-3 Basic Inheritance quadrant-4 Modern Composition Class Inheritance: [0.3, 0.2] Interface Implementation: [0.6, 0.4] Mixin Patterns: [0.7, 0.7] Pure Composition: [0.8, 0.9] Factory Functions: [0.5, 0.8] Prototype Chain: [0.4, 0.3] ``` > 💡 **专业提示**:两种模式在现代 JavaScript 开发中各有用武之地。类适合明确定义的层级结构,而组合在需要最大灵活性时表现出色。 > **何时使用:** - **当存在明确的“是一个”关系时选择继承**(例如,英雄 *是一个* 可移动对象) - **当存在“有一个”关系时选择组合**(例如,英雄 *拥有* 移动能力) - **考虑**团队偏好和项目需求 - **记住**可以在同一应用中混合使用两者 ### 🔄 **教学自检** **对象组织理解**:在进入通信模式之前,确保你能: - ✅ 解释继承与组合的区别 - ✅ 识别何时使用类还是工厂函数 - ✅ 理解继承中 `super()` 关键字的作用 - ✅ 认识每种方法对游戏开发的优势 **快速自测**:你如何创建一个既可移动又能飞行的敌人? - **继承方法**: `class FlyingEnemy extends Movable` - **组合方法**: `{ ...movable, ...flyable, ...gameObject }` **现实联系**:这些模式无处不在: - **React 组件**:属性(组合)与类继承 - **游戏引擎**:实体组件系统使用组合 - **移动应用**:UI 框架常用继承层级 ## 通信模式:发布/订阅系统 随着应用复杂度提升,管理组件间通信变得困难。发布-订阅模式(pub/sub)用类似广播的原理解决这个问题——一个发射器能触达多个接收器,无需知道谁在监听。 考虑英雄受伤时的情况:血条更新,音效播放,视觉反馈出现。英雄对象不用直接耦合这些系统,而是广播一个“受伤”消息。任何需要响应的系统都能订阅该消息并做出反应。 ✅ **Pub/Sub** 代表“发布-订阅” ```mermaid flowchart TD A[英雄受到伤害] --> B[发布:HERO_DAMAGED] B --> C[事件系统] C --> D[生命值条订阅者] C --> E[声音系统订阅者] C --> F[视觉效果订阅者] C --> G[成就系统订阅者] D --> H[更新生命值显示] E --> I[播放伤害音效] F --> J[显示红色闪光] G --> K[检查存活成就] style A fill:#ffebee style B fill:#e1f5fe style C fill:#e8f5e8 style H fill:#fff3e0 style I fill:#fff3e0 style J fill:#fff3e0 style K fill:#fff3e0 ``` ### 理解 Pub/Sub 架构 pub/sub 模式使应用的不同部分松散耦合,即它们能协作而不直接依赖彼此。这种分离让代码更易维护、测试和灵活应对变化。 **pub/sub 的关键角色:** - **消息** —— 简单的文本标签,例如 `'PLAYER_SCORED'` ,描述发生的事件(含额外信息) - **发布者** —— 向所有监听者“大声宣布”发生了什么 - **订阅者** —— 说“我关注这个事件”,并在事件发生时响应 - **事件系统** —— 确保消息送达正确监听者的中间人 ### 构建事件系统 让我们创建一个简单而强大的事件系统,演示这些概念: ```javascript // 第一步:创建 EventEmitter 类 class EventEmitter { constructor() { this.listeners = {}; // 存储所有事件监听器 } // 为特定消息类型注册监听器 on(message, listener) { if (!this.listeners[message]) { this.listeners[message] = []; } this.listeners[message].push(listener); } // 向所有注册的监听器发送消息 emit(message, payload = null) { if (this.listeners[message]) { this.listeners[message].forEach(listener => { listener(message, payload); }); } } } ``` **拆解发生的事情:** - **创建**一个简单类实现的中心事件管理系统 - **按消息类型**在对象中存储监听器 - **使用** `on()` 方法注册新监听器 - **用** `emit()` 方法广播消息到所有感兴趣的监听者 - **支持**可选的数据负载以传递相关信息 ### 综合示例演示 好,让我们看看实际应用!我们构建一个简单的移动系统,展示 pub/sub 的清晰灵活性: ```javascript // 第一步:定义你的消息类型 const Messages = { HERO_MOVE_LEFT: 'HERO_MOVE_LEFT', HERO_MOVE_RIGHT: 'HERO_MOVE_RIGHT', ENEMY_SPOTTED: 'ENEMY_SPOTTED' }; // 第二步:创建你的事件系统和游戏对象 const eventEmitter = new EventEmitter(); const hero = createHero(0, 0); ``` **这段代码作用:** - **定义**常量对象防止消息名拼写错误 - **创建**事件发射器实例处理所有通信 - **初始化**位于起始位置的英雄对象 ```javascript // 第三步:设置事件监听器(订阅者) eventEmitter.on(Messages.HERO_MOVE_LEFT, () => { hero.moveTo(hero.x - 5, hero.y); console.log(`Hero moved to position: ${hero.x}, ${hero.y}`); }); eventEmitter.on(Messages.HERO_MOVE_RIGHT, () => { hero.moveTo(hero.x + 5, hero.y); console.log(`Hero moved to position: ${hero.x}, ${hero.y}`); }); ``` **上述代码实现:** - **注册**响应移动消息的事件监听器 - **根据移动方向**更新英雄位置 - **添加**控制台日志跟踪英雄位置变化 - **移动逻辑**与输入处理分离 ```javascript // 第4步:将键盘输入连接到事件(发布者) window.addEventListener('keydown', (event) => { switch(event.key) { case 'ArrowLeft': eventEmitter.emit(Messages.HERO_MOVE_LEFT); break; case 'ArrowRight': eventEmitter.emit(Messages.HERO_MOVE_RIGHT); break; } }); ``` **进一步理解:** - **连接**键盘输入与游戏事件,避免紧耦合 - **使**输入系统可间接与游戏对象通信 - **允许**多个系统响应同一键盘事件 - **便于**更改键绑定或添加新输入方法 ```mermaid sequenceDiagram participant User participant Keyboard participant EventEmitter participant Hero participant SoundSystem participant Camera User->>Keyboard: 按下 左箭头 Keyboard->>EventEmitter: emit('HERO_MOVE_LEFT') EventEmitter->>Hero: 向左移动5像素 EventEmitter->>SoundSystem: 播放脚步声 EventEmitter->>Camera: 跟随英雄 Hero->>Hero: 更新位置 SoundSystem->>SoundSystem: 播放音频 Camera->>Camera: 调整视口 ``` > 💡 **专业提示**:此模式的美妙在于灵活性!你只需添加更多事件监听器即可添加音效、屏幕抖动或粒子效果——无需修改现有的键盘或移动代码。 > **你会喜欢此方法的原因:** - 新功能添加变得非常简单——只需监听感兴趣事件 - 多个模块可响应同一事件,互不干扰 - 测试更加简单,因为各部分独立工作 - 出现问题时,知道准确排查位置 ### Pub/Sub 为什么能有效扩展 随着应用复杂性增加,pub/sub 模式保持简洁。无论是管理数十个敌人、动态 UI 更新还是音效系统,该模式在不改变架构的情况下处理规模增大。新功能可无缝集成进现有事件系统,不影响已建立功能。 > ⚠️ **常见错误**:早期不要创建过多具体消息类型。先用大类分类,随着游戏需求明确再细化。 > **最佳实践:** - **将相关消息分组归类** - **使用描述性名称清晰表达事件** - **保持消息负载简单聚焦** - **为团队协作记录消息类型文档** ### 🔄 **教学自检** **事件驱动架构理解**:确认你掌握整体系统: - ✅ pub/sub 模式如何防止组件间紧耦合? - ✅ 为什么事件驱动架构更易添加新功能? - ✅ EventEmitter 在通信流程中起什么作用? - ✅ 消息常量如何防止错误并提升可维护性? **设计挑战**:你如何用 pub/sub 处理这些游戏场景? 1. **敌人死亡**:更新得分,播放音效,生成道具,移除屏幕 2. **关卡完成**:停止音乐,显示 UI,保存进度,加载下一关 3. **道具收集**:增强能力,更新 UI,播放效果,启动计时 **专业联系**:此模式应用于: - **前端框架**:React/Vue 事件系统 - **后端服务**:微服务通信 - **游戏引擎**:Unity 事件系统 - **移动开发**:iOS/Android 通知系统 --- ## GitHub Copilot Agent 挑战 🚀 使用 Agent 模式完成以下挑战: **描述:** 使用继承和发布/订阅模式创建一个简单的游戏对象系统。你将实现一个基本游戏,其中不同对象通过事件进行通信而无需直接了解彼此。 **提示:** 创建一个 JavaScript 游戏系统,要求如下:1)创建一个基础 GameObject 类,含 x, y 坐标和 type 属性。2)创建一个继承自 GameObject 且可以移动的 Hero 类。3)创建一个继承自 GameObject 且能追踪英雄的 Enemy 类。4)实现一个用于发布/订阅模式的 EventEmitter 类。5)设置事件监听器,当英雄移动时,附近敌人接收 'HERO_MOVED' 事件并更新位置向英雄移动。使用 console.log 展示对象间的通信。 了解更多[Agent 模式](https://code.visualstudio.com/blogs/2025/02/24/introducing-copilot-agent-mode)。 ## 🚀 挑战 考虑发布-订阅模式如何增强游戏架构。确定哪些组件应发出事件以及系统应如何响应。设计一个游戏概念并绘制其组件之间的通信模式。 ## 课后测验 [课后测验](https://ff-quizzes.netlify.app/web/quiz/30) ## 复习与自学 通过[阅读相关内容](https://docs.microsoft.com/azure/architecture/patterns/publisher-subscriber/?WT.mc_id=academic-77807-sagibbon)深入了解发布/订阅。 ### ⚡ **接下来5分钟你可以做的事情** - [ ] 打开任何在线HTML5游戏,使用开发者工具检查其代码 - [ ] 创建一个简单的HTML5 Canvas元素并绘制基本形状 - [ ] 尝试使用`setInterval`创建简单动画循环 - [ ] 浏览Canvas API文档,尝试某个绘图方法 ### 🎯 **本小时你可以完成的任务** - [ ] 完成课后测验并理解游戏开发概念 - [ ] 搭建包含HTML、CSS和JavaScript文件的游戏项目结构 - [ ] 创建一个持续更新和渲染的基础游戏循环 - [ ] 在画布上绘制你的第一个游戏精灵 - [ ] 实现基础的图片和声音资源加载 ### 📅 **你的一周游戏创作计划** - [ ] 完成包含所有计划功能的完整太空游戏 - [ ] 添加精细的图形、音效和流畅动画 - [ ] 实现游戏状态(开始界面、游戏中、游戏结束) - [ ] 创建计分系统和玩家进度追踪 - [ ] 让游戏响应式兼容多设备 - [ ] 在线分享游戏并收集玩家反馈 ### 🌟 **你的一月游戏开发计划** - [ ] 开发多个不同类型和机制的游戏 - [ ] 学习使用像Phaser或Three.js这样的游戏开发框架 - [ ] 参与开源游戏开发项目 - [ ] 精通高级游戏编程模式和优化技巧 - [ ] 创建展示游戏开发技能的作品集 - [ ] 指导对游戏开发和互动媒体感兴趣的新人 ## 🎯 你的游戏开发精通时间表 ```mermaid timeline title 游戏架构学习进度 section 对象模式(20分钟) 代码组织:类继承 :组合模式 :工厂函数 :行为混合 section 通信系统(25分钟) 事件架构:发布/订阅实现 :消息设计 :事件发射器 :松耦合 section 游戏对象设计(30分钟) 实体系统:属性管理 :行为组合 :状态处理 :生命周期管理 section 架构模式(35分钟) 系统设计:组件系统 :观察者模式 :命令模式 :状态机 section 高级概念(45分钟) 可扩展架构:性能优化 :内存管理 :模块化设计 :测试策略 section 游戏引擎概念(1周) 专业开发:场景图 :资源管理 :渲染管线 :物理整合 section 框架精通(2周) 现代游戏开发:React游戏模式 :Canvas优化 :WebGL基础 :PWA游戏 section 行业实践(1个月) 专业技能:团队协作 :代码审查 :游戏设计模式 :性能剖析 ``` ### 🛠️ 你的游戏架构工具包总结 完成本课后,你已有: - **设计模式精通**:理解继承与组合的权衡 - **事件驱动架构**:发布/订阅实现的可扩展通信 - **面向对象设计**:类层级和行为组合 - **现代JavaScript**:工厂函数、展开语法以及ES6+模式 - **可扩展架构**:低耦合和模块化设计原则 - **游戏开发基础**:实体系统和组件模式 - **专业模式**:业界标准代码组织方法 **现实应用场景**:这些模式适用于: - **前端框架**:React/Vue组件架构和状态管理 - **后端服务**:微服务通信和事件驱动系统 - **移动开发**:iOS/Android应用架构和通知系统 - **游戏引擎**:Unity、Unreal及基于Web的游戏开发 - **企业软件**:事件溯源和分布式系统设计 - **API设计**:RESTful服务和实时通信 **获得的专业技能**:你现在能: - **设计**可扩展的软件架构,使用成熟的设计模式 - **实现**能够处理复杂交互的事件驱动系统 - **选择**适合不同场景的代码组织策略 - **调试**并维护低耦合系统 - **沟通**使用业界标准术语表达技术决策 **下一步**:你已准备好在实际游戏中实现这些模式,探索高级游戏开发主题,或将这些架构理念应用于网页应用! 🌟 **成就解锁**:你已掌握支持从简单游戏到复杂企业系统的基础软件架构模式! ## 任务 [设计一个游戏](assignment.md) --- **免责声明**: 本文件由 AI 翻译服务 [Co-op Translator](https://github.com/Azure/co-op-translator) 翻译而成。尽管我们力求准确,但请注意自动翻译可能存在错误或不准确之处。原始语言版本的文件应被视为权威来源。对于重要信息,建议使用专业人工翻译。我们不对因使用本翻译而产生的任何误解或误释承担责任。