ぐっちーの駄弁り部屋

個人的に制作しているものの進捗や日常について不定期投稿

Stateパターンで状態管理はじめました

どうも皆さんこんにちはぐっちーです。
ブログでは昨年末ぶりですね、あけましておめでとうございますww(なお2月)
新年明けてからは卒論の最終段階に入ったり、研究室全体で出席した学会用の論文の校正をしたり新生活に向けての手続きやらでいつの間にか2月になっていました。
2月になったら落ち着くわけもなく卒論発表が半ばにあったり引っ越しがあったりするので結局忙しいのです(´・ω・`)。
しかしまあ長いようで割とあっという間だった大学生活最後の2ヶ月を謳歌していきたいと思っていますww
さて、今日の本題にはいる前にお知らせが一つあります。当ブログのヘッダー画像を更新いたしました。
私のアカウント全般のサムネにもなっている私のVRChatアバターですがこの度絵師の方に依頼してちびキャラにしていただきましたのでそちらを使用したヘッダー画像となっています。私の要望通り完璧なイラストを描いていただいたこずえさんには感謝しかありません。この場を使ってお礼申し上げます。
イラストを描いていただいたこずえさんのツイッターtwitter.com

それでは本編

ではでは本編に入っていきたいと思います。今日はタイトルにもある通りStateパターンについて話していきたいと思います。今回の記事はどちらかというと私が学んだ内容を書き示しておこうみたいな記事にする予定です。現在制作中の自作ゲーでStateパターンを用いて状態管理をするようになったのでどんなふうにしたのかをある程度簡略化して紹介していきたいと思います。

状態管理

Stateパターンの話に入る前に状態の管理について考えてみましょう。ゲームを例に取ってみると、ゲームの状態はざっくり分けて「スタート画面」「ゲーム本編」「リザルト画面」に分かれると思います。(ゲームの種類によってはこの限りではありませんがわかりやすくするためにこの記事ではこの3つで話します。)そしてゲームを作っていく中で各状態でしたいことは変わってくると思います。当然現在どの状態であるかを判断して適切な動作をするような実装が求められます。かんたんな実装ではifを用いた分岐でしょうか。

if文で状態管理(クリックで展開されます)

string currentState;

if(currentState == "Start"){
------スタート画面の処理-----
} else if(currentState == "Play"){
------ゲーム本編の処理-----
} else if(currentState == "Result"){
------リザルト画面の処理-----
}


私自身もこのようにこれまでこのように書いていました。しかしこの方法では状態が増えるごとに条件分岐全体を見直す必要が出てきます。今でこそ短いコードですがコメント部分を実際に実装するとなると結構なコード量になると思います。そのためあまりこの方法は好ましくありません。そこでStateパターンというものを導入します。

Stateパターン

StateパターンとはGoFデザインパターンの一つで状態をそれぞれクラスとして、それらを切り替えることでオブジェクトの状態を表現する方法のことです。
Stateパターンは各状態を分けて実装するのでスタート画面での振る舞いを実装しているときには本編の振る舞いを木にする必要はありません。よく「分割して統治せよ」と言われるようになるべく細分化して置くことであらゆる点で有効に働きます。
以下にStateパターンのクラス図を示します。

f:id:gucchi512:20210209225416p:plain
クラス図
属性や操作についてはこの後にコードを載せるのでそれを見ていただければいいですが、かんたんに説明をすると状態を管理するStateManagerクラスがStateインターフェースを実装した各状態を表すクラスに対して操作を行うのですがStateManager側の処理はStateで定義されている関数を呼ぶに過ぎないのでそれぞれの状態でどのような振る舞いをするかは状態を表すクラスに一任されているという点がポイントです。
続いてコードの方に移ります。
State (状態を示すインターフェース)

public interface State{
    public void OnUpdate();       //毎フレーム呼ばれる関数
    public void OnStateEnter();  //状態に入ったときに呼ばれる関数
    public void OnStateExit();    //状態を出るときに呼ばれる関数
} 

Start (スタート画面)

public class Start: State{
    private Start instance = new Start();

    public Start GetInstance(){
          return instance;
    }

    public void OnUpdate(){
         -----スタート画面の処理-----
    }

    public void OnStateEnter(){
         -----スタート画面開始時の処理-----
    }

    public void OnStateExit(){
         -----スタート画面終了時の処理-----
    }
   
    public string GetStateName(){
         return "Start"
    }
} 

Play (ゲーム本編)

public class Play: State{
    private Play instance = new Play();

    public Play GetInstance(){
          return instance;
    }

    public void OnUpdate(){
         -----ゲーム本編の処理-----
    }

    public void OnStateEnter(){
         -----ゲーム本編開始時の処理-----
    }

    public void OnStateExit(){
         -----ゲーム本編終了時の処理-----
    }

    public string GetStateName(){
         return "Play"
    }
} 

Result (リザルト画面)

public class Result: State{
    private Result instance = new Result();

    public Result GetInstance(){
          return instance;
    }

    public void OnUpdate(){
         -----リザルト画面の処理-----
    }

    public void OnStateEnter(){
         -----リザルト画面開始時の処理-----
    }
Z
    public void OnStateExit(){
         -----リザルト画面終了時の処理-----
    }

    public string GetStateName(){
         return "Result"
    }
} 

StateManager (状態を管理するクラス)

public class StateManager{
    private State currentState;
    private State nextState
    private Start startState;
    private Play playState;
    private Result resultState;

    public State CurrentState => currentState;
    
    private void Start(){
        startState = Start.GetInstance();
        playState = Play.GetInstance();
        resultState = Result.GetInstance();
        currentState = startState;
    }
    private void Update(){
        if(nextState!=null){
               currentState.OnStateExit();
               currentState = nextState;
               nextState = null;
               currentState.OnStateEnter();
        }
        currentState.OnUpdate();
    }
    
    public void RequestNextState(State next){
        nextState = next;
    }
} 


こんな感じで状態管理側からは各々がどのような振る舞いをするかはわかりませんがどの状態であっても同じ処理をすることができます。一応今回状態へ入るときと出るときに呼ばれる関数も用意しましたがこれが正解というわけでは無いのであしからず。なくても大丈夫だと思います。
今回のコードではGameManagerなどのクライアントからCurrentStateプロパティにアクセスして現在の状態を取得したりRequestNextState関数を呼んで状態の遷移を行えるようになっています。
状態ごとの振る舞いについてもクラスで分けているので変更を加えるときもわかりやすいですね。それでいて他に影響を及ぼさないのがいいです。
また、各状態は2つ以上存在することはありませんのでSingletonパターンを使ってインスタンスが一つであることを保証しておきます。(Singletonパターンについては他記事様を参考にしてください)

まとめ

ではではこの記事のまとめです。

  • Stateパターンはオブジェクトの状態をクラスを用いて表現する方法である。
  • Stateパターンを採用することで状態の各状態がクラスによって分割されるので保守性が向上する。
  • インターフェースを使用したことで管理側は共通の処理で済むようになっている。

ゲーム開発では様々な場面で状態遷移が登場します。ゲームの規模が大きくなればそれらも非常に複雑化してくるので可能な限りわかりやすく管理したいですよね。今回これを実装してみて状態管理周りがif文を用いていたときに比べてだいぶスッキリしました。先人たちは素晴らしい方法を編み出したものだと思いますねwww
それでは今回はこのへんで( ー`дー´)キリッ

参考記事

qiita.com