Unity Stateパターンで状態管理の実装

raharu(仮名)(プログラマー)
これがダイバージェンス1%の先の世界か。。。

例えばキャラクターの状態管理で
・アイドル
・走る
・攻撃
・ジャンプ

こんな状態を管理する際にどういう処理にしているでしょうか…?
一番簡単な方法で考えれば各状態のフラグを用意して状態遷移させる事や、
状態を管理するビットを用意して内包している状態を判定させる方法などがありそうですが、
どちらもupdate内が段々複雑になっていき、状態が多くなって行くほどにバグの温床になりそうな予感がぷんぷんします。

UnityではPlayMakerやuStateなどのAssetも用意されていますが、
もう少しフランクに状態を管理したいと思い作ってみる事にしました。

考え方はStateパターンの通りなのですが、
処理自体はMonoBehaviourを継承しているクラスが行い、
状態管理のみを行うクラスを作るという方法です。

まぁ百聞は一見に如かずという事で早速

オブジェクトの作成

まずは適当なオブジェクトを用意します。
今回分かりやすいようにテキストにしましたが、実際はキャラクターでも何でもかまいません。

f:id:raharu0425:20150612121606p:plain f:id:raharu0425:20150612121628p:plain

スクリプトの作成

f:id:raharu0425:20150612121645p:plain

TextController.csをCanvas/Textオブジェクトにアタッチします。

f:id:raharu0425:20150612121823p:plain

今回の処理は
初期状態から1秒後にStateAに遷移
StateAから1秒後にStateBに遷移
StateBから1秒後に初期状態に遷移

という事をやります

TextState.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
using UnityEngine;
using System.Collections;

//キューブオブジェクトのステート
namespace  TextState{

    //ステートの実行を管理するクラス
    public class StateProcessor
    {
        //ステート本体
        private TextState _State;
        public TextState State
        {
            set{ _State = value;}
            get{ return _State;}
        }

        // 実行ブリッジ
        public void Execute(){
            State.Execute();
        }

    }

    //ステートのクラス
    public abstract class TextState
    {
        //デリゲート
        public delegate void executeState();
        public executeState execDelegate;

        //実行処理
        public virtual void Execute(){
            if(execDelegate != null){
                execDelegate();
            }
        }

        //ステート名を取得するメソッド
        public abstract string getStateName();
    }

    // 以下状態クラス

    //  DefaultPosition
    public class TextStateDefault : TextState
    {
        public override string getStateName() {
            return "State:Default";
        }
    }

    //  StateA
    public class TextStateA : TextState
    {
        public override string getStateName() {
            return "State:A";
        }
    }

    //  StateB
    public class TextStateB : TextState
    {
        public override string getStateName() {
            return "State:B";
        }

        public override void Execute(){
            Debug.Log ("特別な処理がある場合は子が処理してもよい");
            if(execDelegate != null){
                execDelegate();
            }
        }
    }
}

TextController.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
using UnityEngine;
using System.Collections;
using TextState;
using UniRx;
using UnityEngine.UI;
using System;

public class TextController : MonoBehaviour {

    //変更前のステート名
    private string _beforeStateName;

    //ステート
    public StateProcessor StateProcessor = new StateProcessor();           //プロセッサー
    public TextStateDefault StateDefault = new TextStateDefault();
    public TextStateA StateA = new TextStateA();
    public TextStateB StateB = new TextStateB();

    // Use this for initialization
    void Start () {

        //DefaultState
        StateProcessor.State = StateDefault;
        StateDefault.execDelegate = Default;
        StateA.execDelegate = A;
        StateB.execDelegate = B;

    }

    // Update is called once per frame
    void Update () {

        //ステートの値が変更されたら実行処理を行う
        if(StateProcessor.State == null){
            return;
        }

        if (StateProcessor.State.getStateName () != _beforeStateName) {
            Debug.Log (" Now State:" + StateProcessor.State.getStateName ());
            _beforeStateName = StateProcessor.State.getStateName ();
            StateProcessor.Execute ();
        }

    }

    public void Default()
    {
        gameObject.transform.GetComponent<Text> ().text = "初期状態です";
        //1秒後にStateAに状態遷移
        Observable
            .Timer (TimeSpan.FromSeconds (1))
            .Subscribe (x => StateProcessor.State = StateA);
    }

    public void A()
    {
        gameObject.transform.GetComponent<Text> ().text = "StateAです";
        //1秒後にStateBに状態遷移
        Observable
            .Timer (TimeSpan.FromSeconds (1))
            .Subscribe (x => StateProcessor.State = StateB);
    }

    public void B()
    {
        gameObject.transform.GetComponent<Text> ().text = "StateBです";
        //1秒後にDefaultに状態遷移
        Observable
            .Timer (TimeSpan.FromSeconds (1))
            .Subscribe (x => StateProcessor.State = StateDefault);

    }
}

f:id:raharu0425:20150612125121g:plain

f:id:raharu0425:20150612124635p:plain

こんな感じでしょうか? TextStateクラスは完全に独立していますので、新しいStateを追加しても影響範囲はStateクラス内だけで留まります。
Controllerクラスは自分で行っているオブジェクトへの処理をStateクラスに委譲しているだけなので、必要なStateを必要な時に呼び出す事で状態遷移が可能です。
例えばこのTextControllerを基底クラスにして、子のControllerクラスを作成してもよいでしょう。

ただ、Processorと基底クラスをnamespaceでまとめましたが、
これ分けておいた方が後々楽になると思います。

私はこんな感じでキャラクターの状態管理や、
ログイン処理の状態管理などを行っています。

もっといい方法があるよ!とかご意見マサカリ歓迎しておりますm( )m