# 构建太空游戏第一部分:介绍 ![video](../../../../6-space-game/images/pewpew.gif) ## 课前测验 [课前测验](https://ff-quizzes.netlify.app/web/quiz/29) ### 游戏开发中的继承与组合 在之前的课程中,由于项目规模较小,几乎不需要考虑应用程序的设计架构。然而,当你的应用程序规模和范围扩大时,架构决策就变得更加重要。在 JavaScript 中创建大型应用程序有两种主要方法:*组合* 或 *继承*。两者各有优缺点,但我们可以通过游戏的背景来解释它们。 ✅ 最著名的编程书籍之一与[设计模式](https://en.wikipedia.org/wiki/Design_Patterns)有关。 在游戏中,你有 `游戏对象`,它们是屏幕上的对象。这意味着它们在笛卡尔坐标系中有一个位置,由 `x` 和 `y` 坐标来表示。当你开发游戏时,你会注意到所有的游戏对象都有一个标准属性,这些属性在你创建的每个游戏中都很常见,即: - **基于位置** 大多数游戏元素都是基于位置的。这意味着它们有一个位置,即 `x` 和 `y`。 - **可移动** 这些对象可以移动到新位置。通常是英雄、怪物或 NPC(非玩家角色),但例如树这样的静态对象则不是。 - **自我销毁** 这些对象只存在一段时间,然后设置自己为删除状态。通常通过一个 `dead` 或 `destroyed` 的布尔值来表示,告诉游戏引擎该对象不再需要渲染。 - **冷却时间** “冷却时间”是短生命周期对象的典型属性。一个典型的例子是文本或图形效果(如爆炸),它们只需要显示几毫秒。 ✅ 想想像吃豆人这样的游戏。你能在这个游戏中识别出上述四种对象类型吗? ### 表达行为 我们上面描述的都是游戏对象可以拥有的行为。那么我们如何编码这些行为呢?我们可以通过与类或对象相关联的方法来表达这些行为。 **类** 使用 `类` 和 `继承` 的想法可以为类添加特定的行为。 ✅ 继承是一个重要的概念。可以通过[MDN关于继承的文章](https://developer.mozilla.org/docs/Web/JavaScript/Inheritance_and_the_prototype_chain)了解更多。 通过代码表达,一个游戏对象通常看起来像这样: ```javascript //set up the class GameObject class GameObject { constructor(x, y, type) { this.x = x; this.y = y; this.type = type; } } //this class will extend the GameObject's inherent class properties class Movable extends GameObject { constructor(x,y, type) { super(x,y, type) } //this movable object can be moved on the screen moveTo(x, y) { this.x = x; this.y = y; } } //this is a specific class that extends the Movable class, so it can take advantage of all the properties that it inherits class Hero extends Movable { constructor(x,y) { super(x,y, 'Hero') } } //this class, on the other hand, only inherits the GameObject properties class Tree extends GameObject { constructor(x,y) { super(x,y, 'Tree') } } //a hero can move... const hero = new Hero(); hero.moveTo(5,5); //but a tree cannot const tree = new Tree(); ``` ✅ 花几分钟重新构想一个吃豆人英雄(例如 Inky、Pinky 或 Blinky),并思考它如何用 JavaScript 编写。 **组合** 处理对象继承的另一种方法是使用 *组合*。然后,对象可以这样表达它们的行为: ```javascript //create a constant gameObject const gameObject = { x: 0, y: 0, type: '' }; //...and a constant movable const movable = { moveTo(x, y) { this.x = x; this.y = y; } } //then the constant movableObject is composed of the gameObject and movable constants const movableObject = {...gameObject, ...movable}; //then create a function to create a new Hero who inherits the movableObject properties function createHero(x, y) { return { ...movableObject, x, y, type: 'Hero' } } //...and a static object that inherits only the gameObject properties function createStatic(x, y, type) { return { ...gameObject x, y, type } } //create the hero and move it const hero = createHero(10,10); hero.moveTo(5,5); //and create a static tree which only stands around const tree = createStatic(0,0, 'Tree'); ``` **我应该使用哪种模式?** 选择哪种模式完全取决于你自己。JavaScript 支持这两种范式。 -- 在游戏开发中,还有一种常见模式用于处理游戏的用户体验和性能问题。 ## 发布/订阅模式 ✅ Pub/Sub 代表“发布-订阅” 这种模式解决了应用程序的不同部分不应该相互了解的问题。为什么呢?如果各部分是分离的,整体上会更容易理解发生了什么。同时,如果需要突然改变行为,也会更容易实现。我们如何做到这一点呢?通过建立以下概念: - **消息**:消息通常是一个文本字符串,伴随一个可选的负载(用于说明消息内容的一段数据)。游戏中的典型消息可以是 `KEY_PRESSED_ENTER`。 - **发布者**:这个元素*发布*消息并将其发送给所有订阅者。 - **订阅者**:这个元素*监听*特定消息,并在接收到消息后执行某些任务,例如发射激光。 这种模式的实现虽然代码量很小,但却非常强大。以下是它的实现方式: ```javascript //set up an EventEmitter class that contains listeners class EventEmitter { constructor() { this.listeners = {}; } //when a message is received, let the listener to handle its payload on(message, listener) { if (!this.listeners[message]) { this.listeners[message] = []; } this.listeners[message].push(listener); } //when a message is sent, send it to a listener with some payload emit(message, payload = null) { if (this.listeners[message]) { this.listeners[message].forEach(l => l(message, payload)) } } } ``` 使用上述代码,我们可以创建一个非常小的实现: ```javascript //set up a message structure const Messages = { HERO_MOVE_LEFT: 'HERO_MOVE_LEFT' }; //invoke the eventEmitter you set up above const eventEmitter = new EventEmitter(); //set up a hero const hero = createHero(0,0); //let the eventEmitter know to watch for messages pertaining to the hero moving left, and act on it eventEmitter.on(Messages.HERO_MOVE_LEFT, () => { hero.move(5,0); }); //set up the window to listen for the keyup event, specifically if the left arrow is hit, emit a message to move the hero left window.addEventListener('keyup', (evt) => { if (evt.key === 'ArrowLeft') { eventEmitter.emit(Messages.HERO_MOVE_LEFT) } }); ``` 在上面的代码中,我们连接了一个键盘事件 `ArrowLeft` 并发送了 `HERO_MOVE_LEFT` 消息。我们监听该消息并将 `hero` 移动作为结果。这种模式的优势在于事件监听器和英雄彼此互不知晓。你可以将 `ArrowLeft` 重新映射到 `A` 键。此外,通过对事件发射器的 `on` 函数进行一些编辑,还可以在 `ArrowLeft` 上实现完全不同的行为: ```javascript eventEmitter.on(Messages.HERO_MOVE_LEFT, () => { hero.move(5,0); }); ``` 随着游戏规模的扩大和复杂性的增加,这种模式的复杂性保持不变,而你的代码仍然保持简洁。强烈推荐采用这种模式。 --- ## 🚀 挑战 思考发布-订阅模式如何增强游戏。哪些部分应该发出事件,游戏应该如何对它们做出反应?现在是发挥创意的机会,想象一个新游戏以及它的各个部分可能如何表现。 ## 课后测验 [课后测验](https://ff-quizzes.netlify.app/web/quiz/30) ## 复习与自学 通过[阅读相关内容](https://docs.microsoft.com/azure/architecture/patterns/publisher-subscriber/?WT.mc_id=academic-77807-sagibbon)了解更多关于发布/订阅模式的信息。 ## 作业 [设计一个游戏](assignment.md) --- **免责声明**: 本文档使用AI翻译服务 [Co-op Translator](https://github.com/Azure/co-op-translator) 翻译而成。尽管我们努力确保准确性,但请注意,自动翻译可能包含错误或不准确之处。应以原始语言的文档作为权威来源。对于关键信息,建议使用专业人工翻译。因使用本翻译而引起的任何误解或误读,我们概不负责。