シンプルなコードで状態を管理(ステートマシン)

2022年2月10日

状態を遷移するケースを管理するには、ステートマシンを使うのがいいですが、初学の書籍などには記載されていないことが多いです。if文やswitchを駆使しても、次第に複雑になり混乱してしまいます。

ステートマシンとは

条件が揃えば、今の状態から次の状態に移るイメージですね
Unityの経験があれば、Animatorコントローラ、VisualScriptingのステートなどを思い浮かべてもらえばいいと思います

どのような場面で使うの?

  • 脱出ゲームのように、「条件が揃ったら次のステージに進める」方法の実現
  • 「一定のポイントが貯まったら、何かが発動する」方法の実現

など、フェーズの管理が必要な場面で使えます

簡易ステートマシン

実現する方法はさまざまありますが、今回は、ステージの遷移を管理するのにコルーチンを使う方法を紹介します
このサンプルでは、常に変数をチェックしており、条件が揃うまで待ちます
条件が揃うと次のフェーズに進むことができます

状態を保持しているコルーチン

「yield return null;で、1フレーム待つ」を無限ループとしています
keyが3になると、break;文でwhileを抜けるのでこのルーチンが終了します

IEnumerator Phase2()
{
    while (true)
    {
        yield return null;

        if (key == 3)
        {
            break;
        }
    }
    Debug.Log("鍵が全て手に入った");
}

呼び出す方法

次のコードで呼び出します。Phese2コルーチンが終了しない限り次の行に制御が移りません

yield return StartCoroutine(Phase2());
// 次の行へはPhase2が終了しないと移行しない
Debug.Log("次の行");

サンプルコード

条件を満たせたら、次のフェーズに移るコードのサンプルになります

using System.Collections;
using UnityEngine;

public class StateSample : MonoBehaviour
{
    int key = 0;
    int door = 0;
    int puzzle = 0;

    void Start()
    {
        StartCoroutine(Phase1());
    }

    IEnumerator Phase1()
    {
        yield return StartCoroutine(Phase2());
        yield return StartCoroutine(Phase3());
        yield return StartCoroutine(Phase4());

        Debug.Log("表に出られた");

    }

    IEnumerator Phase2()
    {
        while (true)
        {
            yield return null;

            if (key == 3)
            {
                break;
            }
        }
        Debug.Log("鍵が全て手に入った");
    }

    IEnumerator Phase3()
    {
        // 必要なら
        door = 0;

        while (true)
        {
            yield return null;

            if (door == 1)
            {
                break;
            }
        }
        Debug.Log("ドアが開いた");
    }

    IEnumerator Phase4()
    {
        // 必要なら
        puzzle = 0;

        while (true)
        {
            yield return null;

            if (puzzle == 1)
            {
                break;
            }
        }
        Debug.Log("パズルを解いた");

    }
    void Update()
    {
        // 1キーが押された
        if (Input.GetKeyDown(KeyCode.Alpha1))
        {
            key++;
        }

        if (Input.GetKeyDown(KeyCode.Alpha2))
        {
            door++;
        }

        if (Input.GetKeyDown(KeyCode.Alpha3))
        {
            puzzle++;
        }
    }
}

デリゲートを使ってコードをまとめる

ギミックが増えていくとコードが冗長になるので、コルーチンをまとめましょう

using System;
using System.Collections;
using UnityEngine;

public class StateSample : MonoBehaviour
{
    // 条件の保管は、Scriptable Object を活用するのがいいです
    int key = 0;
    int door = 0;
    int puzzle = 0;

    IEnumerator Start()
    {
        // keyが3の場合、AllKeyGetメソッドを実行
        yield return StartCoroutine(GimicCheck(() => key == 3, AllKeyGet));
        yield return StartCoroutine(GimicCheck(() => door == 1, DoorOpen));
        yield return StartCoroutine(GimicCheck(() => puzzle == 1, PuzzleClear));

        Debug.Log("表に出られた");
    }

    void AllKeyGet()
    {
        Debug.Log("鍵が全て手に入った");
    }

    void PuzzleClear()
    {
        Debug.Log("パズルを解いた");
    }

    void DoorOpen()
    {
        Debug.Log("ドアが開いた");
    }

    IEnumerator GimicCheck(Func<bool> check, Action allKeyGet)
    {
        while (true)
        {
            yield return null;

            if (check())
            {
                break;
            }
        }
        allKeyGet.Invoke();
    }

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Alpha1))
        {
            key++;
        }

        if (Input.GetKeyDown(KeyCode.Alpha2))
        {
            door++;
        }

        if (Input.GetKeyDown(KeyCode.Alpha3))
        {
            puzzle++;
        }
    }
}

実用的な仕組み

条件が揃った時に実行されるメソッドをイベントにしてみましょう

シーン

GameObjectオブジェクト

EventActionオブジェクト

コード

StateSample.cs

using System;
using System.Collections;
using UnityEngine;
using UnityEngine.Events;

public class StateSample : MonoBehaviour
{
    public UnityEvent AllKeyGetEvent;
    public UnityEvent DoorOpenEvent;
    public UnityEvent PuzzleClearEvent;

    // 条件の保管は、Scriptable Object を活用するのがいいです
    int key = 0;
    int door = 0;
    int puzzle = 0;

    IEnumerator Start()
    {
        // keyが3の場合、AllKeyGetEventを実行
        yield return StartCoroutine(GimicCheck(() => key == 3, AllKeyGetEvent));
        yield return StartCoroutine(GimicCheck(() => door == 1, DoorOpenEvent));
        yield return StartCoroutine(GimicCheck(() => puzzle == 1, PuzzleClearEvent));

        Debug.Log("表に出られた");
    }

    IEnumerator GimicCheck(Func<bool> check, UnityEvent gimicEvent)
    {
        while (true)
        {
            yield return null;

            if (check())
            {
                break;
            }
        }
        gimicEvent.Invoke();
    }

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Alpha1))
        {
            key++;
        }

        if (Input.GetKeyDown(KeyCode.Alpha2))
        {
            door++;
        }

        if (Input.GetKeyDown(KeyCode.Alpha3))
        {
            puzzle++;
        }
    }
}

StateSample.cs

using UnityEngine;

public class GimicEvent : MonoBehaviour
{
    public void AllKeyGet()
    {
        Debug.Log("鍵が全て手に入った");
    }

    public void PuzzleClear()
    {
        Debug.Log("パズルを解いた");
    }

    public void DoorOpen()
    {
        Debug.Log("ドアが開いた");
    }
}

Unity

Posted by hidepon