i-school - インターフェースを利用したステートパターンの実装例

インターフェースの機能と定義


 インターフェースは、クラスに対して特定のメソッドやプロパティを実装することを要求することができるものです。
これにより、複数のクラスで同じ動作をすることを強制することができます。

参考サイト
++C++; // 未確認飛行 C 様
インターフェース



 実装の方法は、インターフェースを定義するための「interface」キーワードを使用します。例えば、次のようなインターフェースを定義できます。


<= クリックすると開きます。




 このインターフェースを実装するクラスは、「IMoveable」を実装して、「Move」メソッドを実装することが必要になります。


<= クリックすると開きます。


  インターフェースで定義したメソッドは、明記しなくても public 扱いになります。



 インターフェースの利点としては、次のようなものがあります。

 ・複数のクラスで同じ動作を強制することができる。
 ・インターフェースを継承したクラスに対して同じように操作することができる。
 ・コードの保守性を高めることができる。
 ・仮想メソッドよりも軽量である。

 これらの利点により、インターフェースを利用した設計を行うことでよりスマートなコードを書くことができます。


ステートパターン

 
 インターフェースを利用したステートパターンは、オブジェクトの状態に応じて処理を切り替えるための設計です。デザインパターンの1つになります。

 ステートパターンは、オブジェクトの状態をステートとして記述(管理)し、各ステートで独自のロジックを実装することができます。
これにより、コードの複雑性を減らし、オブジェクトの状態変化に対して自然に対応することができます。
また、インターフェースを利用することにより、各ステートのクラスが互いに独立しているため、再利用性が高くなります。

 よって、このステートパターンは、ゲーム開発においては敵キャラクターやプレイヤーキャラクターなどの挙動を制御する際などに役立ちます

 インターフェースを利用した実装例を示すと、以下のようになります。


1.ステート用のインターフェースを定義する


 まず、ステート用の IState インターフェースを定義します。
今回はサンプルのためファイルを分けていますが、1つのファイルにインターフェースとクラスとを一緒に書いても問題ありません。


<= クリックすると開きます。


 3つのメソッドの定義をおこないます。

 それぞれのメソッドには名称に応じた役割があります。


2.各ステート用のクラスを定義する


 次に、各ステートを表すクラスを実装します。
ステートごとのに1つのクラスを定義するようにします。

 今回は2つのクラスを定義しています。


<1.移動用のステートクラス>


<= クリックすると開きます。



<2.ジャンプ用のステートクラス>


<= クリックすると開きます。


 どちらのクラスにも ICommand インターフェースが実装され、クラス内に Enter メソッド、Execute メソッド、そして Exit メソッドの実装が強制されていることが分かります。
そして、それぞれのメソッドの実装(中身)が異なるため、同名のメソッドではあるものの処理の内容が変わっています

 このようにして、各メソッドの実装を各クラスによって行うことで、処理を抽象化し、メソッドの振る舞いを変えることで処理を変えることが出来ています(多態性があります)。


3.ステート管理クラスを定義する


 最後に、命令を管理するクラスを作成します。


<= クリックすると開きます。


 ChangeState メソッドを利用し、IState インターフェースを実装しているオブジェクトを currentState に登録しています。
ポイントとしては、currentState 変数は IState インターフェースであるため、クラスに関わらず、IState インターフェースを実装していれば代入できる点です。
そのため、MoveState クラス、JumpState クラスのどちらであっても currentState 変数に代入できます。
この処理を行うことにより、StateMachine クラスは現在のステートがどのステートであるかを管理することが出来ます。

 さらに ChangeState メソッドでは、前のステートが存在する場合には Exit メソッドを実行してステートの終了処理を行った上で、
新しいステートへの切り替え処理を行ってから、Enter メソッドを実行してステートの開始処理を行います。

 Update メソッドでは currentState 変数が存在するか確認し、(現在のステートを確認)
currentState 変数に格納されている IState インターフェースを実装しているクラスの Execute メソッドを実行します。

 Execute メソッドは MoveState クラスと JumpState クラスではメソッド内の処理が異なるため、同じメソッドを実行していますが、処理の振る舞いが変わります。


4.利用方法


 命令を実行するためには、「StateMachine」クラスにある currentState 変数に
「IState インターフェース」を実装した「MoveState」や「JumpState」などを代入することで、現在のステートを逐次切り替えて利用していきます。

 「StateMachine」クラスから「MoveState」を命令を実行するためのサンプルは以下のようになります。


<= クリックすると開きます。


 このサンプルでは、「Start」メソッド内では「StateMachine」に「MoveState」を追加し、Enter メソッドを実行しています。
その後、「Update」メソッド内でステートの更新管理を行い、「stateMachine」の「Update」メソッドを呼び出して、現在のステートの「Execute」メソッドを実行しています。


各ステートをキャッシュして利用するケース実装例


 先ほど提示した実装例の場合、ステートの遷移にあたっては、ステートをその都度インスタンスして利用していました。

 こちらのケースでは、ゲーム内で利用するステートを事前にキャッシュ(最初にインスタンスして変数に代入)しておくことで
インスタンスの生成は各ステート1回だけにし、ステートの代入されている変数を利用して処理を運用していく場合の実装例です。

 新しく StateID を作成し、他のクラスは定義内容を追加・修正しています。


1.StateID(enum) を定義する


 各ステートを識別するための enum を作成します。
string 型で管理するよりも保守しやすく、コードの可読性が向上し、誤入力を防ぐことができます。


<= クリックすると開きます。



2.ステート用のインターフェースを定義する


 先ほど作成した IState インターフェースに StateID を追加します。
これにより、各ステートの状態を登録する機能を持たせます。


<= クリックすると開きます。



3.各ステート用のクラスを定義する


 先ほど作成した MoveState クラスと JumpState クラスに StateID を追加します。
ここに各ステートの状態を登録し、StateMachine クラスで識別できる状態にします。

 その他の変更点はありません。


<1.移動用のステートクラス>


<= クリックすると開きます。



<2.ジャンプ用のステートクラス>


<= クリックすると開きます。



4.StateMachine クラスを定義する


 次に、StateMachine クラスにステートのキャッシュができるように Dictionary<string, IState> を追加します。
こちらにステートを登録するための RegisterState メソッドを追加します。
また ChangeState メソッドを削除し、代わりにステートIDを指定して遷移する ChangeStateByID メソッドを追加します。
Update メソッドには変更ありません。


<= クリックすると開きます。



5.Example クラスを定義して活用する例


 準備が整ったので、Example クラスを修正して運用していく例を提示します。

 Start メソッドを修正し、StateMachine クラスの RegisterState メソッドを実行して各ステートを事前に登録しておきます。
その後、ゲーム開始時のステートを ChangeStateByID メソッドを実行して設定しています。


<= クリックすると開きます。


 この設計では、ステートが StateMachine の Dictionary にキャッシュされて運用されています。
enum を使用して各ステートを ステートID で識別できるように管理しているため、ステート遷移の処理も読みやすく、安全に行うことができます。

 こちらの方法で実装を行うことで、最初に提示した、その都度ステートのインスタンスを作成して利用する設計よりも効率化が図れます。



 以上になります。