news 2026/9/11 9:50:21

Web-Dev-For-Beginners 空间游戏课程作业:用继承、组合与 Pub/Sub 事件系统打造游戏原型

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Web-Dev-For-Beginners 空间游戏课程作业:用继承、组合与 Pub/Sub 事件系统打造游戏原型

Web-Dev-For-Beginners 空间游戏课程作业:用继承、组合与 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 开源课程6-space-game/1-introduction(Build a Space Game Part 1: Introduction)的结课作业展开:使用 JavaScript 设计模式搭建一个可运行的简单游戏原型。你将学会在继承与组合两种对象组织方式之间做出选择,实现一个完整的EventEmitter发布-订阅(pub/sub)通信系统,并把键盘/鼠标输入、游戏对象与事件流串联起来。读完本文,你可以独立完成一套"功能可用、架构清晰、易于扩展"的事件驱动游戏原型,并对照仓库中的评分标准自查交付质量。

作业目标:把设计模式真正用起来

该作业对应课程第一课 课程讲义:空间游戏引言 的结尾任务,其核心目标是:将本课学到的两类知识落成代码——即架构层面的"继承 / 组合"对象组织方式,与通信层面的"pub/sub 事件系统"。

课程讲义用一个非常形象的比喻解释了本作业的价值:游戏对象(英雄、敌人、道具、爆炸特效)虽然外观迥异,但共享四类基本行为——存在(拥有xy坐标)、可移动(许多对象会改变位置)、有生命周期(部分对象短暂存活)、会响应交互(碰撞、拾取、状态更新)。本作业要求你把这些抽象落到一个真实可玩的小型游戏里,从而理解这些模式如何支撑从简单脚本到大型应用的演进。

现实参照:pub/sub 的"发布-订阅"通信思路,与 React/Vue 前端事件系统、后端微服务通信、Unity 事件系统等业界实践同源。本作业正是为这些场景打地基。

二选一:继承还是组合?

作业要求你从两种架构模式中明确选择一种来实现:

  • 方案 A:基于类的继承。典型层级为GameObjectMovableHero,用"is-a"(英雄一个可移动对象)关系组织代码。
  • 方案 B:基于组合。使用工厂函数 + 行为混合(behavior mixing),用"has-a"(英雄拥有移动能力)关系按需拼装对象。

课程讲义给出了两套完整示例,这里复述并拆解其要点,作为作业实现的参照:

// —— 方案 A:类继承 —— // Step 1: 基础 GameObject 类,保存位置与类型 class GameObject { constructor(x, y, type) { this.x = x; this.y = y; this.type = type; } } // Step 2: 通过继承增加移动能力 class Movable extends GameObject { constructor(x, y, type) { super(x, y, type); // 调用父类构造器,初始化继承的属性 } moveTo(x, y) { this.x = x; this.y = y; } } // Step 3: 创建具体的游戏对象类型 class Hero extends Movable { constructor(x, y) { super(x, y, 'Hero'); // 自动设定类型 } } class Tree extends GameObject { constructor(x, y) { super(x, y, 'Tree'); // 树不需要移动能力 } } // Step 4: 使用 const hero = new Hero(0, 0); hero.moveTo(5, 5); // 英雄可以移动 const tree = new Tree(10, 15); // tree.moveTo() 会报错 —— 树不能移动,层级关系阻止了错误行为
// —— 方案 B:组合 / 工厂函数 —— // Step 1: 基础行为对象 const gameObject = { x: 0, y: 0, type: '' }; const movable = { moveTo(x, y) { this.x = x; this.y = y; } }; // Step 2: 用展开语法合并行为 const movableObject = { ...gameObject, ...movable }; // Step 3: 工厂函数按需定制对象 function createHero(x, y) { return { ...movableObject, x, y, type: 'Hero' }; } function createStatic(x, y, type) { return { ...gameObject, x, y, type }; } // Step 4: 使用 const hero = createHero(10, 10); hero.moveTo(5, 5); // 正常 const tree = createStatic(0, 0, 'Tree'); // tree.moveTo 是 undefined —— 没有组合进移动行为

