iT邦幫忙

2021 iThome 鐵人賽

DAY 1
0
Modern Web

Vite 出小蜜蜂~和卡比一起玩網頁遊戲開發!系列 第 6

[Day5] Vite 出小蜜蜂~ Component 元件!

  • 分享至 

  • xImage
  •  

Day5

寫程式寫到一定的階段後,會開始發現,其實做出想要的功能並不困難。
真正難的,其實是如何寫出有彈性的程式碼以應對各種需求跟變化。
卡比接下來要做的,是在一般遊戲引擎都會實作的設計模式,Component

Entity (GameObject) Component and System

在遊戲設計中的 Component 跟 Web 的 Component 不同,
這邊的 Component 是用來提供 GameObject 行為的。

不過卡比會進一步將邏輯跟資料的部分在拆出來,
Component 用於封裝資料, System 用於處理邏輯。

Renderer Component and Render System

卡比注意到,目前每個遊戲角色都有一個類似的 render 函式,
而這部分的程式碼幾乎一樣,我們來試試看能不能將他共用。

首先,先在 src/types.ts 定義新的介面。

-- src/types.ts

export interface Renderer {
  renderer: {
    type: "graphics";
    src: number[][];
  };
}

並修改 GameObject

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

接著,卡比以 src/characters/LaserCannon.ts 作為範例,進行修改
注意到,卡比在這邊用了 Type Intersections 的方式來延展 GameObject

-- src/characters/LaserCannon.ts

export default function LaserCannon(): GameObject & Renderer {
  return {
    renderer: {
      type: "graphics",
      src: [
        [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0],
        [0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0],
        [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
      ],
    },

    position: { x: 0, y: 0 },

    handleInput(pressed) {
      if (pressed.includes(Key.Left)) {
        this.position.x -= 1;
        return;
      }

      if (pressed.includes(Key.Right)) {
        this.position.x += 1;
        return;
      }
    },
  };
}

建立新的檔案 src/systems/render.ts
專門用來處理 render 相關的邏輯。

function Graphics({ renderer }: Renderer) {
  const src = renderer.src;
  const graphics = new _Graphics();

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

      graphics.beginFill(0xffffff);

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

      graphics.endFill();
    }
  }

  return graphics;
}

export function render(stage: Container, instance: GameObject & Renderer) {
  let renderer: DisplayObject | undefined = undefined;

  if (instance.renderer.type === "graphics") {
    renderer = Graphics(instance);
  }

  if (renderer) {
    stage.addChild(renderer);

    renderer.position.set(instance.position.x, instance.position.y);
  }
}

接著,更改 src/main.ts 來接上我們的 Render System

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

  instance.handleInput?.(getKeyPressed());

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

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

確認畫面運作沒問題,我們的重構就完成了。

Transform System

接下來,將 LaserCannon 的其他程式碼也一併 Component 化。

-- src/types.ts

export type Vector = {
  x: number;
  y: number;
};

export interface Transform {
  position: Vector;
}

export interface Control {
  handleInput(pressed: Key[]): void;
}

export interface GameObject {
  update?(delta: number): void;
}

-- src/characters/LaserCannon.ts

import { clamp } from "../functions/utils";
import { Control, GameObject, Key, Renderer, Transform } from "../types";

export default function LaserCannon(): GameObject &
  Transform &
  Control &
  Renderer {
  return {
    renderer: {
      type: "graphics",
      src: [
        [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0],
        [0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0],
        [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
      ],
    },

    position: { x: 10, y: 10 },

    handleInput(pressed) {
      if (pressed.includes(Key.Left)) {
        this.position.x -= 1;
        return;
      }

      if (pressed.includes(Key.Right)) {
        this.position.x += 1;
        return;
      }
    },
  };
}

因為我們將 render 相關的邏輯移到 Render System
但還未實作 Transform 的相關邏輯。

System 中,我們需要過濾被傳入的物件是否擁有 Transform 元件,
如果擁有 Transform 才需要執行 position 相關的邏輯操作。

-- src/types.ts

export function canTransform<T extends GameObject>(
  instance: T
): instance is T & Transform {
  return "position" in instance;
}

-- src/systems/render.ts

export function render(stage: Container, instance: GameObject & Renderer) {
  let renderer: DisplayObject | undefined = undefined;

  if (instance.renderer.type === "graphics") {
    renderer = Graphics(instance);
  }

  if (renderer) {
    stage.addChild(renderer);
  }

+ if (renderer && canTransform(instance)) {
+   renderer.position.set(instance.position.x, instance.position.y);
+ }
}

關於兔兔們:


上一篇
[Day4] Vite 出小蜜蜂~ Input Control 操作系統!
下一篇
[Day6] Vite 出小蜜蜂~ Scene 場景!
系列文
Vite 出小蜜蜂~和卡比一起玩網頁遊戲開發!19
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言