Web-Dev-For-Beginners 实战:用 JavaScript 继承与 Pub/Sub 模式构建太空游戏的第一课
2026/9/24 11:44:59 网站建设 项目流程

Web-Dev-For-Beginners 实战:用 JavaScript 继承与 Pub/Sub 模式构建太空游戏的第一课

【免费下载链接】Web-Dev-For-Beginners24 Lessons, 12 Weeks, Get Started as a Web Developer项目地址: https://gitcode.com/GitHub_Trending/we/Web-Dev-For-Beginners

本课是 Web-Dev-For-Beginners 项目中「构建太空游戏(Build a Space Game)」六课系列的开篇(对应仓库 6-space-game/1-introduction/README.md,本文章基于德语译本整理)。读者将掌握两类最核心的代码组织方式——继承(Inheritance)与组合(Composition)——以及一套让组件互相通信却互不依赖的Pub/Sub 发布-订阅事件系统。学完本课,你将能用纯 JavaScript 设计出可扩展、易维护的游戏对象模型,并为后续在 Canvas 上绘制飞船、处理碰撞、计分打下架构基础。

整个系列最终会产出一个类似《Space Invaders》的可玩太空射击游戏,成品效果见 6-space-game/images/pewpew.gif;六课完整清单见 6-space-game/README.md。

课程定位:软件架构不只是游戏的专利

就像 NASA 的任务控制中心要在发射时协调多个子系统,一个太空游戏也要让程序的各个部分无缝协作。本课讨论的两种代码组织方式——继承与组合——并不是学术概念,它们正是从电子游戏到银行系统都在使用的真实模式;而 Pub/Sub 通信系统的工作原理则类似航天器上的通信网络,允许不同组件共享信息却不产生依赖

系列导航:本课为 Theory(理论)部分,后续 Practice(实践)部分包括 绘制到 Canvas、让元素移动、碰撞检测、计分 与 结束与重启游戏。

游戏对象:它们在屏幕上到底是什么

在任何游戏中都存在game objects(游戏对象)——英雄、敌人、能量道具、视觉特效,它们都通过xy坐标存在于屏幕上,就像在坐标系中描点。尽管外观千差万别,这些对象往往共享四类基础行为:

  • 它们存在于某处:每个对象都有 x、y 坐标,游戏据此决定在哪里绘制它;
  • 许多对象可以移动:英雄奔跑、敌人追击、子弹划过屏幕;
  • 它们有生命周期:有的常驻,有的(如爆炸)短暂出现后消失;
  • 它们会对事物作出反应:碰撞发生时,能量道具被收集、血条被更新。

可以设想一下《Pac-Man》:其中的幽灵、豆子、能量豆、水果分别对应上面哪一类对象?

在仓库的最终实现 6-space-game/solution/app.js 中可以看到真实落地:玩家被建模为一个带xy、半径r、速度speed和速度分量vx/vy的实体对象,而子弹、敌人、粒子则各自用数组bulletsenemiesparticles管理——这正是「对象有属性、有行为、有生命周期」的直观体现。

用类与继承表达行为

类的分类思想与林奈的生物分类系统类似:先有一个包含公共属性的基类,再由子类继承基础并追加特定能力。以 JavaScript 为例:

// 步骤 1:创建基类 GameObject class GameObject { constructor(x, y, type) { this.x = x; this.y = y; this.type = type; } }

这个类保存了对象的位置(xy)和类型(type),是所有游戏对象的共同地基。

// 步骤 2:通过继承加入移动能力 class Movable extends GameObject { constructor(x, y, type) { super(x, y, type); // 调用父类构造函数 } moveTo(x, y) { this.x = x; this.y = y; } }

这里用extends扩展了GameObject,通过super()初始化继承而来的属性,并新增moveTo()更新对象位置。

// 步骤 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() 会报错 —— 树不能移动