选型建议(来自课程讲义)

  • 存在清晰的 "is-a" 关系时选继承(Herois-aMovable);
  • 需要 "has-a" 关系与最大灵活性时选组合(Herohasmovement);
  • 同一应用中可以混合使用两种方式;还需考虑团队习惯与项目需求。

仓库中的实际实现可以印证方案 A 的可行性。例如 移动元素一课的解决方案 app.js 中,Hero extends GameObjectEnemy extends GameObject,基类GameObject保存xydeadtypewidthheightimg并统一提供draw(ctx)方法,子类通过super(x, y)复用基类构造器并追加自己的属性(如Hero.speedEnemy的自动下移逻辑)——这正是作业要求"有清晰类/对象层级"的评分标准所指的实现形态。

进阶练习(讲义自测题):如果要创建一个既能移动又能飞行的 Flying Enemy,继承方案是class FlyingEnemy extends Movable,组合方案则是{ ...movable, ...flyable, ...gameObject }

构建 EventEmitter:pub/sub 通信的核心

作业要求包含一个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); }); } } }

这一实现的四个关键点:

  1. 集中管理:所有监听器存在一个以消息名为键的对象中,形成唯一的事件中枢;
  2. on()订阅:为指定消息追加监听函数,重复调用可注册多个监听者;
  3. emit()发布:把消息广播给该类型下的全部监听者,支持可选的payload参数传递相关信息;
  4. 解耦:发布者不需要知道谁在监听,订阅者也不需要知道消息从何而来。

仓库中的正式解决方案与讲义实现完全一致(见 移动元素解决方案):on负责收集监听器,emit(message, payload)的形式逐个调用监听函数。后续课程进一步展示了该模式的伸缩性——在 终局判定一课的解决方案 app.js 中,同一个EventEmitter类被原样复用,通过注册更多消息类型支撑起完整的对局流程,这正是作业"可扩展架构"加分项的最佳示范。

设计消息类型:至少 2–3 种,从常量开始

作业要求设定至少 2–3 种不同的消息类型(例如PLAYER_MOVEENEMY_SPAWNSCORE_UPDATE),并建议用常量对象集中定义,以杜绝拼写错误并提升可维护性。讲义中的示范:

const Messages = { HERO_MOVE_LEFT: 'HERO_MOVE_LEFT', HERO_MOVE_RIGHT: 'HERO_MOVE_RIGHT', ENEMY_SPOTTED: 'ENEMY_SPOTTED' };

仓库中的实际项目展示了消息类型如何随游戏复杂度自然生长。早期阶段(移动元素解决方案)只有方向键事件:

const Messages = { KEY_EVENT_UP: 'KEY_EVENT_UP', KEY_EVENT_DOWN: 'KEY_EVENT_DOWN', KEY_EVENT_LEFT: 'KEY_EVENT_LEFT', KEY_EVENT_RIGHT: 'KEY_EVENT_RIGHT', };

到终局判定阶段(终局判定解决方案),消息类型扩展为输入、碰撞、胜负三类共 10 种:

const Messages = { KEY_EVENT_ENTER: 'KEY_EVENT_ENTER', KEY_EVENT_UP: 'KEY_EVENT_UP', KEY_EVENT_DOWN: 'KEY_EVENT_DOWN', KEY_EVENT_LEFT: 'KEY_EVENT_LEFT', KEY_EVENT_RIGHT: 'KEY_EVENT_RIGHT', KEY_EVENT_SPACE: 'KEY_EVENT_SPACE', COLLISION_ENEMY_LASER: 'COLLISION_ENEMY_LASER', COLLISION_ENEMY_HERO: 'COLLISION_ENEMY_HERO', GAME_END_LOSS: 'GAME_END_LOSS', GAME_END_WIN: 'GAME_END_WIN', };

注意它的演进轨迹完全符合讲义中的最佳实践警告:不要一开始就定义过多过细的消息,先使用宽泛类别,随需求明朗再细化COLLISION_ENEMY_LASER等消息还携带payload(如{ enemy }{ first, second })来传递碰撞双方的信息,这是消息设计的第二个要点:payload 保持简单聚焦,只带必要数据。

在事件驱动架构下,碰撞检测系统只负责发布COLLISION_ENEMY_LASER,而得分更新、爆炸特效、音效播放等系统各自订阅并响应同一事件——多个订阅者互不知晓、互不干扰,这就是解耦的直接收益(见 终局判定解决方案 中多监听器订阅同一碰撞消息的写法)。

