ステート・プログラミング・パターンを使用して、モジュール化された柔軟なコードベースを開発する。
Unityプロジェクトに一般的なゲームプログラミングデザインパターンを実装することで、クリーンで整理された読みやすいコードベースを効率的に構築し、維持することができます。デザインパターンは、リファクタリングやテストに費やす時間を削減するだけでなく、オンボーディングや開発プロセスをスピードアップし、ゲーム、開発チーム、そしてビジネスを成長させるための強固な基盤に貢献する。
デザイン・パターンは、コードにコピー&ペーストできる完成されたソリューションとしてではなく、正しく使用することで、より大規模でスケーラブルなアプリケーションを構築するのに役立つ追加ツールとして考えるのだ。
このページでは、Stateパターンと、それがコードベースの管理をどのように容易にするかを見ていく。
ここでの内容は無料電子書籍に基づいています、 ゲームプログラミングパターンでコードをレベルアップよく知られたデザインパターンを説明し、Unityプロジェクトでそれらを使用するための実践的な例を共有しています。
Unityゲームプログラミングデザインパターンシリーズの他の記事は、Unityベストプラクティスハブでご覧いただけます:
プレイアブルキャラクターを構築することを想像してみてほしい。ある瞬間、キャラクターは地面に立っているかもしれない。コントローラーを動かすと、走ったり歩いたりするように見える。ジャンプボタンを押すと、キャラクターは空中に飛び上がる。数フレーム後、着地し、再びアイドリング状態に戻る。
コンピュータゲームのインタラクティブ性は、実行時に変化する多くのシステムの追跡と管理を必要とする。キャラクターのさまざまな状態を表す図を描くと、上の図のようになるかもしれない:
フローチャートに似ているが、いくつかの違いがある:
- アイドリング/スタンディング、ウォーキング、ランニング、ジャンプなど)。
- 各状態は、実行時の条件に基づいて、他の1つの状態への遷移を引き起こすことができる。
- 遷移が起こると、出力状態が新しいアクティブ状態になる。
この図は有限状態機械 (FSM)と呼ばれるものを示している。ゲーム開発において、FSMの典型的な使用例の1つは、小道具やプレイアブルキャラクターのような「ゲームアクター」の内部状態を追跡することです。ゲーム開発におけるFSMのユースケースは数多くあり、Unityでプロジェクトを開発した経験があれば、Unityのアニメーションステートマシンの文脈でFSMをすでに採用している可能性が高い。
FSMは状態のリストによって定義される。初期状態を持ち、各遷移には条件がある。FSMは、外部からの入力に応答して、ある状態から別の状態に変化する可能性がある。
一方、Stateデザイン・パターンでは、状態を表すインターフェースと、各状態に対してこのインターフェースを実装するクラスを定義する。ステートに基づいて動作を変更する必要があるコンテキスト(クラス)は、現在のステート・オブジェクトへの参照を保持する。コンテキストの内部状態が変わると、ステートオブジェクトへの参照を更新して別のオブジェクトを指すようにするだけで、コンテキストの振る舞いが変わる。
StateパターンはFSMに似ているが、異なるステートを管理し、その間を遷移させることができる。しかし、FSMは通常switch文を使って実装されるのに対し、Stateデザイン・パターンは、状態を表すインターフェースと、各状態に対してこのインターフェースを実装するクラスを定義する。
ステートパターンはゲーム開発で広く使われており、メインメニュー、ゲームプレイ状態、ゲームオーバー状態など、ゲームのさまざまな状態を管理する効果的な方法となる。
次のセクションの例で、ステート・パターンを実際に見てみよう。
Githubにデモ・プロジェクトがあり、このセクションのサンプル・コードを提供している。
基本的なFSMをコードで簡単に説明すると、列挙型と switch文を使った以下の例のようになる。
まず、3つの状態からなるPlayerControllerState 列挙型を定義する:アイドル、ウォーク、ジャンプ。
そして、switchをUpdateループの条件文として使用し、現在どの状態にあるかをテストする。状態に応じて、適切な関数を呼び出して、該当する特定の動作を実行することができる。
これはうまくいくが、PlayerControllerスクリプトはすぐにゴチャゴチャになる。1つのスクリプトでゲームの状態を管理するためにswitchステートメントを使用することは、複雑でメンテナンスしにくいコードにつながる可能性があるため、ベストプラクティスとはみなされません。状態や遷移の数が増えると、switch文は大きくなり、理解するのが難しくなる。
さらに、switch文に変更を加える必要があるため、新しいステートやトランジションを追加するのが難しくなる。一方、Stateパターンは、よりモジュール化された拡張可能な設計を可能にし、新しいステートやトランジションを追加しやすくする。
Stateパターンを再実装して、PlayerControllerのロジックを再編成しよう。このコード例は、Githubにホストされているデモ・プロジェクトでも利用できる。
オリジナルのGang of Fourによれば、ステート・デザインパターンは2つの問題を解決する:
- オブジェクトは、その内部状態が変化したときに振る舞いを変えるべきである。
- 状態別動作は独立して定義される。新しいステートを追加しても、既存のステートの動作に影響はない。
先ほどのコード例では UnrefactoredPlayerControllerクラスは状態の変化を追跡できますが、2つ目の問題を満たしていません。新しい状態を追加する際には、既存の状態への影響を最小限に抑えたい。その代わりに、状態をオブジェクトとしてカプセル化することができる。
例の各状態を上の図のように構成することを想像してほしい。ここでは、適切なステートに入り、条件によって制御フローが終了するまで各フレームをループする。言い換えれば、特定の状態をEntry、Update、Exitでカプセル化する。
上記のパターンを実装するには、IStateというインターフェースを作る。ゲーム内の各具象状態は、この規約に従ってインターフェースを実装する:
- エントリー:このロジックは、最初にステートに入るときに実行される。
- 更新このロジックは毎フレーム実行されます(ExecuteまたはTickと呼ばれることもあります)。MonoBehaviourがそうであるように、Updateメソッドをさらに細分化し、物理演算用のFixedUpdate、LateUpdateなどを使うことができる。アップデートのどの機能も、状態変化のトリガーとなる条件が検出されるまで、各フレームで実行される。
- 出口:ここでのコードは、ステートを離れて新しいステートに移行する前に実行される。
IStateを実装した各状態のクラスを作成する必要がある。サンプル・プロジェクトでは、WalkState、IdleState、JumpStateに別々のクラスが設定されている。
別のクラスStateMachine.csは、制御フローがどのようにステートに入り、ステートを出るかを管理する。3つのステートの例では、ステートマシンは以下のコードサンプルのようになる。
このパターンに従うために、ステートマシンは、その管理下にある各状態(この場合、walkState、jumpState、idleState)のパブリックオブジェクトを参照する。ステートマシンはMonoBehaviourを継承していないので、コンストラクタを使って各インスタンスをセットアップする。
コンストラクターに必要なパラメーターは何でも渡すことができる。サンプルプロジェクトでは、各ステートでPlayerControllerを参照している。そして、それを使ってフレームごとに各状態を更新する(以下のIdleStateの例を参照)。
ステートマシンの概念について、以下の点に注意:
- Serializable属性を使用すると、StateMachine.cs(およびそのパブリック・フィールド)をInspectorに表示できます。別のMonoBehaviour(PlayerControllerやEnemyControllerなど)は、ステートマシンをフィールドとして使用できる。
- CurrentStateプロパティは読み取り専用です。StateMachine.cs自体は、このフィールドを明示的に設定していない。PlayerControllerのような外部オブジェクトは、Initializeメソッドを呼び出してデフォルトのStateを設定できる。
- 各状態オブジェクトは、現在アクティブな状態を変更するためにTransitionToメソッドを呼び出すための独自の条件を決定する。StateMachineインスタンスのセットアップ時に、各ステートに必要な依存関係(ステートマシン自体を含む)を渡すことができる。
サンプル・プロジェクトでは、PlayerControllerにすでにStateMachineへの参照が含まれているので、Playerパラメータを1つ渡すだけです。
各ステートオブジェクトはそれぞれ内部ロジックを管理し、GameObjectやコンポーネントを記述するのに必要な数だけステートを作ることができる。それぞれがIStateを実装した独自のクラスを持つ。SOLIDの原則に従い、ステートを追加しても、以前に作成されたステートへの影響は最小限に抑えられる。
以下はIdleStateの例である。
StateMachine.csスクリプトと同様に、コンストラクタでPlayerControllerオブジェクトを渡す。このプレーヤには、ステートマシンへの参照と、Updateロジックに必要な他のすべてが含まれます。IdleStateは、キャラクタコントローラのベロシティやジャンプの状態を監視し、ステートマシンのTransitionToメソッドを適切に呼び出します。
WalkStateと JumpStateの実装についても、サンプルプロジェクトを確認してください。動作を切り替える1つの大きなクラスがあるのではなく、それぞれのステートが独自の更新ロジックを持ち、互いに独立して機能するようになっている。
ステート・パターンは、オブジェクトの内部ロジックを設定する際に、SOLIDの原則を守るのに役立つ。各状態は比較的小さく、別の状態に移行するための条件だけを追跡する。オープン・クローズの原則に従って、既存のステートに影響を与えることなくステートを追加することができ、1つのモノリシックなスクリプトで煩雑なswitch文やif文を避けることができる。
また、その機能を拡張して、状態の変化を外部のオブジェクトに伝えることもできる。イベントを追加したいかもしれない(オブザーバー・パターンを参照)。ステートに入るとき、またはステートを抜けるときにイベントを持つことで、関連するリスナーに通知し、実行時に応答させることができる。
一方、追跡する州が数州しかない場合、余分な構造は過剰になる可能性がある。このパターンが意味を持つのは、状態がある程度複雑化することを想定している場合だけかもしれない。他のデザインパターンと同様に、特定のゲームのニーズに基づいて長所と短所を評価する必要がある。
Unityでプログラミングするための高度なリソース
電子書籍 ゲームプログラミングパターンでコードをレベルアップには、Unityでデザインパターンを使用する方法について、より多くの例が掲載されています。
Unityの上級テクニカル電子書籍や記事はすべて、ベストプラクティスハブでご覧いただけます。電子書籍は、ドキュメンテーションの上級ベストプラクティスのページでもご覧いただけます。