这一小段代码揭示了继承的四个要点:创建继承合适行为的特化对象类型;继承允许选择性加入功能;类层级天然阻止不合适的操作(树不暴露moveTo);英雄可动而树保持静止。可以花几分钟尝试用 JavaScript 重新设计一个《Pac-Man》幽灵(如 Inky、Pinky、Blinky)。

用组合与工厂函数表达行为

组合遵循模块化设计哲学——像工程师用可互换组件设计航天器一样,不靠继承父类,而是把特定行为拼装成对象,按需拥有恰好所需的功能,摆脱僵化的层级约束。

// 步骤 1:创建基础行为对象 const gameObject = { x: 0, y: 0, type: '' }; const movable = { moveTo(x, y) { this.x = x; this.y = y; } };

这里把位置数据与移动逻辑拆开,各自独立。

// 步骤 2:用 Spread 语法组合对象 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 }; }
// 步骤 4:创建并使用组合对象 const hero = createHero(10, 10); hero.moveTo(5, 5); // 正常工作! const tree = createStatic(0, 0, 'Tree'); // tree.moveTo() 是 undefined —— 没有组合进移动行为

组合方案的记忆要点:用行为混合(behavior mixing)而非继承来组装对象;比僵化的继承层级更灵活;让对象精确拥有所需功能;依托现代 JavaScript 的 Spread 语法实现干净的对象合并。

什么时候选哪种:一个象限决策框架

