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/mo/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 4 weeks ago

README.md

建立太空遊戲第三部分:加入移動功能

課前測驗

課前測驗

遊戲中如果沒有外星人在螢幕上移動,那就少了很多樂趣!在這個遊戲中,我們將使用兩種類型的移動方式:

  • 鍵盤/滑鼠移動:當使用者透過鍵盤或滑鼠與遊戲互動時,物件會在螢幕上移動。
  • 遊戲驅動移動:當遊戲以一定的時間間隔自動移動物件時。

那麼,我們該如何讓物件在螢幕上移動呢?這一切都與笛卡爾座標有關:我們改變物件的位置 (x, y),然後重新繪製螢幕。

通常,您需要以下步驟來實現螢幕上的移動

  1. 設定物件的新位置:這是讓物件看起來像是移動的必要步驟。
  2. 清除螢幕:在每次繪製之間需要清除螢幕。我們可以透過繪製一個填滿背景顏色的矩形來清除螢幕。
  3. 在新位置重新繪製物件:這樣我們就能實現物件從一個位置移動到另一個位置。

以下是程式碼範例:

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

您可以透過輸入以下指令啟動專案中的 your_work 資料夾:

cd your-work
npm start

上述指令將在位址 http://localhost:5000 啟動一個 HTTP 伺服器。打開瀏覽器並輸入該位址,目前應該會顯示英雄和所有敵人;但它們尚未移動!

新增程式碼

  1. 新增專用物件:為 heroenemygame object 新增專用物件,它們應該具有 xy 屬性。(記得參考 繼承或組合 的部分)。

    提示game object 應該是具有 xy 屬性並能夠將自己繪製到畫布上的物件。

    提示:從新增一個 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)
      }
    }
    
  2. 新增鍵盤事件處理器:處理鍵盤導航(移動英雄上下左右)。

    記住:這是一個笛卡爾座標系統,左上角是 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);
    

    此時檢查您的瀏覽器主控台,觀察按鍵記錄。

  3. 實現 Pub sub 模式,這將使您的程式碼在後續部分保持整潔。

    要完成這部分,您可以:

    1. 在視窗上新增事件監聽器

       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. 建立一個 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));
          }
        }
      }
      
    3. 新增常數並設置 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. 初始化遊戲

    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. 設置遊戲迴圈

    重構 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)
    
    };
    
  5. 新增程式碼:以一定的間隔移動敵人。

    重構 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 進行翻譯。儘管我們努力確保翻譯的準確性,但請注意,自動翻譯可能包含錯誤或不準確之處。原始文件的母語版本應被視為權威來源。對於關鍵信息,建議使用專業人工翻譯。我們對因使用此翻譯而引起的任何誤解或誤釋不承擔責任。