十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

用 JavaScript 从零构建太空射击游戏:Web-Dev-For-Beginners 六课实战指南

用 JavaScript 从零构建太空射击游戏:Web-Dev-For-Beginners 六课实战指南 用 JavaScript 从零构建太空射击游戏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 仓库中「太空游戏Space Game」系列课程即 translations/de/6-space-game/README.md 对应的英文原版 6-space-game/README.md为核心展开。该系列用 6 节课、12 周的课程体系中一个完整可玩的游戏项目带你掌握Canvas 绘图、笛卡尔坐标系、Pub/Sub 发布订阅架构、Async/Await 异步资源加载、键盘事件处理等进阶 JavaScript 基础。读完本文你不仅能理解这套太空射击游戏玩法灵感来自经典《太空侵略者》Space Invaders驾驶飞船、射击从上方来袭的怪物从静态画面到完整可玩游戏的全部实现路径还能直接运行仓库中的 完整解决方案 进行验证。一、课程全景六课进阶路线本系列遵循「理论先行、实践跟进」的教学结构仓库 6-space-game/README.md 给出了清晰的学习地图理论使用 JavaScript 构建游戏入门继承、组合与 Pub/Sub 架构实践在 Canvas 上绘制图形让元素在屏幕上移动碰撞检测计分系统游戏结束与重新开始六节课环环相扣先用第 1 课打牢对象组织与事件架构的地基再用第 23 课实现「画出来 动起来」第 4 课加入战斗与碰撞第 56 课补全计分、生命、胜负判定与重开机制最终形成完整游戏闭环。二、Canvas 与笛卡尔坐标系游戏世界的「画布」第 2 课 2-drawing-to-canvas/README.md 讲解了 HTML5 Canvas 的绘图基础。Canvas 是浏览器内实现动态图形与动画的核心 API通过 JavaScript 可以逐像素控制屏幕上的一切非常适合游戏、数据可视化和交互式艺术。2.1 画布的声明与坐标系一个画布元素通常这样声明在页面 body 中canvas idmyCanvas width200 height100/canvas关键在于理解 Canvas 使用的坐标系它基于数学课上学过的笛卡尔坐标系x 轴水平、y 轴垂直但原点(0,0)位于左上角x 值向右递增、y 值向下递增。这与数学坐标系的差异源自早期显示器电子束自上而下的扫描方式。在完整解决方案 6-space-game/solution/index.html 中游戏画布被设置为1024×768所有实体坐标都以该逻辑尺寸为基准。2.2 绘图的固定三步在 Canvas 上绘制任何内容都遵循三步流程获取画布元素引用document.getElementById(myCanvas)获取 2D 渲染上下文canvas.getContext(2d)它提供了全部绘图方法开始绘制使用上下文内置方法fillRect、arc、drawImage等// 第 1 步获取画布元素 const canvas document.getElementById(myCanvas); // 第 2 步获取 2D 渲染上下文 const ctx canvas.getContext(2d); // 第 3 步设置填充色并绘制一个矩形x, y, width, height ctx.fillStyle red; ctx.fillRect(0, 0, 200, 200);Canvas 支持绘制几何形状、任意字体与颜色的文本、以及基于.jpg/.png等图片资源的图像。若需要 3D 能力可进一步研究 WebGL API。在 解决方案的渲染函数 中可以看到更完整的实践clearRect清空画布、按星级分层绘制星空背景、用translate/rotate变换绘制旋转的敌舰与飞船、通过shadowBlur实现发光特效并且针对高 DPI 屏幕用setTransform(dpr, 0, 0, dpr, 0, 0)做了缩放适配见 resizeCanvas 函数。三、对象组织继承与组合第 1 课 1-introduction/README.md 指出任何游戏都包含「游戏对象」Game Objects——英雄、敌人、道具、特效等交互元素。它们虽然视觉各异却共享四类基本行为存在于某处每个对象都有 x、y 坐标游戏据此绘制可以移动英雄奔跑、敌人追击、子弹飞驰拥有生命周期有的常驻有的如爆炸特效短暂出现后消失对事件做出反应碰撞发生时收集道具、更新血条3.1 类与继承Class-Based Approach以基础类承载公共属性再派生子类添加特定能力// 第 1 步创建基础 GameObject 类 class GameObject { constructor(x, y, type) { this.x x; this.y y; this.type type; } } // 第 2 步通过继承添加移动能力 class Movable extends GameObject { constructor(x, y, type) { super(x, y, type); // 调用父类构造函数 } moveTo(x, y) { this.x x; this.y y; } } // 第 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() 会报错——树不能移动继承适合清晰的「is-a」关系Hero 是 Movable 的一种但层级僵化。第 3 课在 3-moving-elements-around/README.md 中给出了本项目的实际基类形态GameObject携带x/y、dead标记、type、width/height、img并提供draw(ctx)方法Hero与Enemy通过extends GameObject继承。3.2 组合与工厂函数Composition Approach组合遵循模块化设计哲学像工程师用可互换组件组装航天器一样把独立行为对象拼装出恰好满足需求的对象// 第 1 步创建基础行为对象 const gameObject { x: 0, y: 0, type: }; const movable { moveTo(x, y) { this.x x; this.y y; } }; // 第 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 }; } // 第 4 步使用组合对象 const hero createHero(10, 10); hero.moveTo(5, 5); // 正常工作 const tree createStatic(0, 0, Tree); // tree.moveTo() 为 undefined——没有组合移动行为组合适合「has-a」关系Hero 拥有移动能力更加灵活。实战中两种模式可以混用第 1 课的自测题给出示例——飞行敌人用继承写是class FlyingEnemy extends Movable用组合写则是{ ...movable, ...flyable, ...gameObject }。四、Pub/Sub 发布订阅架构解耦游戏组件通信第 1 课的核心架构知识点是发布-订阅模式Publish-Subscribe。当英雄受伤时血条要更新、音效要播放、画面要有反馈——若让英雄对象直接耦合这些系统代码将难以维护。Pub/Sub 让英雄只广播一条「受到伤害」的消息任何需要响应的系统订阅该消息即可原理类似无线电广播一个发射端可覆盖多个接收端无需知道谁在收听。4.1 关键角色消息Messages如PLAYER_SCORED这样的简单文本标签描述发生了什么发布者Publishers喊出「有事发生了」的对象订阅者Subscribers声明「我关心该事件」并在发生时响应的对象事件系统Event System确保消息送达正确监听者的「中间人」4.2 手写一个 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); }); } } }4.3 消息常量与键盘事件连接用常量对象防止消息名拼写错误并通过键盘事件驱动游戏逻辑// 第 1 步定义消息类型 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); // 第 2 步设置事件监听器订阅者 eventEmitter.on(Messages.HERO_MOVE_LEFT, () { hero.moveTo(hero.x - 5, hero.y); console.log(Hero moved to position: ${hero.x}, ${hero.y}); }); // 第 3 步将键盘输入连接到事件发布者 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; } });该模式的收益体现在新增音效、屏幕震动、粒子特效只需「多挂一个监听器」无需改动既有键盘或移动代码测试时每个组件可独立运行出问题时能准确定位。课程给出的最佳实践包括尽早以宽泛类别定义消息、使用描述性命名、保持 payload 简单聚焦、为团队文档化消息类型。五、Async/Await 异步加载游戏资源第 2 课指出加载图片资源是异步的在后台进行。需要创建Image对象、设置src触发请求待onload触发后再绘制到画布。课程的进阶方案是把图片加载包装成 Promise用 ES6 的 async/await 让代码更整洁并优雅处理错误function loadAsset(path) { return new Promise((resolve, reject) { const img new Image(); img.src path; img.onload () { resolve(img); }; img.onerror () { reject(new Error(Failed to load image: ${path})); }; }); } // 结合 async/await 的现代用法 async function initializeGame() { try { const heroImg await loadAsset(hero.png); const monsterImg await loadAsset(monster.png); // 图片现已就绪可以绘制 } catch (error) { console.error(Failed to load game assets:, error); } }在完整解决方案中同样采用此模式将飞船、敌舰等素材加载后才启动游戏循环见 6-space-game/solution/app.js 中bgImage的加载逻辑L96-L105并且支持玩家通过输入框提供自定义背景图 URL——设置img.crossOrigin anonymous以规避跨域污染问题。六、键盘事件与游戏循环让一切动起来第 3 课 3-moving-elements-around/README.md 专注于「动起来」。6.1 键盘事件处理键盘事件发生在 window 层级整个浏览器窗口监听按键鼠标点击则可以绑定到具体元素。使用window.addEventListener()传入事件名如keyup和回调函数window.addEventListener(keyup, (evt) { // evt.key 按键的字符串表示 if (evt.key ArrowUp) { // 执行操作 } });按键事件的两个关键属性key字符串如ArrowUp与keyCode数字如37对应左箭头。特别注意方向键会滚动页面、空格键会跳页必须用e.preventDefault()阻止浏览器默认行为否则会干扰游戏操控const onKeyDown function (e) { console.log(e.keyCode); switch (e.keyCode) { case 37: case 39: case 38: case 40: // 方向键 case 32: e.preventDefault(); break; // 空格 default: break; // 不拦截其他按键 } }; window.addEventListener(keydown, onKeyDown);在 解决方案的按键处理 中课程实现更进一步用keys集合记录持续按下的键、用keyMap同时支持 WASD 与方向键两套方案并通过preventDefault阻止空格滚动页面keyup时从集合移除实现连续平滑移动。6.2 游戏循环Game Loop游戏循环是「把帧连起来」的引擎更新所有对象位置 → 清屏 → 重绘 → 循环。课程用setInterval实现const gameLoopId setInterval(() { function gameLoop() { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle black; ctx.fillRect(0, 0, canvas.width, canvas.height); drawHero(); drawEnemies(); drawStaticObjects(); } gameLoop(); }, 200);帧率控制要点60 FPS 对应约 16.67ms/帧30 FPS 约 33.33ms/帧。而 解决方案的循环 采用更专业的requestAnimationFrame(loop)驱动用dt Math.min(0.033, (ts - state.lastTs) / 1000)计算每帧时间差并钳制在 33ms 以内避免切页/失焦导致的跳帧同时每 0.5 秒统计一次实时 FPS 显示在 HUD 上。6.3 敌人自动移动自动移动依赖 JavaScript 定时器每 100ms 让敌人沿 y 轴下移 10 像素const id setInterval(() { enemy.y 10; // 沿 y 轴移动敌人 }, 100);第 3 课还要求用GameObject基类重构x/y、dead、type、width/height、img、draw(ctx)Hero与Enemy继承之Enemy构造器内用setInterval自主向下移动到达画布底部时clearInterval停止并打印日志。七、碰撞检测与激光武器加入战斗系统第 4 课 4-collision-detection/README.md 引入战斗机制。核心思路是把每个游戏对象当作矩形处理——计算效率高适合绝大多数游戏场景。7.1 矩形边界与相交算法每个对象用位置加尺寸推导出四条边界rectFromGameObject() { return { top: this.y, left: this.x, bottom: this.y this.height, right: this.x this.width }; }相交检测采用分离轴否定测试——只要四个「分离条件」有一个成立r2 完全在 r1 右侧/左侧/下方/上方就没有碰撞function intersectRect(r1, r2) { return !(r2.left r1.right || r2.right r1.left || r2.top r1.bottom || r2.bottom r1.top); }解决方案中的碰撞检测 提供了真实世界的印证代码用圆形距离检测dist2(e.x, e.y, b.x, b.y) (e.r b.r)²判断子弹是否命中敌舰命中后加分、生成爆炸粒子、移除敌舰敌舰与玩家碰撞则扣减护盾值护盾耗尽再扣生命。7.2 对象生命周期标记删除而非立即删除在循环中途删除对象可能导致崩溃。正确做法是「标记为 dead帧间再过滤」enemy.dead true; // 标记删除当前帧安全结束 gameObjects gameObjects.filter(go !go.dead); // 渲染前过滤7.3 激光与开火冷却课程用Cooldown类 Weapon类实现 500ms 开火冷却防止无限连发class Cooldown { constructor(time) { this.cool false; setTimeout(() { this.cool true; }, time); } } class Weapon { constructor() { this.cooldown null; } fire() { if (!this.cooldown || this.cooldown.cool) { this.cooldown new Cooldown(500); // 创建激光 } else { // 武器仍在冷却中 } } }Laser类继承GameObject尺寸9×33用setInterval每 100ms 上移 15 像素触顶后自毁。课程要求实现四条碰撞规则激光击中敌人双方销毁、激光触顶移除、敌人与英雄碰撞双方销毁、敌人到达底部游戏结束。解决方案中对应的开火实现 用fireCooldown 0.15秒的帧间冷却并借助sfxShoot?.cloneNode(true)?.play()实现低延迟重叠音效。八、计分与生命游戏反馈系统第 5 课 5-keeping-score/README.md 聚焦让游戏「真正可玩」的反馈机制。8.1 画布文本渲染用fillText()在画布上绘制分数左上角与剩余生命右下角用飞船图标直观展示ctx.font 30px Arial; ctx.fillStyle red; ctx.textAlign right; ctx.fillText(show this on the screen, 0, 0);8.2 计分与生命的实现游戏规则每击毁一艘敌舰 100 分取整便于玩家心算英雄初始 3 条生命与敌人碰撞扣 1 条。核心方法挂在Hero上decrementLife() { this.life--; if (this.life 0) { this.dead true; // 生命耗尽标记死亡 } } incrementPoints() { this.points 100; }再通过事件系统串联COLLISION_ENEMY_LASER时hero.incrementPoints()COLLISION_ENEMY_HERO时hero.decrementLife()。绘制函数drawLife()按剩余生命循环绘制生命图标drawPoints()在左下角渲染文本分数二者在游戏循环中每次更新。解决方案的 HUD 实现 走得更远用语义化output元素与aria-livepolite实时展示分数、等级、生命、护盾、FPS 与时长updateHUD 函数 每帧同步这些 DOM 输出其中护盾以百分比显示、时间格式化为MM:SS。九、胜负判定与重新开始完整的游戏闭环第 6 课 6-end-condition/README.md 为游戏补上「终点」。常见的结束条件包括摧毁 N 艘敌舰、己方飞船被毁耗尽生命、收集 N 分、完成关卡目标。9.1 结束条件检测function isHeroDead() { return hero.life 0; } function isEnemiesDead() { const enemies gameObjects.filter((go) go.type Enemy !go.dead); return enemies.length 0; }9.2 事件化判定与提示消息在碰撞事件处理器中先判定失败再判定胜利避免「同时赢又输」eventEmitter.on(Messages.COLLISION_ENEMY_LASER, (_, { first, second }) { first.dead true; second.dead true; hero.incrementPoints(); if (isEnemiesDead()) { eventEmitter.emit(Messages.GAME_END_WIN); } }); eventEmitter.on(Messages.COLLISION_ENEMY_HERO, (_, { enemy }) { enemy.dead true; hero.decrementLife(); if (isHeroDead()) { eventEmitter.emit(Messages.GAME_END_LOSS); return; // 失败优先于胜利 } if (isEnemiesDead()) { eventEmitter.emit(Messages.GAME_END_WIN); } });新增消息常量GAME_END_LOSS/GAME_END_WIN并在keydown中监听 Enter 键发射KEY_EVENT_ENTER触发重开。胜利/失败用颜色编码提示绿色表示胜利、红色表示失败function displayMessage(message, color red) { ctx.font 30px Arial; ctx.fillStyle color; ctx.textAlign center; ctx.fillText(message, canvas.width / 2, canvas.height / 2); } function endGame(win) { clearInterval(gameLoopId); setTimeout(() { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle black; ctx.fillRect(0, 0, canvas.width, canvas.height); if (win) { displayMessage(Victory!!! Pew Pew... - Press [Enter] to start a new game Captain Pew Pew, green); } else { displayMessage(You died !!! Press [Enter] to start a new game Captain Pew Pew); } }, 200); }9.3 重置系统与内存清理resetGame()必须完整清理旧状态清除游戏循环、清空事件监听器为EventEmitter添加clear()方法重置this.listeners {}、调用initGame()重建对象、再启动新循环。否则会产生内存泄漏与重复事件处理器导致行为不可预测。解决方案中的重置实现 更为彻底整体重建 state 对象分数、等级、生命、护盾、计时、冷却全部归零清空子弹/敌人/粒子数组重置玩家坐标并同步刷新 HUD。十、运行与验证本地起服务看效果每个课程子目录都准备了your-work练习脚手架与solution参考答案两套工程包含assets/图片资源、index.html、app.js与package.json内置轻量开发服务器。启动方式统一为cd your-work npm start上述命令会在http://localhost:5000启动本地 HTTP 服务器并提供文件监听热更新。以第 2 课为例your-work结构为your-work/ ├── assets/ │ ├── enemyShip.png │ └── player.png ├── index.html ├── app.js └── package.json第 2 课的动手任务可对照 参考答案 app.js① 绘制1024×768黑色背景② 用loadAsset()加载玩家与敌舰贴图③ 把英雄画在底部居中canvas.width / 2 - 45, canvas.height - canvas.height / 4④ 用嵌套循环排布 5×5 敌舰方阵。方阵布局常量如下const ENEMY_TOTAL 5; const ENEMY_SPACING 98; const FORMATION_WIDTH ENEMY_TOTAL * ENEMY_SPACING; const START_X (canvas.width - FORMATION_WIDTH) / 2; const STOP_X START_X FORMATION_WIDTH; for (let x START_X; x STOP_X; x ENEMY_SPACING) { for (let y 0; y 50 * 5; y 50) { ctx.drawImage(enemyImg, x, y); } }完整版游戏位于 6-space-game/solution其 app.js 汇集了全部进阶特性可作为功能清单对照学习难度体系difficultyTableeasy/normal/hard/insane 四档分别调节敌舰生成速率1.6→0.7 秒、移动速度60→160与最大敌舰数12→28画质分级graphicsTablelow/medium/high/ultra 控制发光特效、模糊与星空图层数多套操控方案WASD 与方向键二选一空格开火、Shift 释放消耗 25 点护盾的「清屏炸弹」冷却 6 秒、P 暂停、F 全屏等级进度每存活 30 秒自动升级1 Math.floor(state.timeSec / 30)本地排行榜游戏结束时把分数写入localStorage键名spaceGameScores取前 10 名渲染到排行榜见 gameOver 与 renderScores无障碍与可访问性index.html 采用语义化 HTML、aria-label、aria-live、键盘焦点样式并支持dialog原生对话框做设置与暂停界面十一、素材来源与致谢本课程使用的全部游戏素材飞船、敌舰、激光、生命图标、爆炸特效等来自 Kenney.nl第 4 课的挑战题正是利用这些素材为「激光击中外星人」添加爆炸特效。结语这套太空游戏课程的价值在于它不是孤立地讲某个 API而是把**对象组织继承/组合、事件驱动Pub/Sub、异步加载Async/Await、输入处理键盘事件、实时渲染Canvas 游戏循环、物理判定碰撞检测、反馈设计计分/生命、状态管理结束/重开**这些软件工程通用概念编织进一个从零到完整可玩游戏的真实项目中。完成六课学习并对照 solution 源码你收获的不仅是「做出一个射击游戏」更是一套可迁移到任意 Web 应用的可扩展架构能力。【免费下载链接】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),仅供参考
返回列表