打通输入:键盘/鼠标事件如何驱动游戏事件

作业要求把用户输入(键盘/鼠标)连接到游戏事件。实现路径分两步:监听原生输入事件作为"发布者",用emit广播;游戏对象作为"订阅者"用on注册响应。

课程讲义中的最小示例:

// 发布端:原生键盘事件 → 游戏消息 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; } });

仓库解决方案中这条链路被完整落地(移动元素解决方案 app.js):

// 发布端:keydown 中根据按键 emit 对应消息 window.addEventListener('keydown', (event) => { if (event.key === 'ArrowUp') eventEmitter.emit(Messages.KEY_EVENT_UP); if (event.key === 'ArrowDown') eventEmitter.emit(Messages.KEY_EVENT_DOWN); if (event.key === 'ArrowLeft') eventEmitter.emit(Messages.KEY_EVENT_LEFT); if (event.key === 'ArrowRight') eventEmitter.emit(Messages.KEY_EVENT_RIGHT); }); // 订阅端:英雄移动逻辑 eventEmitter.on(Messages.KEY_EVENT_UP, () => { hero.y -= 5; });

这种间接通信带来三个实际好处(讲义原话要点):

  • 改键位零成本:想换键位只需改keydown的映射,游戏逻辑毫发无损;
  • 一个输入、多个响应:同一个KEY_EVENT_UP可以被移动、音效、镜头跟随等多个系统同时订阅;
  • 可测试性提升:每个订阅者独立工作,出现问题可以准确定位到具体监听器。

加分项提示:作业明确把"多种输入方法(键盘鼠标事件)"列为加分项。如果你采用讲义中Pong变体的建议,鼠标移动控制球拍就是天然的鼠标输入用例。

游戏元素与交互建议

作业对游戏内容的最低要求是:

  1. 至少一个玩家控制的角色
  2. 至少一个其他游戏对象(敌人、可收集道具或环境元素);
  3. 对象间的基本交互(碰撞、收集或通信)。

讲义给出的四个可选原型方向,均适合作为作业载体:

建议游戏核心机制可用消息示例
贪吃蛇(Snake)蛇身跟随蛇头,食物随机出现PLAYER_MOVEFOOD_EATEN
Pong 变体球拍响应输入,球体弹墙PADDLE_MOVEBALL_BOUNCESCORE_UPDATE
收集游戏玩家移动拾取物品、躲避障碍PLAYER_MOVEITEM_COLLECTEDOBSTACLE_HIT
塔防基础(Tower Defense)防御塔探测并射击移动敌人ENEMY_SPAWNTOWER_FIREENEMY_DIED

仓库成品 空间游戏完整解决方案 可以视作"满分作业"的参考:它以playerbulletsenemiesparticles四类对象构成世界,spawnEnemy()在屏幕四边随机生成敌人(见 app.js),fireBullet()在按住空格时按冷却时间发射子弹,update()中通过距离平方比较实现子弹-敌人、敌人-玩家的碰撞判定,gameOver()localStorage持久化最高分。这个实现证明了:即使不引入重型框架,仅凭原生 Canvas 与清晰的对象组织就能构建完整可玩、可扩展的游戏循环。

代码结构指南(作业给定骨架)

作业给出了推荐的起始结构,请在你的实现中沿用:

// 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 */ }

一个符合要求的原型结构可以组织为:Messages常量 →EventEmitter类 → 游戏对象定义(继承或组合二选一)→eventEmitter实例 → 键盘/鼠标监听(发布端)→ 各对象on()订阅(订阅端)→ 简单requestAnimationFramesetInterval驱动渲染。仓库中的完整流程可对照 移动元素解决方案 app.js 全文件研读。

验证你的实现:四条自测标准

作业明确要求对实现进行测试,验证清单如下:

  • 对象随事件移动或改变:发布移动消息后,对象位置确实更新;
  • 多对象响应同一事件:同一个消息被两个以上监听器订阅,且全部正常触发;
  • 无需改动旧代码即可添加新行为:新增一个on()订阅就能接入新系统(这是 pub/sub 的核心卖点);
  • 键盘/鼠标输入正确触发游戏事件:模拟按键,确认emit链路的每一步都生效。

