Web-Dev-For-Beginners 空间游戏实战:用 JavaScript 与 Canvas 从零构建一款可玩射击游戏 Web-Dev-For-Beginners 空间游戏实战用 JavaScript 与 Canvas 从零构建一款可玩射击游戏【免费下载链接】Web-Dev-For-Beginners24 Lessons, 12 Weeks, Get Started as a Web Developer项目地址: https://gitcode.com/GitHub_Trending/we/Web-Dev-For-BeginnersWeb-Dev-For-Beginners 开源教程共 24 课其中第 6 部分“Build a Space Game”用 6 个课时完整带你实现一款类似 Space Invaders 的空间射击游戏——玩家操控飞船向从上方下落的怪物开火。读完本指南你将掌握 Canvas 绘图、笛卡尔坐标系、发布-订阅Pub/Sub模式、Async/Await 异步资源加载与键盘事件处理最终在浏览器中运行一款含碰撞检测、计分、生命值与重开机制的完整游戏。本部分课程入口文档位于 translations/el/6-space-game/README.md希腊语翻译版与英文原版 6-space-game/README.md 内容一致完整的可运行最终代码位于 6-space-game/solution/app.js。课程概览六课构建一个完整游戏整个空间游戏系列被拆解为“理论 实践”两条主线共 6 个课时课时主题核心知识点文档路径1游戏开发导论继承 vs 组合、对象行为抽象1-introduction2在 Canvas 上绘图Canvas API、坐标系、异步加载贴图2-drawing-to-canvas3让元素动起来游戏循环、键盘事件、Pub/Sub 集成3-moving-elements-around4碰撞检测矩形相交算法、激光武器、冷却系统4-collision-detection5计分与生命Canvas 文本渲染、奖励与惩罚系统5-keeping-score6结束与重开胜负条件、状态管理、内存清理6-end-condition每个课时都配套有assignment.md课后作业、solution/参考答案目录与your-work/起始代码目录。例如第 2 课的起始工程位于 6-space-game/2-drawing-to-canvas/your-work参考答案在 solution/app.js。游戏美术资源来自 kenney.nl多为免费素材部分收费适合游戏开发入门者复用。第 1 课游戏架构的两大基石——继承与组合任何游戏都由大量游戏对象game objects组成英雄、敌人、道具、特效。尽管外观各异它们共享四类基本行为存在于某处拥有 x/y 坐标、可以移动、有生命周期如爆炸特效短暂出现即消失、对交互做出反应碰撞、拾取道具。继承基于类的层级结构以基础GameObject类存放公共属性再通过extends派生特化类// 基础类每个游戏对象都有位置与类型 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); // 调用父类构造函数 } moveTo(x, y) { this.x x; this.y y; } } // 特化对象类型 class Hero extends Movable { constructor(x, y) { super(x, y, Hero); } }继承适用于清晰的 is-a 关系Hero是一个 Movable 对象。文档中的类图1-introduction/README.md展示了MovableObject、TemporaryObject、InteractiveObject从GameObject派生的完整层级。组合基于工厂函数的行为混入组合遵循模块化设计用展开语法把独立行为对象拼装成目标对象适用于 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 { ...gameObject, ...movable, x, y, type: Hero }; } const hero createHero(10, 10); hero.moveTo(5, 5); // 正常可用选型建议有清晰的层级关系时用继承需要最大灵活性时用组合两者可在同一应用中混用。这在后续课中直接落地——第 3 课的GameObject基类含dead标志与draw(ctx)方法正是继承方案的具体实现。发布-订阅Pub/Sub模式构建低耦合事件系统当英雄受伤时血条、音效、视觉反馈都要响应。与其让英雄对象直接依赖这些系统Pub/Sub 允许它广播一条 HERO_DAMAGED 消息任何关心的系统自行订阅。这一模式保持组件间松散耦合使代码更易维护、测试与扩展。核心角色消息MessagesPLAYER_SCORED这类描述事件的文本标签发布者Publishers发出发生了什么的对象订阅者Subscribers声明我关心该事件并在事件发生时响应的对象事件系统Event System确保消息到达正确监听者的中间人。实现一个 EventEmitterclass 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); }); } } }实战键盘输入驱动英雄移动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); // 订阅者移动逻辑与输入处理分离 eventEmitter.on(Messages.HERO_MOVE_LEFT, () { hero.moveTo(hero.x - 5, hero.y); }); // 发布者键盘事件仅负责发消息 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; } });这种架构的收益新增音效、屏幕震动等特性只需追加监听器多个系统可响应同一事件每个部件可独立测试。实践建议避免过早创建过多细粒度消息类型从大类目开始逐步细化。第 2 课Canvas 绘图与异步资源加载canvas是 HTML5 提供的可编程绘图表面JavaScript 通过 2D 渲染上下文获得逐像素控制能力canvas idmyCanvas width200 height100/canvas笛卡尔坐标系的关键差异Canvas 同样使用 x/y 笛卡尔坐标但原点 (0,0) 位于左上角x 向右增大y 向下增大。这与数学课堂中原点在左下、y 向上的习惯相反是所有绘图定位的基础。参考图示见 canvas_grid.png。绘图的固定三步流程// 1. 获取 canvas 元素引用 const canvas document.getElementById(myCanvas); // 2. 获取 2D 渲染上下文 const ctx canvas.getContext(2d); // 3. 开始绘制设置填充色并画矩形 ctx.fillStyle red; ctx.fillRect(0, 0, 200, 200); // x, y, width, heightCanvas API 支持绘制几何形状、文本任意字体颜色、以及基于 .jpg/.png 等资源的图片。用 Promise Async/Await 健壮地加载贴图图片加载是异步的直接用img.onload容易陷入回调地狱。专业做法是把加载包装成 Promisefunction 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 function renderGameScreen() { try { const heroImg await loadAsset(hero.png); const monsterImg await loadAsset(monster.png); const canvas document.getElementById(myCanvas); const ctx canvas.getContext(2d); // 英雄居中偏下、怪物放左上角作为阵型起点 ctx.drawImage(heroImg, canvas.width / 2, canvas.height / 2); ctx.drawImage(monsterImg, 0, 0); } catch (error) { console.error(Failed to render game screen:, error); } }实战任务搭建游戏场景目标渲染一块1024*768黑色画布居中偏下放置英雄飞船player.png并绘制 5×5 怪物阵型enemyShip.png。工程结构如下your-work/ ├── assets/ │ ├── enemyShip.png │ └── player.png ├── index.html ├── app.js └── package.json在your-work目录启动本地开发服务器cd your-work npm start服务运行在http://localhost:5000支持文件监听与自动刷新。阵型布局用常量计算而非硬编码使队形始终居中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; // 嵌套循环生成 5 行 × 5 列的怪物阵型 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); } }第 3 课让游戏动起来——游戏循环与键盘控制动画的本质是翻页式逐帧绘制每帧经历三步更新位置 → 清空旧帧 → 绘制新帧hero.x 5; // 1. 更新位置 ctx.clearRect(0, 0, canvas.width, canvas.height); // 2. 清除旧帧 ctx.fillStyle black; ctx.fillRect(0, 0, canvas.width, canvas.height); // 3. 重绘背景 ctx.drawImage(heroImg, hero.x, hero.y); // 重绘英雄处理键盘事件键盘事件挂在 window 层级使用addEventListener()注册事件对象上可通过key字符串如ArrowUp或keyCode数字如37对应ArrowLeft判断按键。务必阻止默认行为方向键默认会滚动页面、空格键会跳转这会干扰游戏。用e.preventDefault()拦截const onKeyDown function (e) { switch (e.keyCode) { case 37: case 39: case 38: case 40: // 方向键 case 32: // 空格 e.preventDefault(); break; default: break; // 其他按键不拦截 } }; window.addEventListener(keydown, onKeyDown);游戏循环与对象系统游戏循环用setInterval周期性执行清屏 → 填背景 → 绘制所有对象const gameLoopId setInterval(() { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle black; ctx.fillRect(0, 0, canvas.width, canvas.height); drawHero(); drawEnemies(); drawStaticObjects(); }, 100);为支撑移动引入GameObject基类与Hero/Enemy子类见 3-moving-elements-around/README.mdclass GameObject { constructor(x, y) { this.x x; this.y y; this.dead false; // 标记是否应被移除 this.type ; this.width 0; this.height 0; this.img undefined; } draw(ctx) { ctx.drawImage(this.img, this.x, this.y, this.width, this.height); } } class Enemy extends GameObject { constructor(x, y) { super(x, y); this.width 98; this.height 50; this.type Enemy; const id setInterval(() { if (this.y canvas.height - this.height) { this.y 5; // 敌人自动向下推进 } else { clearInterval(id); } }, 300); } }再把第 1 课学到的 Pub/Sub 接入输入系统键盘监听器只负责emit(Messages.KEY_EVENT_UP/DOWN/LEFT/RIGHT)initGame()中通过eventEmitter.on(...)订阅并更新hero.y/hero.x每次 ±5 像素。完整接线方式见 3-moving-elements-around/README.md。第 4 课激光武器与碰撞检测碰撞检测采用矩形包围盒方案把每个游戏对象视为矩形检查矩形是否相交。该方法计算高效适合绝大多数 2D 游戏场景。矩形表示与相交算法rectFromGameObject() { return { top: this.y, left: this.x, bottom: this.y this.height, right: this.x this.width }; } // 分离轴测试只要任一分离条件成立则不相交 function intersectRect(r1, r2) { return !(r2.left r1.right || r2.right r1.left || r2.top r1.bottom || r2.bottom r1.top); }安全的对象生命周期管理删除正在被循环遍历的对象会导致崩溃。正确做法是标记删除先设enemy.dead true让当前帧安全结束再在下一渲染周期前过滤enemy.dead true; // ... gameObjects gameObjects.filter(go !go.dead);激光发射与冷却系统为防连射滥用加入 500ms 冷却机制4-collision-detection/README.mdclass Laser extends GameObject { constructor(x, y) { super(x, y); this.width 9; this.height 33; this.type Laser; this.img laserImg; let id setInterval(() { if (this.y 0) { this.y - 15; } // 激光向上飞行 else { this.dead true; clearInterval(id); } // 出屏自毁 }, 100); } } // Hero 中的发射与冷却 fire() { gameObjects.push(new Laser(this.x 45, this.y - 10)); this.cooldown 500; let id setInterval(() { if (this.cooldown 0) { this.cooldown - 100; } else { clearInterval(id); } }, 200); } canFire() { return this.cooldown 0; }碰撞规则与事件联动本课需要实现的四条碰撞规则4-collision-detection/README.md激光击中敌人 → 敌人被摧毁激光到达屏幕顶部边界 → 激光移除敌人与英雄相撞 → 双方销毁敌人到达屏幕底部 → 游戏结束。碰撞检测函数按类型过滤对象、两两测试并发出事件6-space-game/4-collision-detection/README.mdfunction updateGameObjects() { const enemies gameObjects.filter(go go.type Enemy); const lasers gameObjects.filter(go go.type Laser); lasers.forEach((laser) { enemies.forEach((enemy) { if (intersectRect(laser.rectFromGameObject(), enemy.rectFromGameObject())) { eventEmitter.emit(Messages.COLLISION_ENEMY_LASER, { first: laser, second: enemy, }); } }); }); gameObjects gameObjects.filter(go !go.dead); }对应的订阅逻辑eventEmitter.on(Messages.COLLISION_ENEMY_LASER, (_, { first, second }) { first.dead true; second.dead true; });记住把updateGameObjects()加入主游戏循环碰撞检测才会生效。第 5 课计分与生命值——游戏反馈系统Canvas 文本渲染用fillText()在画布上直接绘制文本可完全控制字体、颜色与对齐ctx.font 30px Arial; ctx.fillStyle red; ctx.textAlign right; ctx.fillText(show this on the screen, 0, 0);计分与生命的完整实现设计要点每摧毁一艘敌舰 100 分整数更利于玩家心算形成心理奖励英雄初始 3 条命沿用街机时代平衡挑战与可玩性的标准分数显示在左下角生命值用飞船图标显示在右下角life.png。// Hero 类中初始化 this.life 3; this.points 0; // 绘制生命图标与分数 function drawLife() { const START_POS canvas.width - 180; for (let i 0; i hero.life; i) { ctx.drawImage(lifeImg, START_POS (45 * (i 1)), canvas.height - 37); } } function drawPoints() { ctx.font 30px Arial; ctx.fillStyle red; ctx.textAlign left; drawText(Points: hero.points, 10, canvas.height - 20); } function drawText(message, x, y) { ctx.fillText(message, x, y); }在游戏循环中updateGameObjects()之后调用drawPoints()与drawLife()。然后把奖惩逻辑挂到碰撞事件上5-keeping-score/README.mdeventEmitter.on(Messages.COLLISION_ENEMY_LASER, (_, { first, second }) { first.dead true; second.dead true; hero.incrementPoints(); // 命中 100 }); eventEmitter.on(Messages.COLLISION_ENEMY_HERO, (_, { enemy }) { enemy.dead true; hero.decrementLife(); // 相撞 -1 命 });其中decrementLife()在生命归零时把hero.dead置为trueincrementPoints()每次累加 100 分。第 6 课结束条件、重开机制与状态清理定义胜负条件游戏结束的常见判定方式消灭 N 艘敌舰过关、飞船被摧毁且生命耗尽失败、收集 N 分、完成特定关卡目标。本课采用两个监控函数function isHeroDead() { return hero.life 0; } function isEnemiesDead() { const enemies gameObjects.filter((go) go.type Enemy !go.dead); return enemies.length 0; }事件驱动的胜负流转在碰撞事件中追加胜负检查注意先判失败再判胜利避免边赢边输的矛盾状态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); } });消息展示与重开displayMessage()用颜色区分结果——绿色代表胜利、红色代表失败默认参数让颜色可配置endGame()先clearInterval(gameLoopId)冻结游戏延迟 200ms 等最后一帧渲染完成后清屏并显示提示6-end-condition/README.mdfunction 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, green); } else { displayMessage(You died !!! Press [Enter] to start a new game); } }, 200); }resetGame干净地重置全部状态重开的关键是完整清理防止内存泄漏与重复事件处理。先给 EventEmitter 增加clear()方法再实现clear() { this.listeners {}; } function resetGame() { if (gameLoopId) { clearInterval(gameLoopId); // 停止当前游戏循环 eventEmitter.clear(); // 清空所有事件监听 initGame(); // 重新初始化游戏对象 gameLoopId setInterval(() { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle black; ctx.fillRect(0, 0, canvas.width, canvas.height); drawPoints(); drawLife(); updateGameObjects(); drawGameObjects(ctx); }, 100); } }并在initGame()中注册 Enter 键触发重开eventEmitter.on(Messages.KEY_EVENT_ENTER, () { resetGame(); });键盘监听中加入else if (evt.key Enter) eventEmitter.emit(Messages.KEY_EVENT_ENTER);。至此玩家可在胜利/失败后按 Enter 秒速开启新一局无需刷新页面。仓库中的完整参考实现系列课程最终汇总的完整可玩版本位于 6-space-game/solution/配套 index.html 与 app.js。该实现进一步演进出若干工程化细节可作为学习样板IIFE 封装整个游戏逻辑包裹在立即执行函数中避免污染全局命名空间requestAnimationFrame 游戏循环替代setInterval配合dt增量时间上限钳制 33ms实现与刷新率同步的平滑动画app.js难度与画质配置表difficultyTableeasy/normal/hard/insane 对应敌人生成速率、速度、上限与graphicsTablelow/medium/high/ultra 控制辉光、模糊与星空层数见 app.js高清屏适配按devicePixelRatio缩放 canvas 并setTransform保证高分屏清晰app.js粒子爆炸特效与音效explode()生成粒子cloneNode(true).play()实现低延迟重叠音效app.jslocalStorage 最高分持久化gameOver()将分数写入spaceGameScores并保留前 10 名app.js。各课时your-work/目录则保留每课对应的起始工程例如 2-drawing-to-canvas/your-work 到 6-end-condition/your-work便于按课逐步对照练习。总结通过这 6 个课时你完整经历了从空 HTML 页面到可玩、可重开、有反馈的浏览器射击游戏的全过程用继承与组合组织游戏对象用 Pub/Sub 解耦输入、逻辑与渲染用 Canvas 与异步资源加载搭建画面用矩形相交算法实现碰撞用分数与生命构建反馈系统再用结束条件与状态重置完成游戏闭环。这些能力不仅适用于游戏开发同样直接迁移到数据可视化、交互式 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),仅供参考