適用して学ぶデザインパターン ~Stateパターン~
はじめに
みなさんこんにちは。期末試験期間中に3日でゲームを作ったさんたろです。
最低限のデザインパターンの知見を得るべきだと感じ、『Game Programming Patterns』という本を読みました。
実際に私が書いていたプログラムにデザインパターンを適用し、どのような効果が期待できるか試してみようと思いました。
今回はいくつもあるパターンの中から、Stateパターンについて実装していきたいと思います。
(次回はたぶんありません)
使用言語はC#です(かなり抽象的に書いているので、C#を知らなくても大丈夫だと思います)
Stateパターン
本書第7章で紹介されているStateというパターン。
"オブジェクト内部の状態が変化したときにオブジェクトの振る舞いが変わるようにする。オブジェクトはクラスが変わったように見える。"という紹介から始まります。
・状態を表す
・状態の変化を表す
・振る舞いに関するデザインパターン
今回適用するプログラム
私が制作した『SpaceJumpGame』という3Dアクションゲームは、キャラクターの状態遷移が多少複雑でした。
そこで、列挙型によって現在の状態を記憶し、ifやswitchによって、状態に応じた処理分けをしていました。
//Playerクラス内 //プレイヤーの入力に応じたキャラクター処理 private void UpdateInput(){ switch(this.currentState){ case E_State.Standing: if(input == jump) Jump(); else if(input == move) Run(); break; case E_State.Running: if(input == jump) Jump(); break; case E_State.StickingWall: if(input == jump) WallKick(); break; case E_State.JumpToTop: if(input == hipDrop) HipDrop(); break; case E_State.Falling: if(input == hipDrop) HipDrop(); break; } }
これは本書における2つ目の提案、有限状態機械、列挙と分岐による実装です。
この実装は、常に一つの状態しかとることができない、という前提のもと成り立っています。
本書でも、場合によってはこれで問題ないと書かれていますが、この実装にどのような問題があるか考えていきます。
(ちなみに、実際のコードはこちら↓ 156行目あたりから)
この実装の問題点
プレイヤーがジャンプしているときに、ジャンプ入力時間に応じてジャンプ力を変化させることにしました。
_countJumpTimeというフィールドを用意し、ジャンプ開始時から数秒、ジャンプ入力をしているか判断するようにします。
入力を確認しているかのフラグとして、_checkPressJumpButtonも用意します。
private void UpdateInput(){ switch(this.currentState){ case E_State.Standing: if(input == jump) Jump(); else if(input == move) Run(); break; case E_State.Running: if(input == jump) Jump(); break; case E_State.StickingWall: if(input == jump) WallKick(); break; case E_State.Jumping: if(input == hipDrop) HipDrop(); /****************************/ else if(_checkPressJumpButton){ if(input == jump){ _countJumpTime += Time.deltaTime; if(_countJumpTime > 0.03f){ _velocity.y += 30f; _checkPressJumpButton = false; } }else{ _checkPressJumpButton = false; } } /****************************/ break; case E_State.Falling: if(input == hipDrop) HipDrop(); break; } } private void Jump(){ //一番小さいジャンプの処理 ._velocity.y += 20f; //初期化 /****************************/ _countJumpTime = 0f; _checkPressJumpButton = true; /****************************/ }
さて、ジャンプ量調整を追加するために、2つのメソッドを修正し、2つのフィールドをこのクラスに追加する必要がありました。
ジャンプし始めのときしか、有効でないにもかかわらずです。
改善案: 各状態のクラスを定義
「状態」のインターフェースを用意し、そこから各状態のクラスを定義します。
interface PlayerState{ public void UpdateInput(Player player); } class JumpingState : PlayerState { private float _countJumpTime; private bool _checkPressJumpButton; public void UpdateInput(Player player){ if(input == hipDrop) HipDrop(); else if(_checkPressJumpButton){ if(input == jump){ _countJumpTime += Time.deltaTime; if(_countJumpTime > 0.03f){ player._velocity.y += 30f; _checkPressJumpButton = false; } }else{ _checkPressJumpButton = false; } } } }
Playerクラスからの呼び出しは以下のようになります。
class Player { private PlayerState _currentState; private JumpingState _jumpingState; private void UpdateInput(){ _currentState.UpdateInput(this); } }
ここで問題が発生します。_checkPressJumpButtonのフラグをtrueにするタイミングが、Player.Jump()でしたが、JumpingStateからはわかりません。
もちろんJumpingStateのフィールドにPlayerからアクセスをかけることは容易です。
しかし、より良い設計にするために、今回は本書の『入口処理と出口処理』に着目します。
改善案: 入口処理と出口処理
『入口処理』はその状態になったときに呼ばれる処理、
『出口処理』はその状態から別の状態に移るときに呼ばれる処理です。
この二つの処理をStateごとに実装することで、状態に応じた処理だけでなく、状態遷移の処理も委任できます。
interface PlayerState{ public void UpdateInput(Player player); /****************************/ public void Enter(Player player); //この状態になったとき public void Exit(Player player); //別の状態に移るとき /****************************/ } class JumpingState : PlayerState { private float _countJumpTime; private bool _checkPressJumpButton; public void UpdateInput(Player player){ if(input == hipDrop) HipDrop(); else if(_checkPressJumpButton){ if(input == jump){ _countJumpTime += Time.deltaTime; if(_countJumpTime > 0.03f){ player._velocity.y += 30f; _checkPressJumpButton = false; } }else{ _checkPressJumpButton = false; } } } /****************************/ public void Enter(Player player){ _checkPressJumpButton = true; } public void Exit(Player player){ _checkPressJumpButton = false; } /****************************/ }
Playerクラスは以下のように
class Player { private PlayerState _currentState; private JumpingState _jumpingState; private void UpdateInput(){ _currentState.UpdateInput(this); } /****************************/ public void MoveState(PlayerState nextState){ _currentState.Exit(this); _currentState = nextState; _currentState.Enter(this); } /****************************/ }
無事、状態に応じた処理、状態遷移の処理の委任ができました。
個人的には、驚くほど各状態の処理及び遷移処理が明確になったと思います。
おわりに
今回はStateと呼ばれるデザインパターンに着目してみましたが、他の紹介されたパターンもゲーム制作をする上で知っていて損はないものでした。
また、どのパターンも一長一短であり、ひとつのパターンで見ても、詳細部分や使用言語などによって多少変わってきます。
その点本書は、それぞれのパターンを適用したい場面を毎回提示し、なぜそのパターンを適用したいのか、という思想的な部分をしっかりと記述しています。
また、パターン適用による利点・欠点も書かれているので、入門書として悪くないと感じました。
そして、本書にも何度も出てきたことですが、設計に完璧な正解なんてないということも、よくわかりました。
あくまで、どこで楽をして、どこで苦労するのかというもので、全て楽にできるものではないですし、
プロジェクトの規模や仕様の変更によって、適する設計は変化していきます。
様々な設計思想を知り、できるだけマシな設計をしていきたいです。
...それとパズルみたいで面白い