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

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);
+ }
}

关於兔兔们:


<<:  【Day 7】GIT版本还原及回归

>>:  JAVA - JAVA Maven 错误处理

入门魔法 - 阵列

前情提要 经过上一回的测验,我发现了自己不是天选之人。 「哪尼,为什麽我不是天选之人,我不是有魔力吗...

Day04 关於分散式系统的一些概念 (一)

今天开始要讲分散式系统的一些概念罗。 影片在此: Day04_关於分散式系统的一些概念 (一) ...

【day18】聊天室(上) X Realtime database

好的,今天我们要来看的就是我们的精华啦-聊天室。 原本我们在设计邀约流程的时候是。 (原本设想的流程...

[第十四只羊] 迷雾森林舞会VII 开完房间後走进房间

天亮了 昨晚是平安夜 关於迷雾森林故事 焦虑抑制剂 4号:我跟全场站不同边耶,我站7耶,我跟7号玩家...

【第十九天 - PHP反序列化(1)】

Q1. 什麽是 php 反序列化? 为了让程序中的物件可以在保存到 persistent datab...