13. 敵の移動パターン

はじめに

敵の動きがワンパターンだったので、敵ごとに動き方を設定しました。

方針

  • MovementStrategyComponentを作成します
  • createEnemy で敵タイプごとに MovementStrategyComponent を設定します
    • 各MovementStrategyごとに必要なパラメータやステートも用意
  • EnemyAISystem で MovementStrategy に応じてvelocityを計算してspriteに設定します

フローチャート

前回のフローチャートで流れを確認してみます。

flowchart TD
  subgraph EnemyAISystem
    subgraph update
      A{{すべての敵についてループ}} -->
      Call[movementStrategyに応じてupdateXXXMovement呼び出し]
    end

    Call -.-> updateXXXMovement

    subgraph updateXXXMovement
      direction TB
      velocity計算 -->
      movmentStrategy.state更新 -->
      sprite.setVelocity
    end
  end

移動パターン

現在のプレイヤーまで直線で向かう(Direct)に

  • ジグザグに動きながら寄ってくる(ZigZag)
  • 渦巻き状に近寄ってくる(Spiral)

を追加しました。

コード

MovementStrategyComponent.ts

ジグザグ移動は、直線移動にサイン波を足して計算します。現在のサイン波の位置をphaseというステートに保存しています。

export type MovementType = "direct" | "zigzag" | "spiral";

export interface ZigzagMovementParams {
  amplitude: number;
  frequency: number;
}

export interface ZigzagMovementState {
  phase: number;
}

export interface SpiralMovementParams {
  clockwise: boolean;
  spiralStrength: number;
}

export type MovementStrategyComponent =
  | { type: "direct"; params: {}; state: {} }
  | { type: "zigzag"; params: ZigzagMovementParams; state: ZigzagMovementState }
  | { type: "spiral"; params: SpiralMovementParams; state: {} };

createEnemy.ts

// createEnemy関数に以下を追加
...
// 敵のタイプによって異なる移動戦略を設定
switch (enemyType) {
  case "slime":
    ComponentManager.set("movementStrategy", entity, {
      type: "direct",
      params: {},
      state: {},
    });
    break;
  case "spider":
    ComponentManager.set("movementStrategy", entity, {
      type: "zigzag",
      params: { amplitude: 50, frequency: 2 },
      state: { phase: 0 },
    });
    break;
  case "bat":
    ComponentManager.set("movementStrategy", entity, {
      type: "spiral",
      params: { spiralStrength: 0.8, clockwise: Phaser.Math.Between(0, 1) > 0.5 },
      state: {},
    });
    ComponentManager.set("flyTag", entity, {});
    break;
}

EnemyAISystem.ts

export class EnemyAISystem {
  update(delta: number) {
    for (const entity of ComponentManager.entitiesWith("enemyTag", "target")) {
      const { targetX, targetY } = ComponentManager.get("target", entity);
      const { speed } = ComponentManager.get("speed", entity);
      const { sprite: enemySprite } = ComponentManager.get("sprite", entity);
      const targetPosision = { x: targetX, y: targetY };
      // MovementStrategy
      let movementStrategy = ComponentManager.getOptional("movementStrategy", entity);
      if (!movementStrategy) continue; // 未設定なら移動しない
      switch (movementStrategy.type) {
        case "direct":
          this.updateDirectMovement(enemySprite, targetPosision, speed);
          break;
        case "spiral":
          this.updateSpiralMovement(enemySprite, targetPosision, speed, movementStrategy);
          break;
        case "zigzag":
          this.updateZigzagMovement(enemySprite, targetPosision, speed, movementStrategy, delta);
          break;
      }
    }
  }

移動方法ごとのVelocity計算は以下のとおりです。

直線移動 (Direct)

private updateDirectMovement(sprite: Phaser.Physics.Arcade.Sprite, target: { x: number; y: number }, speed: number) {
  const { vx, vy } = calcVelocity(sprite, target, speed);
  sprite.setVelocity(vx, vy);
}

渦巻き状 (Spiral)

private updateSpiralMovement(
  sprite: Phaser.Physics.Arcade.Sprite,
  target: { x: number; y: number },
  speed: number,
  strategy: MovementStrategyComponent
) {
  const { clockwise, spiralStrength } = strategy.params as SpiralMovementParams;
  // targetへのベクトル
  const direction = new Phaser.Math.Vector2(target.x - sprite.x, target.y - sprite.y);
  const distance = direction.length();
  if (distance < 10) {
    sprite.setVelocity(0, 0);
    return;
  }
  // 垂直なベクトル
  const perpendicular = new Phaser.Math.Vector2(-direction.y, direction.x).scale(clockwise ? 1 : -1);
  // スパイラルの強さを考慮して、目標地点に向かうベクトルと垂直なベクトルを組み合わせる
  const spiralVector = direction.scale(1 - spiralStrength).add(perpendicular.scale(spiralStrength));
  // 速度を計算
  const velocity = spiralVector.normalize().scale(speed);
  sprite.setVelocity(velocity.x, velocity.y);
}

ジグザグ移動 (ZigZag)

private updateZigzagMovement(
  sprite: Phaser.Physics.Arcade.Sprite,
  target: { x: number; y: number },
  speed: number,
  strategy: MovementStrategyComponent,
  delta: number
) {
  // 基本方向を計算
  const targetVector = new Phaser.Math.Vector2(target.x, target.y);
  const spriteVector = new Phaser.Math.Vector2(sprite.x, sprite.y);
  const distance = targetVector.distance(spriteVector);
  if (distance < 10) {
    sprite.setVelocity(0, 0);
    return;
  }
  const direction = targetVector.subtract(spriteVector).normalize();

  // ジグザグ効果を計算(サイン波を使用
  const params = strategy.params as ZigzagMovementParams;
  const state = strategy.state as ZigzagMovementState;
  state.phase += 2 * Math.PI * (delta / 1000) * params.frequency; // 1秒間にfrequency回の周期
  let amp = params.amplitude;
  if (distance < 100) amp *= Phaser.Math.Linear(0, 1, 1 - distance / 100);
  const sideOffset = Math.sin(state.phase) * params.amplitude;
  // 方向ベクトルに垂直なベクトルを計算
  const perpendicular = new Phaser.Math.Vector2(-direction.y, direction.x);
  // 最終的な速度を計算
  const velocity = direction.scale(speed).add(perpendicular.scale(sideOffset));
  sprite.setVelocity(velocity.x, velocity.y);
}

まとめ

敵の移動パターンを追加しました。 さらに、最終ウエーブでボスも登場させました。

次回はボスが火の玉を吐くようにしたいと思います。