【Unity】状態をオブジェクトにして進行管理する

投稿者: | 2020-12-25

Stateデザインパターンのように、各状態をオブジェクトにして進行管理してみました。

まず状態クラスの基底クラスを作りました。

abstract public class State
{
    public virtual void Update() { } // フレームごとの処理

    public State() // コンストラクタ
    {
        ProgressManager3.GetInstance().holdItem += HoldItem;
        ProgressManager3.GetInstance().releaseItem += ReleaseItem;
        ProgressManager3.GetInstance().getItem += GetItem;
        ProgressManager3.GetInstance().destroyItem += DestroyItem;
        ProgressManager3.GetInstance().playerJump += PlayerJump;
        ProgressManager3.GetInstance().doorEvent += DoorEvent;
    }

    abstract protected State GetNextState();

    public void ChangeState () // 状態を変える
    {
        RemoveEvent();
        ProgressManager3.GetInstance().SetState(GetNextState());
    }

    void RemoveEvent()
    {
        ProgressManager3.GetInstance().holdItem -= HoldItem;
        ProgressManager3.GetInstance().releaseItem -= ReleaseItem;
        ProgressManager3.GetInstance().getItem -= GetItem;
        ProgressManager3.GetInstance().destroyItem -= DestroyItem;
        ProgressManager3.GetInstance().playerJump -= PlayerJump;
        ProgressManager3.GetInstance().doorEvent -= DoorEvent;
    }

    protected virtual void HoldItem(GameObject sender, ItemEventArgs e) { }
    protected virtual void ReleaseItem(GameObject sender, ItemEventArgs e) { }
    protected virtual void GetItem(GameObject sender, ItemEventArgs e) { }
    protected virtual void DestroyItem(GameObject sender, ItemEventArgs e) { }
    protected virtual void PlayerJump(GameObject sender, EventArgs e) { }
    protected virtual void DoorEvent(GameObject sender, DoorEventArgs e) { }
}

状態クラスがその状態での処理や状態を変えるときの処理、遷移先などを持っています。

進行管理クラスのUpdateで、その状態でのフレームごとの処理を呼びます。基底クラスではvirtual修飾子を付けて、何もしないメソッドを定義しています。その状態で何か処理をしたいときは、これを継承したクラスでオーバーライドします。

コンストラクタでは、アイテムを取得したりドアを開けた時などに呼ぶイベントの中にメソッドを加算代入しています。これらのメソッドも、基底クラスでは何もしないメソッドを定義しているので、何かしたいときは派生クラスに書きます。

それらのメソッドの中で次の状態へ遷移するメソッドを呼びます。そのときにイベントからメソッドを減算代入して、次の状態を進行管理クラスにセットします。この時に次の状態をインスタンス化するので、コンストラクタが呼ばれて、次の状態で定義しているメソッドがイベントに加算代入されます。

決まった処理は基底クラスにすべて書いているので、派生クラスでは次の状態オブジェクトを返すメソッドと、イベントやUpdateで何かしたいときにオーバーライドするだけで済みます。

// 派生クラス
class State1 : State
{
    protected override State GetNextState()
    {
        return new State2();
    }

    protected override void HoldItem(GameObject sender, ItemEventArgs e)
    {
        if (e.itemName == "Cube1")
        {
            TextManager.GetInstance().SetText("緑の箱を3個集める", 0);
            ChangeState();
        }
    }
}

class State2 : State {

    int count;

    protected override State GetNextState()
    {
        return new State3();
    }

    protected override void GetItem(GameObject sender, ItemEventArgs e)
    {
        if (e.itemName == "GreenCube")
        {
            count++;

            if (count >= 3)
            {
                TextManager.GetInstance().SetText("ドアを開ける", 0);
                ChangeState();
            }
        }
    }
}

class State3 : State
{

    protected override State GetNextState()
    {
        return new State4();
    }

    protected override void DoorEvent(GameObject sender, DoorEventArgs e)
    {
        if(sender.name == "Door" && e.open)
        {
            TextManager.GetInstance().SetText("バットを投げる", 0);
            ChangeState();

        }
    }
}

class State4 : State
{

    protected override State GetNextState()
    {
        return new State5();
    }

    protected override void ReleaseItem(GameObject sender, ItemEventArgs e)
    {
        if (e.itemName == "Bat")
        {
            TextManager.GetInstance().SetText("終了", 0);

            ChangeState();
        }
    }
}

class State5 : State
{
    float sec;

    protected override State GetNextState()
    {
        return new State1();
    }

    public override void Update()
    {
        sec += Time.deltaTime;

        if(sec >= 3)
        {
            SceneManager.LoadScene(SceneManager.GetActiveScene().name);
        }
    }
}

今まで状態をメソッドで定義して、デリゲートの中身を入れ替えてゲームを進行させていましたが、状態をクラスにすると状態ごとに違うメンバを作れます。

例えば、上のState2では取得したCubeを数えるためにこのクラスだけに整数型のフィールドを宣言しています。State5では秒読みのためのfloat型を使っています。これらは今までは進行管理クラスのメンバにしていました。

進行管理クラスではイベントを宣言して、Start()で初めの状態のインスタンスを作り、Updateで状態オブジェクトのフレーム毎の処理を呼ぶだけです。

using UnityEngine;
using System;
using UnityEngine.SceneManagement;

// ...

public class ProgressManager3 : MonoBehaviour
{
    // シーン上のインスタンスを返す
    static ProgressManager3 instance;
    public static ProgressManager3 GetInstance()
    {
        return instance;
    }

    State state; // 今の状態

    // 状態をセット
    public void SetState(State state)
    {
        this.state = state;
    }

    // イベント
    public delegate void EventHandler(GameObject sender, ItemEventArgs e);
    public delegate void PlayerJumpEventHandler(GameObject sender, EventArgs e);
    
    public event EventHandler holdItem;
    public event EventHandler releaseItem;
    public event EventHandler getItem;
    public event EventHandler destroyItem;
    public event PlayerJumpEventHandler playerJump;

    public delegate void DoorEventHandler(GameObject sender, DoorEventArgs e);
    public event DoorEventHandler doorEvent;

    // イベント変数を呼ぶためのメソッド
    public void HoldItem(GameObject sender , ItemEventArgs e)
    {
        if (holdItem != null) holdItem(sender,e);
    }

    public void GetItem(GameObject sender, ItemEventArgs e)
    {
        if (getItem != null) getItem(sender, e);
    }

    public void ReleaseItem(GameObject sender, ItemEventArgs e)
    {
        if (releaseItem != null) releaseItem(sender, e);
    }

    public void DestroyItem(GameObject sender, ItemEventArgs e)
    {
        if(destroyItem != null) destroyItem(sender, e);
    }

    public void PlayerJump(GameObject sender, EventArgs e)
    {
        if (playerJump != null) playerJump(sender, e);
    }

    public void DoorEvent(GameObject sender, DoorEventArgs e)
    {
        if (doorEvent != null) doorEvent(sender, e);
    }

    private void Awake()
    {
        instance = this;
    }

    // Start is called before the first frame update
    void Start()
    {
        TextManager.GetInstance().SetText("白い箱を持つ", 0);

        state = new State1();
    }

    // Update is called once per frame
    void Update()
    {
        state.Update();
    }

}

これで状態オブジェクトを使って簡単に進行管理ができました。

コメントを残す

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