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/tw/6-space-game/3-moving-elements-around/README.md

402 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": "a9a161871de7706cb0e23b1bd0c74559",
"translation_date": "2025-08-29T15:21:04+00:00",
"source_file": "6-space-game/3-moving-elements-around/README.md",
"language_code": "tw"
}
-->
# 建立太空遊戲第三部分:加入移動功能
## 課前測驗
[課前測驗](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
```
您可以透過輸入以下指令來啟動專案中的 `your_work` 資料夾:
```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. **實現** [Pub-Sub 模式](../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 Canvas 框架可用於遊戲開發。花些時間閱讀[這些框架](https://github.com/collections/javascript-game-engines)。
## 作業
[為您的程式碼新增註解](assignment.md)
---
**免責聲明**
本文件使用 AI 翻譯服務 [Co-op Translator](https://github.com/Azure/co-op-translator) 進行翻譯。我們致力於提供準確的翻譯,但請注意,自動翻譯可能包含錯誤或不準確之處。應以原始語言的文件作為權威來源。對於關鍵資訊,建議尋求專業人工翻譯。我們對因使用此翻譯而產生的任何誤解或錯誤解讀概不負責。