【Unity】キャラの状態をオブジェクトにする

投稿者: | 2021-05-18

ナビメッシュエージェントコンポーネントを付けた敵キャラの状態をオブジェクトにして管理してみました。

敵に付けるスクリプトのクラスの中に状態のクラスを定義して、各状態はそのクラスを継承します。各状態のクラスにその状態に必要なフィールドやメソッドを定義します。

そして、現在の状態オブジェクトのそれらのメソッドを、外側のスクリプトのUpdateメソッドやOnCollisionEnterメソッドの中で呼ぶことで、状態によって敵の動きがかわります。

内部クラス

// 外側のクラス
class A
{
    // 内部のクラス
    class B
    {

    }
}

普通のクラスのアクセシビリティはデフォルトでinternalですが、内部クラスの場合はprivateになります。privateだと、外部のクラスからは見えなくなります。

publicにするとアクセスできます。

また、内部クラスがprivateだと、そのクラスを戻り値にしたpublicメソッドを定義できないので、外部に渡すこともできません。

内部クラスからは、外側のクラスのプライベートなメンバにもアクセスできます。

// 外側のクラス
public class A
{
    // プライベートなメンバ
    private int i; 

    // 内部のクラス
    class B
    {
        A a;
        
        void Method()
        {
            // 内部クラスから値を変える
            a.i++;
        }
    }
}

敵のスクリプト

敵に付けるスクリプトでは、はじめマップを徘徊させて、ボールが当たると3秒間立ち止まり、その後スピードを上げてプレイヤーを追跡します。その状態で再度ボールを当てると、また3秒立ち止まってから徘徊状態に戻ります。

この4つの状態を表す内部クラスを作って、その中のメソッドをUpdateメソッドやOnCollisionEnterメソッドで呼びます。

using UnityEngine;
using UnityEngine.AI;

public class Agent1 : MonoBehaviour
{
    NavMeshAgent agent;
    MeshRenderer meshRenderer;

    float defaultSpeed;
    [SerializeField] Transform points; // 目的地のrootオブジェクト
    [SerializeField] Transform player;

    State[] states; // 各状態オブジェクトを入れる配列
    int currentIndex; // 現在の状態のインデックス

    private void Awake()
    {
        agent = GetComponent<NavMeshAgent>();
        meshRenderer = GetComponent<MeshRenderer>();

        // デフォルトのスピードを保存
        defaultSpeed = agent.speed;

        states = new State[4];
        states[0] = new Wandering();
        states[1] = new Stopping();
        states[2] = new Tracking();
        states[3] = new Hitted();

        // 状態を変える
        SetState(0);
    }

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // 現在の状態を取得
    public int GetState()
    {
        return currentIndex;
    }

    // 状態を変える
    void SetState(int index)
    {
        // 現在の状態を出たときのメソッドを呼ぶ
        states[currentIndex].ExitState(this);

        // 状態を変更する
        currentIndex = index;

        // 現在の状態に入ったときのメソッドを呼ぶ
        states[currentIndex].EnterState(this);
    }

    // Update is called once per frame
    void Update()
    {
        // 現在の状態のUpdateメソッドを呼ぶ
        states[currentIndex].Update(this);
    }

    // ナビメッシュエージェントの目的地を変える
    void SetDestination(Vector3 pos)
    {
        agent.destination = pos;
    }

    void SetDestination()
    {
        // ランダムで次の目的地を設定
        agent.destination = points.GetChild(Random.Range(0, points.childCount)).position;
    }

    // ナビメッシュエージェントのスピードを変える
    void SetSpeed(float speed)
    {
        agent.speed = speed;
    }

    void SetSpeed()
    {
        agent.speed = defaultSpeed;
    }

    // マテリアルの色を変える
    void SetColor(Color color)
    {
        meshRenderer.material.SetColor("_BaseColor", color);
    }

    // 状態の基底クラス
    class State
    {
        public virtual void Update(Agent1 a) { }
        public virtual void EnterState(Agent1 a) { }
        public virtual void ExitState(Agent1 a) { }
        public virtual void CollisionEnter(Agent1 a, Collision collision) { }
    }

    // 徘徊中の状態
    class Wandering: State
    {
        public override void Update(Agent1 a)
        {
            // 目的地に付いたとき
            if (!a.agent.pathPending && a.agent.remainingDistance < 0.5f)
            {
                // 次の目的地を設定
                a.SetDestination();
            }
        }

        public override void EnterState(Agent1 a)
        {
            // スピードをデフォルトに戻す
            a.SetSpeed();

            // 目的地を設定
            a.SetDestination();

            // マテリアルの色を変える
            a.SetColor(Color.green);
        }

