[Day4] Vite 出小蜜蜂~ Input Control 操作系统!

Day4

接下来卡比要是着操作 LaserCannon,让他可以左右移动。

Input

在上个章节,卡比介绍了 GameLoop

示意用

while (true) {
  processesInput();
  update();
  render();
}

但是,有个我们还没有实作到部分,也就是 processInput
这个环节会负责处理玩家输入的指令,并针对指令产生对应的动作。

各种平台会支援不同的输入硬件,像是 键盘、滑鼠、电玩手把 ...etc。
在这边,卡比会撰写如何使用键盘来操作游戏。

Keyboard Event

在浏览器,负责接收键盘输入相关的事件叫做 KeyboardEvent
不同於表单用的 InputEvent,这个只用来处理当下玩家在键盘做了何种操作,属於比较底层的事件。

而卡比接下来会需要 keydownkeyup 这两个事件,
透过这两个事件来得知玩家按下或放开哪个按键。

document.addEventListener("keydown", (event) => {});

document.addEventListener("keyup", (event) => {});

实作 InputSystem

首先,在 types.ts 增加一个 enum

-- src/types.ts

export enum Key {
  Left,
  Right,
}

然後建立一个新的资料夹 src/systems,并新建 src/systems/input.ts

有一点需要注意的,就是这个事件的触发速度跟 GameLoop 并不同步,
所以我们需要做一些调整,让处理速度跟 GameLoop 一样快。

-- src/systems/input.ts

import { Key } from "../types";

let pressed: Set<Key> = new Set();

document.addEventListener("keydown", (event) => {
  if (event.code === "ArrowLeft") {
    pressed.add(Key.Left);
  }

  if (event.code === "ArrowRight") {
    pressed.add(Key.Right);
  }
});

document.addEventListener("keyup", (event) => {
  if (event.code === "ArrowLeft") {
    pressed.delete(Key.Left);
  }

  if (event.code === "ArrowRight") {
    pressed.delete(Key.Right);
  }
});

export function getKeyPressed() {
  return Array.from(pressed);
}

这边我们提供 getKeyPressed 让其他程序码知道当下玩家按下的按键是什麽。
用 Array 或是 Set 来呈现 pressed 是因为玩家可能会同时按下两个以上的按键。

接着,我们要将这个资料传递到需要使用的物件。

新增 handleInputGameObject 介面,
因为不是每个物件都需要处理 handleInputupdate,这边我们采用 optional chaining

-- src/types.ts

export interface GameObject {
+ handleInput?(pressed: Key[]): void;
- update(delta: number): void;
+ update?(delta: number): void;
  render(app: Application): void;
}

-- src/main.ts

+ import { getKeyPressed } from "./systems/input";

const app = new Application({
  width: 20,
  height: 20,
  resolution: 10,
});

document.querySelector("#app")?.append(app.view);

+ const instance = LaserCannon();

app.ticker.add(() => {
  app.stage.removeChildren();

+ instance.handleInput?.(getKeyPressed());

- instance.update(app.ticker.deltaMS);
+ instance.update?.(app.ticker.deltaMS);

  instance.render(app.stage);
});

然後我们在 LaserCannon 里面实作 handleInput 这个方法。

-- src/characters/LaserCannon.ts

export default function LaserCannon(): GameObject {
  return {
    handleInput(pressed) {
      if (pressed.includes(Key.Left)) {
        console.log("move left");
        return;
      }

      if (pressed.includes(Key.Right)) {
        console.log("move right");
        return;
      }
    },

    render(app) {
      const graphics = new Graphics();

      for (let y = 0; y < image.length; y++) {
        for (let x = 0; x < image[y].length; x++) {
          if (image[y][x] === 0) continue;

          graphics.beginFill(0xffffff);

          graphics.drawRect(x, y, 1, 1);

          graphics.endFill();
        }
      }

      app.stage.addChild(graphics);
    },
  };
}

赶快试试看按下去的时候,console 会不会印出东西!

Position

接下来就是让 LaserCannon 动起来,
我们需要新增一个参数用来记录每个物件当前的位置。

首先,新增用於记录位置的型别 Vector
记得有实作 GameObject 介面的物件都要补上喔!

-- src/types.ts

export type Vector = {
  x: number;
  y: number;
};
export interface GameObject {
+ position: Vector;
  handleInput?(pressed: Key[]): void;
  update?(delta: number): void;
  render(app: Application): void;
}

接下来在 LaserCannon 这边实作 position

并在 handleInput 这边判断,
假设按下的是左键,就往左边移动 1 px,
假设按下的是右键,就往右边移动 1 px,。

-- src/characters/LaserCannon.ts

export default function LaserCannon(): GameObject {
  return {
+   position: { x: 0, y: 0 },

    handleInput(pressed) {
      if (pressed.includes(Key.Left)) {
-       console.log("move left");
+       this.position.x -= 1;
        return;
      }

      if (pressed.includes(Key.Right)) {
-       console.log("move right");
+       this.position.x += 1;
        return;
      }
    },

    render(app) {
      const graphics = new Graphics();

      for (let y = 0; y < image.length; y++) {
        for (let x = 0; x < image[y].length; x++) {
          if (image[y][x] === 0) continue;

          graphics.beginFill(0xffffff);

          graphics.drawRect(x, y, 1, 1);

          graphics.endFill();
        }
      }

      app.stage.addChild(graphics);

+     graphics.position.set(this.position.x, this.position.y);
    },
  };
}

最後,将位置同步到 Graphics 物件上,就可以操作 LaserCannon 啦!

对了,画面太小记得调大一点。

-- src/main.ts

const app = new Application({
- width: 20,
+ width: 80,
- height: 20,
+ height: 80,
- resolution: 10,
+ resolution: 5,
});

小考题

  1. 请问要如何让 LaserCannon 移动范围不要超过我们的画面大小呢?

提示 1

function clamp(min: number, max: number, value: number) {
  return Math.max(Math.min(max, value), min);
}

提示 2
app.screen

关於兔兔们:


<<:  Day 4 : Git 分支与远端仓库

>>:  Day6: [资料结构] -  Set

Day 13 Flask 与 Tensorflow Serving 的沟通

Tensorflow Serving 虽然帮你跑模型,但它并不负责展示网页,或是一些预处理的部分。 ...

网页表格-30天学会HTML+CSS,制作精美网站

网页需要制作时间表、收费表等,都可以使用表格制作。 只要了解表格相关的标签,就能够轻松做出表格了,这...

11 - Metrics - 观察系统的健康指标 (5/6) - 使用 Metricbeat 掌握 Infrastructure 的健康状态 Kubernetes 篇

Metrics - 观察系统的健康指标 系列文章 (1/6) - Metrics 与 Metricb...

【Day30】 晋升成铁人龙猫之总结

哈罗~ 今天是铁人赛的最後一天, 来抢个团队中第一发文的位子XD 之前每几日来个小结, 最後一天就来...

Day 26 - WooCommerce: 定义虚拟帐号付款闸道

永丰金流收款 API 在目前我们从文件看到的,支援信用卡付款及虚拟帐号 ATM 付款。信用卡付款方式...