You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Web-Dev-For-Beginners/translations/zh/6-space-game/3-moving-elements-around/README.md

400 lines
13 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!--
CO_OP_TRANSLATOR_METADATA:
{
"original_hash": "23f088add24f0f1fa51014a9e27ea280",
"translation_date": "2025-08-23T22:54:59+00:00",
"source_file": "6-space-game/3-moving-elements-around/README.md",
"language_code": "zh"
}
-->
# 构建太空游戏第三部分:添加运动
## 课前测验
[课前测验](https://ff-quizzes.netlify.app/web/quiz/33)
游戏只有在屏幕上有外星人跑来跑去时才会变得有趣!在这个游戏中,我们将使用两种类型的运动:
- **键盘/鼠标运动**:用户通过键盘或鼠标与屏幕上的对象交互并移动它。
- **游戏驱动运动**:游戏以一定的时间间隔自动移动对象。
那么我们如何在屏幕上移动物体呢?这完全是关于笛卡尔坐标系:我们改变对象的位置 (x, y),然后重新绘制屏幕。
通常需要以下步骤来实现屏幕上的*运动*
1. **设置新位置**:为对象设置一个新位置,这样才能让用户感知到对象已经移动。
2. **清除屏幕**:在每次绘制之间需要清除屏幕。我们可以通过绘制一个填充背景色的矩形来清除屏幕。
3. **在新位置重新绘制对象**:通过这样做,我们最终实现了将对象从一个位置移动到另一个位置。
以下是代码示例:
```javascript
//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);
```
✅ 你能想到为什么每秒重绘你的英雄多次可能会导致性能问题吗?阅读关于[这种模式的替代方案](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Optimizing_canvas)。
## 处理键盘事件
通过将特定事件附加到代码中来处理事件。键盘事件会在整个窗口中触发,而鼠标事件(如`click`)可以连接到点击特定元素。我们将在整个项目中使用键盘事件。
要处理事件,你需要使用窗口的`addEventListener()`方法,并提供两个输入参数。第一个参数是事件的名称,例如`keyup`。第二个参数是事件发生时应该调用的函数。
以下是一个示例:
```javascript
window.addEventListener('keyup', (evt) => {
// `evt.key` = string representation of the key
if (evt.key === 'ArrowUp') {
// do something
}
})
```
对于键盘事件,事件上有两个属性可以用来查看按下了哪个键:
- `key`:这是按下的键的字符串表示,例如`ArrowUp`。
- `keyCode`:这是数字表示,例如`37`,对应于`ArrowLeft`。
✅ 键盘事件的操作在游戏开发之外也很有用。你能想到这种技术的其他用途吗?
### 特殊键:一个注意事项
有一些*特殊*键会影响窗口。这意味着如果你正在监听`keyup`事件,并使用这些特殊键移动你的英雄,它也会执行水平滚动。因此,在构建游戏时,你可能需要*关闭*这种内置的浏览器行为。你需要如下代码:
```javascript
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()`函数)让物体自动移动,这些计时器会在每个时间间隔更新对象的位置。以下是代码示例:
```javascript
let id = setInterval(() => {
//move the enemy on the y axis
enemy.y += 10;
})
```
## 游戏循环
游戏循环是一个概念,实际上是一个以固定间隔调用的函数。它被称为游戏循环,因为所有应该显示给用户的内容都在循环中绘制。游戏循环利用游戏中的所有对象,绘制所有对象,除非某些对象不再属于游戏。例如,如果一个对象是被激光击中的敌人并爆炸,它就不再属于当前的游戏循环(你将在后续课程中了解更多)。
以下是游戏循环的典型代码示例:
```javascript
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`毫秒调用一次以重绘画布。你可以选择适合你游戏的最佳时间间隔。
## 继续太空游戏
你将使用现有代码并进行扩展。可以从你在第一部分完成的代码开始,或者使用[第二部分的起始代码](../../../../6-space-game/3-moving-elements-around/your-work)。
- **移动英雄**:你将添加代码以确保可以使用箭头键移动英雄。
- **移动敌人**:你还需要添加代码以确保敌人以一定速度从上到下移动。
## 推荐步骤
找到在`your-work`子文件夹中为你创建的文件。它应该包含以下内容:
```bash
-| assets
-| enemyShip.png
-| player.png
-| index.html
-| app.js
-| package.json
```
通过输入以下命令启动你的项目:
```bash
cd your-work
npm start
```
上述命令将在地址`http://localhost:5000`上启动一个HTTP服务器。打开浏览器并输入该地址现在它应该渲染英雄和所有敌人目前还没有任何移动
### 添加代码
1. **为`hero`、`enemy`和`game object`添加专用对象**,它们应该具有`x`和`y`属性。(记住关于[继承或组合](../README.md)的部分)。
*提示*`game object`应该是具有`x`和`y`属性并能够将自身绘制到画布上的对象。
>提示从添加一个新的GameObject类开始其构造函数如下所示然后将其绘制到画布上
```javascript
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。
```javascript
class Hero extends GameObject {
constructor(x, y) {
...it needs an x, y, type, and speed
}
}
```
```javascript
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)
}
}
```
2. **添加键盘事件处理程序**以处理键盘导航(移动英雄上下左右)。
*记住*:这是一个笛卡尔系统,左上角是`0,0`。还要记得添加代码以停止*默认行为*。
>提示创建你的onKeyDown函数并将其附加到窗口
```javascript
let onKeyDown = function (e) {
console.log(e.keyCode);
...add the code from the lesson above to stop default behavior
}
};
window.addEventListener("keydown", onKeyDown);
```
此时检查你的浏览器控制台,观察按键被记录。
3. **实现**[发布订阅模式](../README.md),这将使你的代码在后续部分保持整洁。
要完成最后这部分,你可以:
1. **在窗口上添加事件监听器**
```javascript
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);
}
});
```
1. **创建一个EventEmitter类**以发布和订阅消息:
```javascript
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));
}
}
}
```
1. **添加常量**并设置EventEmitter
```javascript
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();
```
1. **初始化游戏**
```javascript
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;
});
}
```
1. **设置游戏循环**
重构window.onload函数以初始化游戏并以合适的间隔设置游戏循环。你还将添加一个激光束
```javascript
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)
};
```
5. **添加代码**以使敌人以一定间隔移动
重构`createEnemies()`函数以创建敌人并将它们推入新的gameObjects类
```javascript
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()`函数以对英雄执行类似的过程。
```javascript
function createHero() {
hero = new Hero(
canvas.width / 2 - 45,
canvas.height - canvas.height / 4
);
hero.img = heroImg;
gameObjects.push(hero);
}
```
最后,添加一个`drawGameObjects()`函数以开始绘制:
```javascript
function drawGameObjects(ctx) {
gameObjects.forEach(go => go.draw(ctx));
}
```
你的敌人应该开始向你的英雄飞船推进!
---
## 🚀 挑战
如你所见,当你开始添加函数、变量和类时,你的代码可能会变成“意大利面条代码”。你如何更好地组织代码,使其更易读?即使代码仍然在一个文件中,也可以画出一个系统来组织代码。
## 课后测验
[课后测验](https://ff-quizzes.netlify.app/web/quiz/34)
## 复习与自学
虽然我们在没有使用框架的情况下编写游戏,但有许多基于JavaScript的画布框架可用于游戏开发。花些时间阅读[相关内容](https://github.com/collections/javascript-game-engines)。
## 作业
[为你的代码添加注释](assignment.md)
**免责声明**
本文档使用AI翻译服务[Co-op Translator](https://github.com/Azure/co-op-translator)进行翻译。尽管我们努力确保翻译的准确性,但请注意,自动翻译可能包含错误或不准确之处。原始语言的文档应被视为权威来源。对于重要信息,建议使用专业人工翻译。我们不对因使用此翻译而产生的任何误解或误读承担责任。