13 KiB
构建太空游戏第三部分:添加运动
课前测验
游戏只有在屏幕上有外星人跑来跑去时才会变得有趣!在这个游戏中,我们将使用两种类型的运动:
- 键盘/鼠标运动:当用户通过键盘或鼠标与屏幕上的对象交互时。
- 游戏驱动运动:当游戏以一定的时间间隔移动对象时。
那么我们如何在屏幕上移动物体呢?这完全是关于笛卡尔坐标系的:我们改变对象的位置 (x, y),然后重新绘制屏幕。
通常,你需要以下步骤来实现屏幕上的运动:
- 设置新位置:为对象设置一个新位置,这是让用户感知对象移动的必要步骤。
- 清除屏幕:在每次绘制之间需要清除屏幕。我们可以通过绘制一个填充背景色的矩形来清除屏幕。
- 在新位置重新绘制对象:通过这样做,我们最终实现了将对象从一个位置移动到另一个位置。
以下是代码示例:
//set the hero's location
hero.x += 5;
// clear the rectangle that hosts the hero
ctx.clearRect(0, 0, canvas.width, canvas.height);
// redraw the game background and hero
ctx.fillRect(0, 0, canvas.width, canvas.height)
ctx.fillStyle = "black";
ctx.drawImage(heroImg, hero.x, hero.y);
✅ 你能想到为什么每秒多次重绘你的英雄可能会导致性能问题吗?阅读关于这种模式的替代方案。
处理键盘事件
通过将特定事件附加到代码上来处理事件。键盘事件会在整个窗口上触发,而鼠标事件(如click
)可以连接到点击特定元素。我们将在整个项目中使用键盘事件。
要处理事件,你需要使用窗口的addEventListener()
方法,并提供两个输入参数。第一个参数是事件的名称,例如keyup
。第二个参数是事件发生时应该调用的函数。
以下是一个示例:
window.addEventListener('keyup', (evt) => {
// `evt.key` = string representation of the key
if (evt.key === 'ArrowUp') {
// do something
}
})
对于键盘事件,事件上有两个属性可以用来查看按下了哪个键:
key
:这是按下的键的字符串表示,例如ArrowUp
。keyCode
:这是按下的键的数字表示,例如37
,对应于ArrowLeft
。
✅ 键盘事件的操作在游戏开发之外也很有用。你能想到这种技术的其他用途吗?
特殊键:一个注意事项
有一些特殊键会影响窗口。这意味着如果你正在监听keyup
事件,并使用这些特殊键移动你的英雄,它也会执行水平滚动。因此,在构建游戏时,你可能需要关闭这种内置的浏览器行为。你需要如下代码:
let onKeyDown = function (e) {
console.log(e.keyCode);
switch (e.keyCode) {
case 37:
case 39:
case 38:
case 40: // Arrow keys
case 32:
e.preventDefault();
break; // Space
default:
break; // do not block other keys
}
};
window.addEventListener('keydown', onKeyDown);
上述代码将确保箭头键和空格键的默认行为被关闭。关闭机制发生在调用e.preventDefault()
时。
游戏驱动运动
我们可以通过使用计时器(例如setTimeout()
或setInterval()
函数)让物体自行移动,这些计时器会在每个时间间隔更新对象的位置。以下是代码示例:
let id = setInterval(() => {
//move the enemy on the y axis
enemy.y += 10;
})
游戏循环
游戏循环是一个概念,实际上是一个以固定时间间隔调用的函数。它被称为游戏循环,因为所有应该显示给用户的内容都在循环中绘制。游戏循环利用游戏中的所有对象,绘制所有对象,除非某些对象不再属于游戏。例如,如果一个对象是被激光击中的敌人并爆炸,它就不再属于当前的游戏循环(你将在后续课程中了解更多)。
以下是一个典型的游戏循环代码示例:
let 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();
}, 200);
上述循环每200
毫秒调用一次以重绘画布。你可以选择适合你游戏的最佳时间间隔。
继续太空游戏
你将扩展现有代码。可以使用你在第一部分完成的代码,也可以使用第二部分的起始代码。
- 移动英雄:你将添加代码以确保可以使用箭头键移动英雄。
- 移动敌人:你还需要添加代码以确保敌人以一定速度从上到下移动。
推荐步骤
找到在your-work
子文件夹中为你创建的文件。它应该包含以下内容:
-| assets
-| enemyShip.png
-| player.png
-| index.html
-| app.js
-| package.json
通过输入以下命令启动你的项目:
cd your-work
npm start
上述命令将在地址http://localhost:5000
上启动一个HTTP服务器。打开浏览器并输入该地址,现在它应该渲染英雄和所有敌人;目前还没有任何移动!
添加代码
-
为
hero
、enemy
和game object
添加专用对象,它们应该具有x
和y
属性。(记住关于继承或组合的部分)。提示:
game object
应该是具有x
和y
属性并能够将自身绘制到画布上的对象。提示:从添加一个新的GameObject类开始,其构造函数如下所示,然后将其绘制到画布上:
class 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); } }
现在,扩展这个GameObject以创建Hero和Enemy。
class Hero extends GameObject { constructor(x, y) { ...it needs an x, y, type, and speed } }
class Enemy extends GameObject { constructor(x, y) { super(x, y); (this.width = 98), (this.height = 50); this.type = "Enemy"; let id = setInterval(() => { if (this.y < canvas.height - this.height) { this.y += 5; } else { console.log('Stopped at', this.y) clearInterval(id); } }, 300) } }
-
添加键盘事件处理程序以处理键盘导航(移动英雄上下左右)
记住这是一个笛卡尔系统,左上角是
0,0
。还记得要添加代码以停止默认行为。提示:创建你的onKeyDown函数并将其附加到窗口:
let onKeyDown = function (e) { console.log(e.keyCode); ...add the code from the lesson above to stop default behavior } }; window.addEventListener("keydown", onKeyDown);
此时检查你的浏览器控制台,观察按键被记录。
-
实现发布订阅模式,这将使你的代码在后续部分保持整洁。
要完成最后这部分,你可以:
-
在窗口上添加事件监听器:
window.addEventListener("keyup", (evt) => { if (evt.key === "ArrowUp") { eventEmitter.emit(Messages.KEY_EVENT_UP); } else if (evt.key === "ArrowDown") { eventEmitter.emit(Messages.KEY_EVENT_DOWN); } else if (evt.key === "ArrowLeft") { eventEmitter.emit(Messages.KEY_EVENT_LEFT); } else if (evt.key === "ArrowRight") { eventEmitter.emit(Messages.KEY_EVENT_RIGHT); } });
-
创建一个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((l) => l(message, payload)); } } }
-
添加常量并设置EventEmitter:
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", }; let heroImg, enemyImg, laserImg, canvas, ctx, gameObjects = [], hero, eventEmitter = new EventEmitter();
-
初始化游戏
function initGame() { gameObjects = []; createEnemies(); createHero(); eventEmitter.on(Messages.KEY_EVENT_UP, () => { hero.y -=5 ; }) eventEmitter.on(Messages.KEY_EVENT_DOWN, () => { hero.y += 5; }); eventEmitter.on(Messages.KEY_EVENT_LEFT, () => { hero.x -= 5; }); eventEmitter.on(Messages.KEY_EVENT_RIGHT, () => { hero.x += 5; }); }
-
-
设置游戏循环
重构window.onload函数以初始化游戏并以合适的时间间隔设置游戏循环。你还将添加一个激光束:
window.onload = async () => { canvas = document.getElementById("canvas"); ctx = canvas.getContext("2d"); heroImg = await loadTexture("assets/player.png"); enemyImg = await loadTexture("assets/enemyShip.png"); laserImg = await loadTexture("assets/laserRed.png"); initGame(); let gameLoopId = setInterval(() => { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = "black"; ctx.fillRect(0, 0, canvas.width, canvas.height); drawGameObjects(ctx); }, 100) };
-
添加代码以使敌人以一定间隔移动
重构
createEnemies()
函数以创建敌人并将它们推入新的gameObjects类:function createEnemies() { const MONSTER_TOTAL = 5; const MONSTER_WIDTH = MONSTER_TOTAL * 98; const START_X = (canvas.width - MONSTER_WIDTH) / 2; const STOP_X = START_X + MONSTER_WIDTH; for (let x = START_X; x < STOP_X; x += 98) { for (let y = 0; y < 50 * 5; y += 50) { const enemy = new Enemy(x, y); enemy.img = enemyImg; gameObjects.push(enemy); } } }
并添加一个
createHero()
函数以对英雄执行类似的过程。function createHero() { hero = new Hero( canvas.width / 2 - 45, canvas.height - canvas.height / 4 ); hero.img = heroImg; gameObjects.push(hero); }
最后,添加一个
drawGameObjects()
函数以开始绘制:function drawGameObjects(ctx) { gameObjects.forEach(go => go.draw(ctx)); }
你的敌人应该开始向你的英雄飞船推进!
🚀 挑战
如你所见,当你开始添加函数、变量和类时,你的代码可能会变成“意大利面条代码”。你如何更好地组织代码,使其更易读?画出一个系统来组织你的代码,即使它仍然在一个文件中。
课后测验
复习与自学
虽然我们在没有使用框架的情况下编写游戏,但有许多基于JavaScript的画布框架可用于游戏开发。花些时间阅读相关内容。
作业
免责声明:
本文档使用AI翻译服务Co-op Translator进行翻译。尽管我们努力确保准确性,但请注意,自动翻译可能包含错误或不准确之处。应以原始语言的文档作为权威来源。对于关键信息,建议使用专业人工翻译。对于因使用本翻译而引起的任何误解或误读,我们概不负责。