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
Lee Stott 2daab5271b
Update Quiz Link
3 weeks ago
..
README.md Update Quiz Link 3 weeks ago
assignment.md 🌐 Update translations via Co-op Translator 3 weeks ago

README.md

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

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ã:

//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.

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ụ:

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:

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:

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ã:

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.

  • 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:

-| 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:

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, enemygame object, chúng nên có các thuộc tính xy. (Nhớ phần về Kế thừa hoặc thành phần).

    GỢI Ý game object nên là đối tượng có xy 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:

    
    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.

    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)
      }
    }
    
  2. Thêm trình xử lý sự kiện phím để xử lý việc điều hướng phím (di chuyển nhân vật chính lên/xuống trái/phải)

    NHỚ đây là hệ tọa độ Cartesian, góc trên bên trái là 0,0. Cũng nhớ thêm mã để dừng 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ổ:

     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, đ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ổ:

       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);
         }
       });
      
    2. Tạo một lớp EventEmitter để xuất bản và đăng ký các thông báo:

      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));
          }
        }
      }
      
    3. Thêm các hằng số và thiết lập 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();
      
    4. Khởi tạo trò chơi

    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;
      });
    }
    
  4. 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:

    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:

    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() để thực hiện quy trình tương tự cho nhân vật chính.

    function createHero() {
      hero = new Hero(
        canvas.width / 2 - 45,
        canvas.height - canvas.height / 4
      );
      hero.img = heroImg;
      gameObjects.push(hero);
    }
    

    và cuối cùng, thêm hàm drawGameObjects() để bắt đầu vẽ:

    function drawGameObjects(ctx) {
      gameObjects.forEach(go => go.draw(ctx));
    }
    

    Kẻ thù của bạn sẽ bắt đầu tiến về phía tàu vũ trụ của nhân vật chính!


🚀 Thử thách

Như bạn có thể thấy, mã của bạn có thể trở thành 'mã spaghetti' khi bạn bắt đầu thêm các hàm, biến và lớp. Làm thế nào bạn có thể tổ chức mã của mình tốt hơn để nó dễ đọc hơn? Phác thảo một hệ thống để tổ chức mã của bạn, ngay cả khi nó vẫn nằm trong một tệp.

Câu hỏi sau bài giảng

Câu hỏi sau bài giảng

Ôn tập & Tự học

Trong khi chúng ta đang viết trò chơi mà không sử dụng framework, có rất nhiều framework canvas dựa trên JavaScript dành cho phát triển trò chơi. Dành thời gian để đọc về chúng.

Bài tập

Bình luận mã của bạn


Tuyên bố miễn trừ trách nhiệm:
Tài liệu này đã được dịch bằng dịch vụ dịch thuật AI Co-op Translator. Mặc dù chúng tôi cố gắng đảm bảo độ chính xác, xin lưu ý rằng các bản dịch tự động có thể chứa lỗi hoặc không chính xác. Tài liệu gốc bằng ngôn ngữ bản địa nên được coi là nguồn thông tin chính thức. Đối với các thông tin quan trọng, khuyến nghị sử dụng dịch vụ dịch thuật chuyên nghiệp từ con người. Chúng tôi không chịu trách nhiệm cho bất kỳ sự hiểu lầm hoặc diễn giải sai nào phát sinh từ việc sử dụng bản dịch này.