讲义给出的测试技巧是让每个订阅者独立可测:例如移动逻辑只依赖hero.moveTo(),那么单独调用监听器即可验证坐标变化,无需依赖浏览器事件。

提交清单与评分标准

作业的交付物要求如下:

  1. JavaScript 文件:包含游戏实现(可拆分为多个文件);
  2. HTML 文件:用于运行和测试游戏(可以非常简陋);
  3. 注释:说明你选择了哪种模式及理由;
  4. 简要文档:列出你的消息类型及其功能。

评分标准(原文完整保留):

标准优秀(3 分)合格(2 分)待改进(1 分)
架构模式正确实现继承或组合,类/对象层级清晰使用了所选模式,但存在小问题或不一致尝试使用模式,但实现存在明显问题
Pub/Sub 实现EventEmitter 正常工作,支持多种消息类型且事件流正确基础 pub/sub 可用,具备部分事件处理存在事件系统,但运行不可靠
游戏功能三个及以上通过事件通信的交互元素两个交互元素,具备基本事件通信仅一个元素响应事件或基本交互
代码质量代码整洁、注释完善、组织逻辑清晰且使用现代 JavaScript整体组织良好,注释充分代码可运行,但缺乏组织或清晰注释

加分项

  • 展示模式巧妙用法的创意游戏机制
  • 多种输入方式(键盘与鼠标事件并用);
  • 易于扩展新功能的可扩展架构

小结

本作业是空间游戏课程系列的"架构热身":它不要求华丽画面,只要求你用继承或组合把对象组织清楚,用EventEmitter把通信解耦。完成它之后,你将拥有可直接迁移到前端框架、后端服务与任何事件驱动系统中的三项能力:对象结构设计、发布-订阅实现、输入到事件的链路搭建。仓库中从 第一课讲义 到 移动元素、终局判定 乃至 完整成品 的逐级演进,就是这套作业方法论最直观的参考答案。

【免费下载链接】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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 9:49:47

复现OPERA多模态幻觉抑制:解码策略与工程踩坑全记录

复现OPERA这篇论文,算是我今年做得最折腾也最有收获的一件事。当时在实验室里从周一开始拉代码,一直到周五晚上才把完整的评估流程跑通,中间经历了环境崩溃、显存爆炸、生成结果全是乱码、指标对不上等一系列问题。这篇算是一个补记&#xff…

作者头像 李华
网站建设 2026/9/11 9:48:33

3 步在 AMD 显卡上免费跑起 CUDA 程序:ZLUDA 完整配置教程

3 步在 AMD 显卡上免费跑起 CUDA 程序:ZLUDA 完整配置教程 【免费下载链接】ZLUDA CUDA on non-NVIDIA GPUs 项目地址: https://gitcode.com/GitHub_Trending/zl/ZLUDA ZLUDA 是一个把 CUDA 应用重定向到非 NVIDIA 显卡(以 AMD GPU 为主&#xff…

作者头像 李华
网站建设 2026/9/11 9:48:18

Agno Accuracy Eval 实战指南:用 LLM-as-Judge 量化 Agent 回答准确率

Agno Accuracy Eval 实战指南:用 LLM-as-Judge 量化 Agent 回答准确率 【免费下载链接】agno Build, run, and manage agent platforms. 项目地址: https://gitcode.com/GitHub_Trending/ag/agno 本文围绕 agno 的 accuracy 评估能力展开,聚焦 co…

作者头像 李华
网站建设 2026/9/11 9:46:29

adc采样

ADC采样需要对采样信号加跟随器,因为在对信号采样时,ADC的输入电阻会变小,如果信号的阻抗比较大,则会呗分压,导致电压发生变化,引起采样数据不稳定的现象。ADC主要参数:动态范围DR:指…

作者头像 李华
网站建设 2026/9/11 9:44:39

context-mode:大模型上下文管理的核心工程实践

前阵子在调一个企业知识库问答应用,遇到一个特别典型的问题:用户明明在前几轮对话里强调过“我是财务部门的,预算口径按年度算”,结果模型聊到后面,开始用自然年口径回答,甚至把另一个部门的数据也算了进来…

作者头像 李华