[Day14] Vite 出小蜜蜂~ Game Logic - Sequential Movement!

Day14

Space Invaders 的游戏设计中,
Enemy 的移动逻辑扮演了非常重要的角色,
他为游戏提供了难度,并随着玩家每次击杀 Enemy 增加难度,
是这款游戏最关键的游戏设计。

接下来,卡比要实作原作游戏中,Enemy 的移动逻辑。

Tags

首先,为了让游戏逻辑能够操作特定的游戏物件,
我们需要新增一个概念,tags

透过 tags
我们可以知道当前的游戏物件是什麽,
并对其提供相对应的操作。

-- src/types.ts

export interface GameObject {
  tags?: string[];
  destroy?: boolean;
  update?(delta: number): void;
}

Enemy

为了要将游戏逻辑集中在一处,
我们需要将 Enemy 的逻辑做些调整。

透过分析原作,卡比发现,
Enemy 会在每次移动时才会切换图片,
於是我们要设计一个 Proxy,外面的逻辑可以透过操作 Proxy 来操作图片。

并且需要一个 id
这个在我们逻辑操作时方便我们知道当前物件的位置以及确定个数。

export type IEnemy = GameObject &
  Transform &
  Renderer &
  Collision &
  Shooter & { id: number; frame: number };

export default function Enemy({ type, id, position }: EnemyProps): IEnemy {
  const images = EnemyImages[type];

  let current = 0;

  return {
    id,
    tags: ["enemy"],
    position,

    set frame(value) {
      current = value % images.length;

      this.renderer.src = images[current];
    },
    get frame() {
      return current;
    },

    canShoot: false,
    shoot() {
      const { x, y } = this.position;
      const [w, h] = [images[0].length, images.length];

      return EnemyLaser({
        position: { x: x + w / 2, y: y + h + 1 },
        update(it) {
          it.position.y += 1;
        },
      });
    },

    renderer: {
      type: "graphics",
      src: images[current],
    },

    collider: {
      size: { x: images[0].length, y: images.length },
    },
  };
}

Game

根据原作,Enemy 原先的位置决定了其移动的顺序,
为了复刻原作 按照顺序一个接一个的移动
要透过 id 来对应 Enemy 原先的位置。

对照表示意如下:

[
  [44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54],
  [33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43],
  [22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32],
  [11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21],
  [ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10],
]

实作如下:

-- src/scenes/Game.ts

const GRID_SIZE = 16;
const ROW_WIDTH = 11;

const points: EnemyProps[][] = [
  "squid",
  "crab",
  "crab",
  "octopus",
  "octopus",
].map((type, y, list) =>
  Array.from({ length: ROW_WIDTH }, (_, x) => ({
    id: (list.length - 1 - y) * ROW_WIDTH + x,
    type: type as EnemyTypes,
    position: { x: x * GRID_SIZE, y: y * GRID_SIZE },
  }))
);

GameLogic

为了集中管理游戏逻辑,我们要开一个新的资料夹。
src 底下建立一个新的资料夹 logic 并建立一个档案 SequentialMovement.ts

SequentialMovement 是一个 higherOrderFunction
他会回传一个 update 函式,用於每次刷新时执行。

SequentialMovement 会负责提供 Enemy 在游戏中的移动行为 ,细节如下:

  • 每次画面刷新只移动一个 Enemy 并横向移动。
  • 每移动十步会:
    • 开始反向移动。
    • 往下移动一次。

实作如下:

-- src/logic/SequentialMovement.ts

import { isEnemy } from "../characters/Enemy";
import { GameObject } from "../types";

type Props = {
  counts: number;
  step: number;
};
export function SequentialMovement({ counts, step }: Props) {
  const movement = { x: step, y: 0 };
  let pedometer = 0;
  let index = 0;

  return (instances: GameObject[]) => {
    const enemies = instances.filter(isEnemy);

    let processed = enemies.length > 0;

    while (processed) {
      enemies
        .filter((instance) => instance.id === index)
        .forEach((instance) => {
          instance.position.x += movement.x;
          instance.position.y += movement.y;
          instance.frame += 1;

          processed = false;
        });

      index = (index + 1) % counts;
    }

    if (index === 0) {
      if (pedometer === 0) movement.y = 0;

      pedometer += 1;
    }

    if (pedometer <= 10) return;

    movement.x *= -1;
    movement.y = step;

    pedometer = 0;
  };
}

Apply

接着我们只要在 Game 套上逻辑即可。

-- src/scenes/Game.ts

export default function Game(screen: Rectangle): Scene<Container> {
  let instances: GameObject[] = [LaserCannon(screen), ...spawn(Enemy, points)];

  const update = SequentialMovement({
    counts: instances.filter(isEnemy).length,
    step: 2,
  });

  return {
    update(delta) {
      collisionDetect(instances.filter(canCollision).filter(canTransform));

      update(instances);

      instances.forEach((instance) => {
        if (canControl(instance)) {
          instance.handleInput(getKeyPressed());
        }

        if (canShoot(instance) && instance.canShoot) {
          requestAnimationFrame(() => {
            instances = [...instances, instance.shoot()];
          });

          instance.canShoot = false;
        }

        if (instance.destroy) {
          requestAnimationFrame(() => {
            instances = instances.filter((_instance) => _instance !== instance);
          });

          return;
        }

        instance.update?.(delta);
      });
    },

    render(stage) {
      clear();

      instances
        .filter(canRender)
        .forEach((instance) => render(stage, instance));
    },
  };
}

关於兔兔们:


<<:  [Day19] 长尾问题:案例分析

>>:  Day 5 - Using Argon2 for Password Verifying with ASP.NET Web Forms C# 使用 Argon2 验证密码

Day12-Express 的部署

Express 利用 pm2 做管理(因为 docker 坑很深 加上来的话会写不完) Expres...

[Python 爬虫这样学,一定是大拇指拉!] DAY28 - 实战演练:集大成 - 自动更新每日个股日成交资讯

自动更新每日个股日成交资讯 结合前几篇所学,我们来做一个可以自动更新日成交资讯的程序吧! Reque...

Day25 - LIFF 使用入门

LINE Developers:https://developers.line.biz/zh-ha...

第33天~还原资料库

这个的上一篇:https://ithelp.ithome.com.tw/articles/10283...

<Day9> Contract — 取得期货(Futures)资讯

● 这章将以模拟帐户来示范如何取得期货(Futures)资讯 回顾上一章,我们学习如何用Contra...