用设计模式与 Pub/Sub 构建游戏原型:Web-Dev-For-Beginners 空间游戏系列实战作业完全指南
【免费下载链接】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 空间游戏课程第一课(Introduction)的配套作业展开:要求你用继承或组合两种架构模式,配合发布-订阅(Pub/Sub)事件系统,从零搭建一个可运行、可扩展的简单游戏原型。读完本文,你将掌握 EventEmitter 的完整实现思路、消息类型的设计规范、键盘/鼠标输入与游戏事件的解耦连接方式,并拿到一份可直接执行的验收清单与评分标准。
作业定位:为什么第一课就要搭原型
空间游戏系列共六课,第一课的主题是"Introduction",核心是两件事:用代码组织游戏对象(继承 vs 组合)和用 Pub/Sub 解耦系统间通信。作业要求你立刻把这两项能力落地为一个"模拟游戏"(Mock up a Game)——它不必有复杂美术,但必须在架构上自洽:
- 选择一种架构模式组织游戏对象;
- 用
EventEmitter搭建事件总线; - 定义 2~3 种以上的消息类型并让它们真正驱动游戏逻辑;
- 让用户输入(键盘/鼠标)通过事件系统影响游戏世界。
这与后续课程(Canvas 绘制、移动、碰撞检测、计分、结束条件)形成递进关系,最终目标是在本系列结束时完成一个完整的太空射击游戏,参考实现位于 6-space-game/solution/app.js 与 6-space-game/solution/index.html。
第一步:选定你的架构模式
游戏对象(Hero、Enemy、PowerUp、Bullet 等)都拥有坐标x、y、类型type,且普遍具备"存在某处、可移动、有生命周期、可交互"四类共性行为。组织这些行为有两种主流方式,作业要求二选一。
方案 A:类继承(is-a 关系)
对应课程文档 6-space-game/1-introduction/README.md 中的GameObject→Movable→Hero示例,逐层扩展:
// 基类:所有游戏对象的共同底座 class GameObject { constructor(x, y, type) { this.x = x; this.y = y; this.type = type; } } // 继承:在基类上追加移动能力 class Movable extends GameObject { constructor(x, y, type) { super(x, y, type); // 调用父类构造函数初始化 x/y/type } moveTo(x, y) { this.x = x; this.y = y; } } // 具体对象:自动把 type 设为 'Hero' class Hero extends Movable { constructor(x, y) { super(x, y, 'Hero'); } } const hero = new Hero(0, 0); hero.moveTo(5, 5); // 正常:Hero 可以移动继承的好处是"is-a"关系清晰、类型层级明确;代价是层级一旦过深就僵化——比如一棵树Tree直接继承GameObject而非Movable,它天然就没有moveTo(),这既是约束也是保护。
方案 B:组合(has-a 关系,工厂函数)
组合思路是把行为拆成独立对象,用展开语法按需拼装,对应课程中的"混入行为"写法:
// 基础属性与独立行为 const gameObject = { x: 0, y: 0, type: '' }; const movable = { moveTo(x, y) { this.x = x; this.y = y; } }; // 工厂函数:按需组合出具体对象 function createHero(x, y) { return { ...movable, ...gameObject, x, y, type: 'Hero' }; } function createStatic(x, y, type) { return { ...gameObject, x, y, type }; // 没有 movable,不能移动 } const hero = createHero(10, 10); hero.moveTo(5, 5); // 正常 const tree = createStatic(0, 0, 'Tree'); // tree.moveTo is undefined —— 未组合进移动行为组合的优势是灵活、无层级负担,一个"既能移动又能飞行"的对象只需{ ...movable, ...flyable, ...gameObject }即可拼出;课程文档也强调"可以在同一应用里混合使用两种思路"。
选型建议:存在清晰的"is-a"层级(如 Hero 就是一种 Movable)时用继承;需要"has-a"灵活拼装(如一个对象同时具备多种能力)时用组合。作业中无论选哪种,都要在代码注释里写清理由。
第二步:搭建 Pub/Sub 通信系统
课程把 Pub/Sub 比作广播电台:发布者不知道谁在收听,订阅者按消息类型各取所需,中间由事件系统负责路由。三个角色缺一不可:
- Messages:描述"发生了什么"的文本常量,如
'PLAYER_SCORED'; - Publishers:发出事件的源头(如键盘、碰撞检测);
- Subscribers:对某类事件感兴趣的监听者(如计分 UI、音效系统)。
实现 EventEmitter
作业要求包含一个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数据。这就是作业要求中的 pub/sub 消息机制核心。
定义消息类型常量
为防止字符串拼写错误,用常量对象集中管理消息类型。课程示例:
const Messages = { HERO_MOVE_LEFT: 'HERO_MOVE_LEFT', HERO_MOVE_RIGHT: 'HERO_MOVE_RIGHT', ENEMY_SPOTTED: 'ENEMY_SPOTTED' };作业要求至少 2~3 种消息类型,可参考PLAYER_MOVE、ENEMY_SPAWN、SCORE_UPDATE这类命名——统一用大写蛇形命名,语义上"动词+对象"最清晰。
连接输入与游戏事件
键盘事件只做"发布",不做任何游戏逻辑,这正是解耦的关键:
const eventEmitter = new EventEmitter(); const hero = createHero(0, 0); // 订阅:多个系统可同时响应同一事件 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); }); // 发布:键盘只负责 emit,不知道谁在听 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; } });后续要加音效、镜头跟随或屏幕震动,只需再on()一个监听者,键盘与移动代码一行都不用改。
第三步:设计游戏元素与交互
作业对游戏本体提出三条硬性要求:
- 至少一个玩家控制角色;
- 至少一个其他游戏对象(敌人、可收集物或环境元素);
- 对象间存在基本交互(碰撞、收集或通信)。
仓库完整实现 6-space-game/solution/app.js 可以作为架构参考:它用player对象保存坐标x/y、速度speed、半径r等属性(app.js#L81-L89),用bullets、enemies、particles三个数组管理实体,并在update()中以碰撞检测驱动得分、护盾、生命值等状态流转(app.js#L345-L399)——这种"输入收集 → 状态更新 → 碰撞交互 → HUD 刷新"的主循环,正是作业原型需要的最小闭环。
作业也给出了四个可选的游戏点子,按实现难度从低到高排列:
- 贪吃蛇(Snake):蛇身段跟随蛇头,食物随机生成;
- Pong 变体:挡板响应输入,球在墙壁间反弹;
- 收集游戏(Collector):玩家移动拾取物品、躲避障碍;
- 基础塔防(Tower Defense):防御塔探测并射击移动中的敌人。
第四步:按骨架代码起步
作业提供了一段起始结构,建议直接在此基础上填充:
// Example starting structure const Messages = { // Define your game messages here }; class EventEmitter { // Your event system implementation } // Choose either class-based OR composition approach // Class-based example: class GameObject { /* base properties */ } class Player extends GameObject { /* player-specific behavior */ } // OR Composition example: const gameObject = { /* base properties */ }; const movable = { /* movement behavior */ }; function createPlayer() { /* combine behaviors */ }注意作业中类继承示例用的是Player extends GameObject——这提示你不需要照搬课程的Movable中间层,只要继承关系清晰即可;组合路线则要求用工厂函数把行为拼装成完整对象。
第五步:测试你的实现
作业要求从四个维度验证代码确实工作:
- 事件触发时对象发生移动或状态变化——在控制台观察
hero坐标随按键改变; - 多个对象能响应同一事件——给同一消息挂两个以上监听者,确认都被调用;
- 新增行为无需改动既有代码——新功能只通过新增
on()监听实现,不改发布端; - 键盘/鼠标输入正确触发游戏事件——把真实按键/点击映射到
emit(),手动验证端到端链路。
课程文档对此有精辟总结:因为每个组件独立工作,测试变简单了;出问题时,你也知道该去哪里查。
提交清单与评分标准
作业的交付物共四件:
- JavaScript 文件——包含完整的游戏实现;
- HTML 文件——用于运行和测试游戏(可以很简陋);
- 注释——说明你选择了哪种模式以及为什么;
- 简短文档——列出你的消息类型及其作用。
评分表按四个维度、每项 1~3 分:
| 标准 | 优秀(3 分) | 合格(2 分) | 待改进(1 分) |
|---|---|---|---|
| 架构模式 | 正确实现继承或组合,类/对象层级清晰 | 使用了所选模式但有小问题或不一致 | 尝试使用模式但实现有明显问题 |
| Pub/Sub 实现 | EventEmitter 工作正常,多种消息类型且事件流正确 | 基础 pub/sub 可用,部分事件处理 | 事件系统存在但不可靠 |
| 游戏功能 | 三个以上通过事件通信的交互元素 | 两个交互元素、基础事件通信 | 仅一个元素响应事件或基础交互 |
| 代码质量 | 干净、注释充分、逻辑组织良好、现代 JavaScript | 大体组织良好、注释足够 | 能运行但缺乏组织或清晰注释 |
另有三个加分项:创意游戏机制、多种输入方式(键盘和鼠标)、易于扩展新功能的可伸缩架构。想在最后一项拿分,一个可实践的技巧是:把 HUD 更新(计分、生命值)全部写成SCORE_UPDATE、PLAYER_DAMAGED等消息的订阅者,而不是在游戏逻辑里直接改 DOM——这正是课程所倡导的"松散耦合"。
写在最后
这个作业的价值不在于画面多华丽,而在于让你亲手验证两件事:继承与组合如何影响代码的可扩展性,以及事件驱动架构如何让"加功能"变成"挂监听"。完成它之后,你就可以带着这套架构进入空间游戏系列的后续课程,用 Canvas 绘制、碰撞检测与计分系统把它一步步充实成完整的游戏。继续参考 6-space-game/1-introduction/README.md 的课后挑战,用 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
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考