複雑なロジックを制御する方法 – 有限状態機械(FSM)
これを読んでいる方の中には、初心者向けのチュートリアルを実践して、そこで学んだゲーム機構をもとに自分自身のプロジェクトを作成しようと決めた方もいるはずです。しかし複雑なアクションを追加するやいなや、ネストした条件のジャングルに迷い込み、見つけるのが困難なバグに行き当たる羽目になるのです。そして最後には、プロジェクトを放棄するでしょう。
インターネットで見つかるチュートリアル(ゲームエンジンは問いません)のほとんどは、可能な限り余計なものを排除して、チュートリアルに特化した目的を達成する方法だけを示します。あいにく、たいていの場合そこで示されるコードには拡張性のかけらもありません。
このチュートリアルでは、視点をより高次に置き、プレイヤー(あるいは敵でも何でも)のロジックをカプセル化してプロジェクトをどのように構成するかを説明します。
状態機械とは何か?
状態機械(ステートマシンとも)は、オブジェクトのロジックを、明確に定義された状態の固定セットに分割し、各状態を独立して操作します。それぞれの状態は、それ自体に関係するロジックしか含みません。たとえばプレイヤーが「落下中」の状態のときは、左右に移動するボタンやジャンプボタンのことを気にする必要はありません。なぜなら地面の上に立っていないからです。特定の条件が揃ったら、プレイヤーの状態を切り替えます。
プレイヤーが「落下中」の状態を想像してください。空中にいるあいだは、いかなるアクションも実行すべきではありません。これはただ、下方向へかかる重力に身を任せているだけだからです。別の状態へ移行するには、所定の条件を満たす必要があります。「落下中」の状態の場合、その条件は「地面と衝突している」になるでしょう。これが起きたら、プレイヤーの状態を「落下中」から「待機」に変更します。さて、「待機」状態のあいだは移動ボタンが押されていないかを定期的にチェックして、押されていたらプレイヤーの状態を「待機」から「歩行」に変更します。「歩行」状態のあいだ、何かほかのイベント(移動ボタンがリリースされるとか)が発生するまでプレイヤーは移動しつづけます。流れがつかめてきましたか?
やってみよう
まず「」チュートリアルからアセットをダウンロードしましょう。上記チュートリアルにのっとってプレイヤーを「プラットフォーマーオブジェクト」として作成し、上を歩いたりジャンプできるようにプラットフォーム(「プラットフォーム」ビヘイビア)をいくつか作成します。 プレイヤーオブジェクトのプロパティを開き、「待機」、「歩行」、「ジャンプ」、「落下」というアニメーションを作成します。それからプレイヤーオブジェクトにテキスト型の変数を作成し、変数名を「方向」、値を「右」と設定します。 イベントエディターに切り替え(main シーン)、プロジェクトマネージャーから外部イベント「プレイヤー状態初期化」、「プレイヤー状態落下」、「プレイヤー状態待機」、「プレイヤー状態歩行」、「プレイヤー状態ジャンプ」を作成します。右側のツールバーからイベント作成用のドロップダウンメニューを開き([イベントを選択して追加]ボタン)、main シーンのイベントに新しいイベントグループを作成します。「プレイヤーのロジック」と名前をつけます。シーンが始まった条件をイベントに追加します。サブイベントを追加して、[他を追加](ツールバーまたは右クリックメニュー)から「プレイヤー状態初期化」外部イベントへのリンクを作成します。他のプレイヤー状態についても、同様の手順を繰り返してください。
Note
アニメーションに頼らなくても、「状態」専用の シーン変数でプレイヤーの状態を管理することもできます。これはより上級のテクニックですが、自由度は増します。しかしここで説明している、ロジックとそれに対応するアニメーションをセットにするアプローチには、分かりやすさとイベントシートのシンプルさという利点があります。さらに、現在のアニメーション/状態のフレームをチェックして、アニメーション内の特定フレームに差し掛かったときに別のロジックをトリガーするというような高度な分岐処理もできます。
デバッグ情報
ゲームプレイ中にプレイヤーが今どの状態にあるのかを知るために、テキストオブジェクトを作成して「デバッグ状態」という名前をつけます。これをメインシーンに追加して、次のようなアクションを作成します(条件は無しでよいです)。これでプレイヤーの頭上に現在の状態が表示されるようになり、何か問題が発生したときにはいつでも、どの状態を調べればいいのかが一目でわかるでしょう。
最初の状態「初期化」
外部イベント「プレイヤー状態初期化」を開きます。この状態は、ゲーム開始時にプレイヤーオブジェクトを初期化するのに使います。「外部イベントを編集するには、イベントが含まれるシーンを選択してください。」というダイアログの「シーンを選択する」から main シーンを選びます。
メインシーンのイベントシートには、この状態初期化のコードを実行する条件をすでに設定してあるため、ここで必要なのはアクションの追加だけです。
まず最初に、プラットフォームビヘイビアによるデフォルトの制御を無効にする必要があります。これが状態機械の妨げになるからです。
次にプレイヤーのスプライトのアニメーションに「落下」を設定します。これによってイベントのゲームループの次回のイテレーションでは「落下」状態が実行されます。ここで落下状態を設定したのはプレイヤーが最初は空中にいるからで、そこから着地して待機状態に移行するという流れを想定しています。
この状態には、ほかにもプレイヤーの獲得ポイントや残弾数などをリセットする処理を含めることができます。レベルを再スタートしたいときには、初期化状態に切り替えればいつでもプレイヤーの属性をリセットできます。
「落下」状態
落下はいちばん融通の利く状態です。どの状態に切り替えるのがいいか迷うようなときには、「落下」状態にすれば間違いありません。なぜなら、プレイヤーがオブジェクトに衝突するとすぐに適切な状態に移行するのが確実だからです。落下中のプレイヤーにできるアクションは特にありません。落下状態に入る前に受けたフォースの影響が、ただそのまま継続するだけです。たとえばジャンプした後に落下状態に入った場合は、ジャンプしたときの方向に向かって引き続き移動するでしょう。しかし途中で方向を変えることはできません(チュートリアルの最後にある演習で、この挙動を変えるテーマを扱います)。したがって、この状態でやることは、プレイヤーが地面と衝突したかどうかチェックすることだけです。衝突したら、プレイヤーを「待機」状態に移行します。
「待機」状態
待機状態は、コンピューターの前にいる人が何も操作しないとなる状態です。別の言い方をすれば、何のキーも押されていなくて、プレイヤーオブジェクトがただ立っているだけの状態です。「落下」状態に似て、ここでする必要のあるアクションは特にありません。ただ「待機」状態から抜ける条件をチェックするだけです。まず最初に、プレイヤーが地面の上にいるかどうかチェックします。もし違ったら、落下状態に移行します。次に、プレイヤーが左右の矢印キーを押しているかどうかチェックします。そうであれば、歩行状態に移行します。最後に上矢印キーが押されていないかチェックし、押されていたらジャンプ状態に移行します。どうです、明快でしょう?
「歩行」状態
ついにプレイヤーに能動的にアクションをさせる番が来ました。歩行状態です。ひとつの状態の中で左への歩行と右への歩行の両方をあつかうので、最初にプレイヤーの動く方向を決めないとなりません。そこで歩行状態に入った時点でどのキーが押されているかを一度だけチェックし、それに基づいてプレイヤーの方向変数を設定します。歩行状態のあいだは、変数に設定されたその方向に向かってプレイヤーを移動させ続けます。
これでプレイヤーにさせるアクションの件は片付きました。残るは、別の状態に切り替えるための条件設定です。ところで、そもそも歩行中には何が起きうるのでしょうか?いちばん有りうるのは、移動キーを放すことでしょう。この場合は、「待機」状態に移行します。もしプラットフォームの端を越えたら、「落下」状態に切り替えます。ジャンプキーを押されたら、ジャンプ状態に切り替えます。
「ジャンプ」状態
もう見当がつくと思いますが、最初にやることは、この状態に入ったら一度だけジャンプアクションをトリガーすることです。ジャンプするフォースは黙っていてもかかるので、一度ジャンプしたらあとはもうその件について考える必要はありません。毎度のことですが、最後に必要なのは現在の状態から抜けるための条件設定です。今回の場合は、プレイヤーが「ジャンプ中ではない」ことと「落下中ではない(地面の上にいる)」ことをチェックします。どちらかに当てはまっていれば、「落下」状態に移行します。
これで全部です。
まとめ
プレイヤーのロジックを 5 つの異なる状態に分割し、それぞれの状態には関係する処理だけを含めました。もしプレイヤーに飛行や潜水、瀕死、激突などの能力を追加したくなったら、新しい状態を作成して、そこに処理を任せればよいでしょう。
こちらからプロジェクト全体をダウンロードできます(英語)。
演習
1)たぶんお気づきでしょうが、現在のプレイヤーは空中にいるあいだ、デフォルトの制御とは異なる動き方をします。これは「落下」状態が今のところキー入力を受けつけていないからです。そこで、プレイヤーが空中にいるあいだでも移動できるようにしてみてください。移動キーが押されているかどうかを確認して、押されていたら通常の移動にかかるフォースの半分くらいを適用するのがいいでしょう。