🤼‍♂️

【Unity】ユニットの一元管理とAIの基本【タワーディフェンス】

2020/11/27に公開

はじめに

前回タワーディフェンスの作り方で敵と味方の出し方を紹介しました。
今回は移動と索敵、戦闘について書こうと思います。
前回の続きとなりますのでこちらの記事を見ておくと理解しやすいかと思います。
https://zenn.dev/supple/articles/2012a0ebfe13181286af

ユニットの一元管理

まずは色々とやりやすくするためにユニットを一元管理できるようにしましょう。
前回作成したBattleManagerにユニットを管理する機能を追加します。
前回のコードからの差分となります。

BattleManager.cs
// 出撃中のユニット
public List<Unit> sortieUnits = new List<Unit>();

BattleManagerに管理するユニットのリストを追加します。
ユニットの方には初期化が終わったらBattleManagerのリストに自分を追加する部分を追加します。

Unit.cs
private void Start()
{
    // TODO:ユニットの初期化処理
    
    // ユニットリストに自分を追加する
    BattleManager.Get().sortieUnits.Add(this);
}

これでBattleManagerはすべてのユニットの情報を持つことになり、バトル全体の管理がしやすくなります。

ユニットのUpdateをBattleManagerが握る

MonoBehaviourのUpdateはオブジェクト単独で動作することができ便利ですが、シミュレーションやRPGのような「場」を管理する必要があるゲームには不便になることがあります。
次のようにすることでBattleManagerがユニットのUpdateを制御することが可能になります。

BattleManager.cs
private void Update()
{
    foreach(Unit unit in sortieUnits)
    {
        unit.UpdateUnit(Time.deltaTime);
    }
}
Unit.cs
// ユニットの状態を更新する
public void UpdateUnit(float deltaTime)
{
    // TODO:ユニットの行動を記述する
}

UnitクラスのUpdateUnitはMonoBehaviourのUpdateの代わりです。
Unitクラス自前のUpdateを使わずにBattleManagerを経由してUpdateを行います。
こうすることで比較的自由なユニットの制御が可能になります。
例えばポーズ機能なんかはこうするだけで楽に実装できたりします[1]

BattleManager.cs
public bool isPause = false; // ポーズフラグ
private void Update()
{
    // ポーズ中はユニットを更新しない
    if(isPause) return;
    
    foreach(Unit unit in sortieUnits)
    {
        unit.UpdateUnit(Time.deltaTime);
    }
}

ユニットのAI

ユニットを動かすための枠組みは出来たので今度は中身を見てみましょう。
タワーディフェンスの行動パターンとはどんなものでしょうか。

ユニットの行動パターン

行動1:移動する

ユニットは配置したら自動で進軍を開始します。
ユニットの一番基本的な行動です。
この行動をメソッド化します。

Unit.cs
public Vector3 targetPosition; // 目標地点
public float moveSpeed;        // 移動速度

// 移動する
void MoveAction(float deltaTime)
{
    // 目的地に向かって移動する
    transform.position = Vector3.MoveTowards(transform.position, targetPosition, moveSpeed * deltaTime);
}

行動2:索敵する

ユニットは近くに敵を見つけると戦い始めます。
近くに敵がいるかどうかを判断するためにBattleManagerに一番近い敵を探すメソッドを追加しましょう。

Unit.cs
public float Distance(Unit targetUnit)
{
    return Vector3.Distance(transform.position, targetUnit.transform.position);
}

その前にUnitクラスにユニット同士の距離を取得するメソッドを作っておきます。
1行のメソッドですが、距離の算出は割とよく使うのでメソッド化しておいた方がいいでしょう。

Battlemanager.cs
public Unit FindNearestEnemy(Unit unit)
{
    Unit nearest_enemy = null;
    float nearest_distance = float.MaxValue;

    foreach (Unit enemy in sortieUnits)
    {
        if (enemy.IsDead() || !unit.IsEnemy(enemy))
        {   // 死んでない敵以外は無視する
            continue;
        }

        float distance = unit.Distance(enemy);
        if (distance < nearest_distance)
        {   // 一番近い敵を覚えておく
            nearest_enemy = enemy;
            nearest_distance = distance;
        }
    }
    // 一番近い敵を返す
    return nearest_enemy;
}

全ユニットをなめて一番距離の近い敵を探しています。

行動3:攻撃する

敵を見つけたらひたすら攻撃します。
行動間隔は攻撃したあと、次に攻撃できるまでの時間を表しています。
敵を倒したら交戦相手から外します。

Unit.cs
public float actionInterval; // 行動間隔
float actionWait;            // 次の行動までの時間
Unit battleTarget;           // 交戦相手

// 攻撃行動ををするメソッド
void AttackAction(float deltaTime)
{
    // 次の行動間隔まで待つ
    actionWait -= deltaTime;
    if (actionWait > 0) return;
    
    // 次の行動をするまでの待ち時間をセット
    actionWait += actionInterval;
    
    // 攻撃する
    Attack(battleTarget);
    if(battleTarget.IsDead())
    {   // 相手を倒したらターゲットから外す
        battleTarget = null;
    }
}

ユニットの制御

これらの行動パターンをUpdateUnitで制御します。
行動の優先順位は以下の通りです

  1. 交戦相手がいるとき、攻撃行動を取る
  2. 一番近い敵を探し、索敵範囲内なら交戦相手に設定する
  3. 目標に向かって移動する
Unit.cs
public float searchEnemyDistance;   // 索敵範囲

public void UpdateUnit(float deltaTime)
{
    if (battleTarget != null)
    {   // 交戦相手がいるとき、攻撃行動を取る
        AttackAction(deltaTime);	
    }
    else
    {   // 交戦相手がいないとき一番近い敵を探す
        Unit enemy = BattleManager.Get().FindNearestEnemy(this);
        if(enemy != null && Distance(enemy) <= searchEnemyDistance)
        {   // 一番近い敵が索敵範囲内なら交戦に入る
            battleTarget = enemy;
        }
        else
        {   // いなかったら目的地に向かって移動する
            MoveAction(deltaTime);	
        }
    }
}

最後に

前回と今回の内容をまとめると以下の機能を持つユニットをフィールドに配置することができるようになります。
・攻撃力
・防御力
・HP
・移動速度
・攻撃間隔
・索敵範囲
基本的なユニットの機能としては十分じゃないでしょうか。
あとは作りたいものに必要なものを拡張していけばよいと思います。

それでは良い創作ライフを!

脚注
  1. AnimatorControllerや物理エンジンを使用している場合は別途対応が必要です ↩︎

Discussion