场景推荐方案原因
存在清晰的「is-a」关系(英雄是一个可移动对象)继承(class Hero extends Movable类层级天然表达父子关系
需要「has-a」关系(英雄拥有移动能力)组合({ ...movable, ...flyable, ...gameObject }灵活拼装、避免深层级
需要最大灵活性、频繁扩展行为组合 + 工厂函数无需改动已有类即可增删行为
团队约定与项目规范明确依团队/项目而定两种模式可以在同一应用内混用

从源码实现看,6-space-game/solution/app.js 中的settingsdifficultyTablegraphicsTable等配置对象正是「数据与行为分离、按需组合」思路的延续:难度、画质等能力通过查表而非多层继承来实现。

通信模式:Pub/Sub 发布-订阅系统

当应用变复杂,组件间的通信管理会成为难题。发布-订阅模式(Publish-Subscribe,即 Pub/Sub)借鉴无线电广播的原理——一个发射端可以到达多个接收端,而无需知道谁在收听

设想英雄受伤:血条要更新、音效要播放、视觉反馈要出现。与其让英雄对象直接耦合这些系统,Pub/Sub 允许英雄广播一条「受到伤害」的消息,任何需要响应的系统只需订阅该消息类型并做出反应。

Pub/Sub 中的四个关键角色:

  • 消息(Messages):简单的文本标签,如'PLAYER_SCORED',描述发生了什么,可附带额外信息;
  • 发布者(Publishers):向所有听众喊出「有事发生了!」的对象;
  • 订阅者(Subscribers):声明「我关心这个事件」并在事件发生时做出反应的对象;
  • 事件系统(Event System):确保消息送达正确听众的中介。

这种模式让应用各部分松散耦合(loosely coupled):它们能协同工作却不互相依赖,代码因此更易维护、更易测试、更抗变更。

亲手构建 EventEmitter 事件系统

下面这个简单却强大的事件系统完整演示了上述概念:

// 步骤 1:创建 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()向所有感兴趣者广播消息;可选payload参数用于传递相关信息。

实战串联:键盘输入 + 事件驱动移动

把前面学到的概念组装成一个可运行的移动系统:

// 步骤 1:定义消息类型(常量可防止拼写错误) const Messages = { HERO_MOVE_LEFT: 'HERO_MOVE_LEFT', HERO_MOVE_RIGHT: 'HERO_MOVE_RIGHT', ENEMY_SPOTTED: 'ENEMY_SPOTTED' }; // 步骤 2:创建事件系统与游戏对象 const eventEmitter = new EventEmitter(); const hero = createHero(0, 0); // 步骤 3:设置事件监听器(订阅者) 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}`); }); // 步骤 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; } });

这段代码的价值:键盘输入与游戏事件之间无紧密耦合;输入系统可间接与游戏对象通信;多个系统能响应同一键盘事件而互不干扰;更换按键映射或新增输入方式都很容易。

这个模式在实际仓库中得到了充分印证:6-space-game/solution/app.js 用Set维护按键状态、用keyMap映射 WASD/方向键/空格/Shift 等控制;update(dt)(见 6-space-game/solution/app.js)再依据按键集合计算玩家速度并更新坐标;主循环loop(ts)(见 6-space-game/solution/app.js)通过requestAnimationFrame驱动「更新世界 → 渲染」的每帧流程。输入、逻辑、渲染被拆成相互独立的部分,这正是事件驱动与松散耦合在真实游戏中的体现。

为什么 Pub/Sub 能良好扩展

随着应用复杂度上升,Pub/Sub 依然保持简单:无论管理几十个敌人、动态 UI 更新还是音效系统,模式都能承载规模增长而无需架构调整;新功能接入既有事件系统即可,不会破坏已有功能。

⚠️常见误区:不要在早期就创建过多过于具体的消息类型。先从宽泛的类别开始,等游戏需求清晰后再细化。

最佳实践:把相关消息归类到有意义的类别;使用能清楚表明「发生了什么」的描述性命名;保持消息 payload 简单聚焦;为团队协作而文档化消息类型。

设计挑战自测——如何用 Pub/Sub 处理这些场景?

  1. 敌人死亡:更新分数、播放音效、生成能量道具、从屏幕移除;
  2. 关卡完成:停止音乐、显示 UI、保存进度、加载下一关;
  3. 拾取能量道具:增强能力、更新 UI、播放特效、启动计时器。

课后实践与作业

  • GitHub Copilot Agent 挑战:在 Agent 模式下,让 Copilot 生成一个同时使用继承与 Pub/Sub 的小型游戏对象系统——要求包括:带x/ytypeGameObject基类、可移动的Hero类、能追踪英雄的Enemy类、EventEmitter类,以及「英雄移动时附近敌人收到HERO_MOVED事件并向英雄靠近」的监听逻辑,并输出console.log展示对象间通信。
  • 作业(Assignment):完成 6-space-game/1-introduction/assignment.md 中的「Mock up a Game」任务,可选类继承(GameObject → Movable → Hero)或组合(工厂函数混合行为)任一路线,实现至少 2~3 种消息类型与一种玩家交互,并撰写说明文档解释所选模式。
  • 5 分钟速练:用 DevTools 检查任意在线 HTML5 游戏的代码;创建一个简单 Canvas 元素并绘制基础图形;尝试用setInterval搭建简易动画循环。
  • 一小时目标:搭建由 HTML/CSS/JS 文件组成的游戏项目结构;编写持续更新与渲染的游戏循环;在 Canvas 上绘制第一批精灵;实现图片与音频的基础资源加载。

学习路线:从架构模式到完整游戏

本课对应时间线中的「对象模式(20 分钟)」与「通信系统(25 分钟)」两段。后续可按 6-space-game/README.md 的总览推进:对象模式之后是游戏对象设计(属性管理、行为组合、状态与生命周期)、架构模式(组件系统、观察者模式、命令模式、状态机),进而延伸到性能优化、场景图、渲染管线、物理集成,直至 Phaser/Three.js 等框架与 PWA/WebGL 进阶方向。

本课收获小结:掌握继承与组合的取舍;拥有可落地的 Pub/Sub 事件驱动架构;理解类层级与行为组合两种面向对象设计;熟悉工厂函数、Spread 语法等现代 JavaScript 手法;建立起松散耦合与模块化设计的可扩展架构意识。这些模式可直接迁移到前端框架(React/Vue 组件架构与状态管理)、后端微服务通信、移动端通知系统乃至企业级事件溯源与分布式架构之中。

【免费下载链接】Web-Dev-For-Beginners24 Lessons, 12 Weeks, Get Started as a Web Developer项目地址: https://gitcode.com/GitHub_Trending/we/Web-Dev-For-Beginners

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询