        // ボールがぶつかったとき
        public override void CollisionEnter(Agent1 a, Collision collision)
        {
            a.SetState(1);
        }
    }

    // 立ち止まっている状態
    class Stopping : State
    {
        float sec;

        public override void Update(Agent1 a)
        {
            // 秒読み
            sec += Time.deltaTime;

            // 3秒立つと次の状態に遷移
            if (sec >= 3f)
            {
                SetNextState(a);
            }
        }

        public override void EnterState(Agent1 a)
        {
            // 秒数を初期化
            sec = 0f;

            // 立ち止まる
            a.SetSpeed(0f);

            // マテリアルの色を変える
            a.SetColor(Color.yellow);

        }

        protected virtual void SetNextState(Agent1 a)
        {
            a.SetState(2);
        }
    }

    // プレイヤーを追跡している状態
    class Tracking : State
    {

        public override void Update(Agent1 a)
        {
            // プレイヤーを追い続ける
            a.SetDestination(a.player.position);
        }

        public override void EnterState(Agent1 a)
        {
            // スピードを上げる
            a.SetSpeed(10f);

            // マテリアルの色を変える
            a.SetColor(Color.red);
        }

        // ボールがぶつかったとき
        public override void CollisionEnter(Agent1 a, Collision collision)
        {
            a.SetState(3);
        }
    }

    // 追跡中にボールを当てられた状態
    class Hitted : Stopping
    {
        protected override void SetNextState(Agent1 a)
        {
            a.SetState(0);
        }
    }

    // コライダーが衝突した時に呼ばれる
    private void OnCollisionEnter(Collision collision)
    {
        // ボールが衝突したとき
        if(collision.collider.tag == "Ball")
        {
            // 現在の状態の衝突メソッドを呼ぶ
            states[currentIndex].CollisionEnter(this, collision);
        }
    }
}

状態クラスには、毎フレーム呼ばれるメソッドや、状態の切り替わりで呼ばれるメソッド、衝突時に呼ばれるメソッドがあります。各状態はこのクラスを継承します。

// 状態の基底クラス
class State
{
    public virtual void Update(Agent1 a) { }
    public virtual void EnterState(Agent1 a) { }
    public virtual void ExitState(Agent1 a) { }
    public virtual void CollisionEnter(Agent1 a, Collision collision) { }
}

// 徘徊中の状態
class Wandering: State
{

// ...

外側のクラスでは、まず初期化のために呼ばれるAwakeメソッドで、各コンポーネントの取得やフィールドの初期化をしています。

State[] states; // 各状態オブジェクトを入れる配列
int currentIndex; // 現在の状態のインデックス

private void Awake()
{
    agent = GetComponent<NavMeshAgent>();
    meshRenderer = GetComponent<MeshRenderer>();

    // デフォルトのスピードを保存
    defaultSpeed = agent.speed;

    states = new State[4];
    states[0] = new Wandering();
    states[1] = new Stopping();
    states[2] = new Tracking();
    states[3] = new Hitted();

    // 状態を変える
    SetState(0);
}

各状態クラスのインスタンスを保存しておく配列を作って、ここで4つの状態をインスタンス化します。最後に状態を変えるメソッドを呼んでいます。

状態を変えるメソッドでは、現在の状態を表すint型のフィールドに、引数の値を入れます。その前後で、現在の状態が変わったときのメソッドを呼んでいます。

// 状態を変える
void SetState(int index)
{
    // 現在の状態を出たときのメソッドを呼ぶ
    states[currentIndex].ExitState(this);

    // 状態を変更する
    currentIndex = index;

    // 現在の状態に入ったときのメソッドを呼ぶ
    states[currentIndex].EnterState(this);
}

現在の状態は整数値でわかります。

// 現在の状態を取得
public int GetState()
{
    return currentIndex;
}

毎フレーム呼ばれるUpdateメソッドでは、現在の状態オブジェクトのUpdateメソッドを呼びます。

// Update is called once per frame
void Update()
{
    // 現在の状態のUpdateメソッドを呼ぶ
    states[currentIndex].Update(this);
}

同様に、コライダーが衝突した時に呼ばれるOnCollisionEnterメソッドの中でも、現在の状態オブジェクトの衝突メソッドを呼びます。

// コライダーが衝突した時に呼ばれる
private void OnCollisionEnter(Collision collision)
{
    // ボールが衝突したとき
    if(collision.collider.tag == "Ball")
    {
        // 現在の状態の衝突メソッドを呼ぶ
        states[currentIndex].CollisionEnter(this, collision);
    }
}

状態クラス

各状態クラスは、状態の基底クラスを継承します。そして、基底クラスの必要なメソッドをオーバーライドして処理を記述します。基底クラスのメソッドには何も書かれていないので、何もしたくないときはオーバーライドしません。

例えば、徘徊中の状態クラスでは、Updateメソッドと状態に入ったときのメソッド、衝突したときのメソッドをオーバーライドしています。

// 状態の基底クラス
class State
{
    public virtual void Update(Agent1 a) { }
    public virtual void EnterState(Agent1 a) { }
    public virtual void ExitState(Agent1 a) { }
    public virtual void CollisionEnter(Agent1 a, Collision collision) { }
}

// 徘徊中の状態
class Wandering : State
{
    public override void Update(Agent1 a)
    {
        // 目的地に付いたとき
        if (!a.agent.pathPending && a.agent.remainingDistance < 0.5f)
        {
            // 次の目的地を設定
            a.SetDestination();
        }
    }

