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

402 lines
16 KiB

<!--
CO_OP_TRANSLATOR_METADATA:
{
"original_hash": "23f088add24f0f1fa51014a9e27ea280",
"translation_date": "2025-08-27T22:32:16+00:00",
"source_file": "6-space-game/3-moving-elements-around/README.md",
"language_code": "vi"
}
-->
# Xây dựng trò chơi không gian Phần 3: Thêm chuyển động
## Câu hỏi trước bài giảng
[Câu hỏi trước bài giảng](https://ff-quizzes.netlify.app/web/quiz/33)
Trò chơi sẽ không thú vị nếu không có người ngoài hành tinh di chuyển trên màn hình! Trong trò chơi này, chúng ta sẽ sử dụng hai loại chuyển động:
- **Chuyển động bằng bàn phím/chuột**: khi người dùng tương tác với bàn phím hoặc chuột để di chuyển một đối tượng trên màn hình.
- **Chuyển động do trò chơi tạo ra**: khi trò chơi tự động di chuyển một đối tượng theo khoảng thời gian nhất định.
Vậy làm thế nào để di chuyển các đối tượng trên màn hình? Tất cả đều liên quan đến tọa độ Cartesian: chúng ta thay đổi vị trí (x, y) của đối tượng và sau đó vẽ lại màn hình.
Thông thường, bạn cần các bước sau để thực hiện *chuyển động* trên màn hình:
1. **Đặt vị trí mới** cho một đối tượng; điều này cần thiết để người dùng cảm nhận rằng đối tượng đã di chuyển.
2. **Xóa màn hình**, màn hình cần được xóa giữa các lần vẽ. Chúng ta có thể xóa bằng cách vẽ một hình chữ nhật được tô màu nền.
3. **Vẽ lại đối tượng** tại vị trí mới. Bằng cách này, chúng ta cuối cùng đạt được việc di chuyển đối tượng từ vị trí này sang vị trí khác.
Dưới đây là cách nó có thể được biểu diễn bằng mã:
```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);
```
✅ Bạn có thể nghĩ ra lý do tại sao việc vẽ lại nhân vật của bạn nhiều khung hình mỗi giây có thể gây ra chi phí hiệu suất không? Đọc thêm về [các giải pháp thay thế cho mô hình này](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Optimizing_canvas).
## Xử lý sự kiện bàn phím
Bạn xử lý sự kiện bằng cách gắn các sự kiện cụ thể vào mã. Các sự kiện bàn phím được kích hoạt trên toàn bộ cửa sổ, trong khi các sự kiện chuột như `click` có thể được kết nối với việc nhấp vào một phần tử cụ thể. Chúng ta sẽ sử dụng các sự kiện bàn phím trong suốt dự án này.
Để xử lý một sự kiện, bạn cần sử dụng phương thức `addEventListener()` của cửa sổ và cung cấp hai tham số đầu vào. Tham số đầu tiên là tên của sự kiện, ví dụ `keyup`. Tham số thứ hai là hàm sẽ được gọi khi sự kiện xảy ra.
Dưới đây là một ví dụ:
```javascript
window.addEventListener('keyup', (evt) => {
// `evt.key` = string representation of the key
if (evt.key === 'ArrowUp') {
// do something
}
})
```
Đối với các sự kiện bàn phím, có hai thuộc tính trên sự kiện mà bạn có thể sử dụng để xem phím nào đã được nhấn:
- `key`, đây là biểu diễn dạng chuỗi của phím đã nhấn, ví dụ `ArrowUp`.
- `keyCode`, đây là biểu diễn dạng số, ví dụ `37`, tương ứng với `ArrowLeft`.
✅ Việc thao tác với sự kiện phím rất hữu ích ngoài việc phát triển trò chơi. Bạn có thể nghĩ ra những ứng dụng nào khác cho kỹ thuật này?
### Các phím đặc biệt: một lưu ý
Có một số phím *đặc biệt* ảnh hưởng đến cửa sổ. Điều này có nghĩa là nếu bạn đang lắng nghe sự kiện `keyup` và bạn sử dụng các phím đặc biệt này để di chuyển nhân vật của mình, nó cũng sẽ thực hiện cuộn ngang. Vì lý do đó, bạn có thể muốn *tắt* hành vi mặc định của trình duyệt khi xây dựng trò chơi của mình. Bạn cần mã như sau:
```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);
```
Mã trên sẽ đảm bảo rằng các phím mũi tên và phím cách có hành vi *mặc định* bị tắt. Cơ chế *tắt* xảy ra khi chúng ta gọi `e.preventDefault()`.
## Chuyển động do trò chơi tạo ra
Chúng ta có thể làm cho các đối tượng tự di chuyển bằng cách sử dụng các bộ hẹn giờ như hàm `setTimeout()` hoặc `setInterval()` để cập nhật vị trí của đối tượng trên mỗi lần tick hoặc khoảng thời gian. Dưới đây là cách nó có thể được biểu diễn:
```javascript
let id = setInterval(() => {
//move the enemy on the y axis
enemy.y += 10;
})
```
## Vòng lặp trò chơi
Vòng lặp trò chơi là một khái niệm về cơ bản là một hàm được gọi theo khoảng thời gian đều đặn. Nó được gọi là vòng lặp trò chơi vì mọi thứ cần hiển thị cho người dùng đều được vẽ trong vòng lặp. Vòng lặp trò chơi sử dụng tất cả các đối tượng trò chơi là một phần của trò chơi, vẽ tất cả chúng trừ khi vì lý do nào đó không còn là một phần của trò chơi nữa. Ví dụ, nếu một đối tượng là kẻ thù bị bắn bởi tia laser và phát nổ, nó sẽ không còn là một phần của vòng lặp trò chơi hiện tại (bạn sẽ học thêm về điều này trong các bài học tiếp theo).
Dưới đây là cách một vòng lặp trò chơi thường được biểu diễn bằng mã:
```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);
```
Vòng lặp trên được gọi mỗi `200` mili giây để vẽ lại canvas. Bạn có thể chọn khoảng thời gian tốt nhất phù hợp với trò chơi của mình.
## Tiếp tục trò chơi không gian
Bạn sẽ lấy mã hiện có và mở rộng nó. Hoặc bắt đầu với mã mà bạn đã hoàn thành trong phần I hoặc sử dụng mã trong [Phần II - khởi đầu](../../../../6-space-game/3-moving-elements-around/your-work).
- **Di chuyển nhân vật chính**: bạn sẽ thêm mã để đảm bảo bạn có thể di chuyển nhân vật chính bằng các phím mũi tên.
- **Di chuyển kẻ thù**: bạn cũng cần thêm mã để đảm bảo kẻ thù di chuyển từ trên xuống dưới với tốc độ nhất định.
## Các bước đề xuất
Tìm các tệp đã được tạo cho bạn trong thư mục con `your-work`. Nó sẽ chứa các tệp sau:
```bash
-| assets
-| enemyShip.png
-| player.png
-| index.html
-| app.js
-| package.json
```
Bạn bắt đầu dự án của mình trong thư mục `your_work` bằng cách nhập:
```bash
cd your-work
npm start
```
Lệnh trên sẽ khởi động một HTTP Server tại địa chỉ `http://localhost:5000`. Mở trình duyệt và nhập địa chỉ đó, hiện tại nó sẽ hiển thị nhân vật chính và tất cả kẻ thù; chưa có gì di chuyển - vẫn còn!
### Thêm mã
1. **Thêm các đối tượng chuyên dụng** cho `hero`, `enemy``game object`, chúng nên có các thuộc tính `x``y`. (Nhớ phần về [Kế thừa hoặc thành phần](../README.md)).
*GỢI Ý* `game object` nên là đối tượng có `x``y` và khả năng tự vẽ lên canvas.
>gợi ý: bắt đầu bằng cách thêm một lớp GameObject mới với constructor được định nghĩa như dưới đây, sau đó vẽ nó lên canvas:
```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);
}
}
```
Bây giờ, mở rộng GameObject để tạo Hero và 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. **Thêm trình xử lý sự kiện phím** để x lý vic điu hướng phím (di chuyn nhân vt chính lên/xung trái/phi)
*NHỚ* đây là h ta độ Cartesian, góc trên bên trái là `0,0`. Cũng nh thêm mã để dng *hành vi mặc định*.
>gợi ý: tạo hàm onKeyDown của bạn và gắn nó vào cửa sổ:
```javascript
let onKeyDown = function (e) {
console.log(e.keyCode);
...add the code from the lesson above to stop default behavior
}
};
window.addEventListener("keydown", onKeyDown);
```
Kiểm tra bảng điều khiển trình duyệt của bạn tại thời điểm này và xem các phím được ghi lại.
3. **Triển khai** [Mô hình Pub sub](../README.md), điều này sẽ giữ cho mã của bạn sạch sẽ khi bạn tiếp tục các phần còn lại.
Để thực hiện phần cuối này, bạn có thể:
1. **Thêm một trình lắng nghe sự kiện** trên cửa sổ:
```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. **Tạo một lớp EventEmitter** để xuất bản và đăng ký các thông báo:
```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. **Thêm các hằng số** và thiết lập 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. **Khởi tạo trò chơi**
```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. **Thiết lập vòng lặp trò chơi**
Tái cấu trúc hàm window.onload để khởi tạo trò chơi và thiết lập vòng lặp trò chơi với khoảng thời gian tốt. Bạn cũng sẽ thêm tia laser:
```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. **Thêm mã** để di chuyển kẻ thù theo khoảng thời gian nhất định
Tái cấu trúc hàm `createEnemies()` để tạo kẻ thù và đẩy chúng vào lớp gameObjects mới:
```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);
}
}
}
```
và thêm hàm `createHero()` để thc hin quy trình tương t cho nhân vt chính.
```javascript
function createHero() {
hero = new Hero(
canvas.width / 2 - 45,
canvas.height - canvas.height / 4
);
hero.img = heroImg;
gameObjects.push(hero);
}
```
và cui cùng, thêm hàm `drawGameObjects()` để bt đầu v:
```javascript
function drawGameObjects(ctx) {
gameObjects.forEach(go => go.draw(ctx));
}
```
K thù ca bn s bt đầu tiến v phía tàu vũ tr ca nhân vt chính!
---
## 🚀 Thử thách
Như bn có th thy, mã ca bn có th tr thành 'mã spaghetti' khi bn bt đầu thêm các hàm, biến và lp. Làm thế nào bn có th t chc mã ca mình tt hơn để nó d đọc hơn? Phác tho mt h thng để t chc mã ca bn, ngay c khi nó vn nm trong mt tp.
## Câu hỏi sau bài giảng
[Câu hỏi sau bài giảng](https://ff-quizzes.netlify.app/web/quiz/34)
## Ôn tập & Tự học
Trong khi chúng ta đang viết trò chơi mà không s dng framework, có rt nhiu framework canvas da trên JavaScript dành cho phát trin trò chơi. Dành thi gian để [đọc về chúng](https://github.com/collections/javascript-game-engines).
## Bài tập
[Bình luận mã của bạn](assignment.md)
---
**Tuyên bố miễn trừ trách nhiệm**:
Tài liu này đã được dch bng dch v dch thut AI [Co-op Translator](https://github.com/Azure/co-op-translator). Mc dù chúng tôi c gng đảm bo độ chính xác, xin lưu ý rng các bn dch t động có th cha li hoc không chính xác. Tài liu gc bng ngôn ng bn địa nên được coi là ngun thông tin chính thc. Đối vi các thông tin quan trng, khuyến ngh s dng dch v dch thut chuyên nghip t con người. Chúng tôi không chu trách nhim cho bt k s hiu lm hoc din gii sai nào phát sinh t vic s dng bn dch này.