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

13 KiB

构建太空游戏第三部分:添加运动

课前测验

课前测验

游戏只有在屏幕上有外星人跑来跑去时才会变得有趣!在这个游戏中,我们将使用两种类型的运动:

  • 键盘/鼠标运动:当用户通过键盘或鼠标与屏幕上的对象交互时。
  • 游戏驱动运动:当游戏以一定的时间间隔移动对象时。

那么我们如何在屏幕上移动物体呢?这完全是关于笛卡尔坐标系的:我们改变对象的位置 (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

通过输入以下命令启动你的项目:

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. 实现发布订阅模式,这将使你的代码在后续部分保持整洁。

    要完成最后这部分,你可以:

    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进行翻译。尽管我们努力确保准确性,但请注意,自动翻译可能包含错误或不准确之处。应以原始语言的文档作为权威来源。对于关键信息,建议使用专业人工翻译。对于因使用本翻译而引起的任何误解或误读,我们概不负责。