    public override void EnterState(Agent1 a)
    {
        // スピードをデフォルトに戻す
        a.SetSpeed();

        // 目的地を設定
        a.SetDestination();

        // マテリアルの色を変える
        a.SetColor(Color.green);
    }

    // ボールがぶつかったとき
    public override void CollisionEnter(Agent1 a, Collision collision)
    {
        // 立ち止まっている状態へ遷移
        a.SetState(1);
    }
}

徘徊状態に入るとスピードをでデフォルトに戻して徘徊の目的地をランダムに決めます。Updateメソッドでは、目的地に付いた時に次の目的地を設定する処理を書いています。そして、ボールがぶつかると、立ち止まっている状態へ遷移します。

目的地をランダムに設定

マップには目的地を表すオブジェクトがいくつか配置しており、それらが一つのゲームオブジェクトの子にまとめられています。

敵のスクリプトには、このルートのオブジェクトがアタッチされています。

目的地をセットするメソッドでは、このオブジェクトの子オブジェクトをランダムで選びます。

// ナビメッシュエージェントの目的地を変える
void SetDestination(Vector3 pos)
{
    agent.destination = pos;
}

void SetDestination()
{
    // ランダムで次の目的地を設定
    agent.destination = points.GetChild(Random.Range(0, points.childCount)).position;
}

別の状態クラスを定義

立ち止まっている状態クラスでは、秒読みをしたいので、秒数を表すfloat型のフィールドを作りました。

// 立ち止まっている状態
class Stopping : State
{
    float sec; // 秒数

    public override void Update(Agent1 a)
    {
        // 秒読み
        sec += Time.deltaTime;

        // 3秒立つと次の状態に遷移
        if (sec >= 3f)
        {
            // 次の状態へ遷移
            SetNextState(a);
        }
    }

    public override void EnterState(Agent1 a)
    {
        // 秒数を初期化
        sec = 0f;

        // 立ち止まる
        a.SetSpeed(0f);

        // マテリアルの色を変える
        a.SetColor(Color.yellow);

    }

    // 次の状態へ遷移
    protected virtual void SetNextState(Agent1 a)
    {
        // 追跡状態へ遷移
        a.SetState(2);
    }
}

この状態に入ったときにこれを0に初期化します。同時に、ナビメッシュエージェントのスピードやマテリアルの色を変えるメソッドも呼んでいます。

public class Agent1 : MonoBehaviour
{
    // ...

    // ナビメッシュエージェントのスピードを変える
    void SetSpeed(float speed)
    {
        agent.speed = speed;
    }

    void SetSpeed()
    {
        agent.speed = defaultSpeed;
    }

    // マテリアルの色を変える
    void SetColor(Color color)
    {
        meshRenderer.material.SetColor("_BaseColor", color);
    }

    // ...

}

Updateでは秒数にTime.deltaTimeを加算代入することで秒読みし、3以上になると、次の状態に遷移するメソッドを呼びます。

次の状態に遷移するメソッドは、virtualキーワードを付けて、この立ち止まっている状態クラスに定義しました。

追跡中にボールを当てられた状態も遷移先以外は同じ処理をするので、追跡中に当てられた状態は、上の立ち止まっている状態を継承して作り、次の状態に遷移するメソッドだけをオーバーライドしています。

// 追跡中にボールを当てられた状態
class Hitted : Stopping
{
    protected override void SetNextState(Agent1 a)
    {
        // 徘徊状態へ遷移
        a.SetState(0);
    }
}

これで、簡単に敵キャラクターの状態管理ができました。敵はこの4つの状態を繰り返します。

立ち止まっている状態では、衝突時のメソッドをオーバーライドしていないので、ボールが当たっても何もしません。

コメントを残す

メールアドレスが公開されることはありません。