柔軟で拡張可能なゲームシステムのためにコマンドパターンを使用する。
Unityプロジェクトに一般的なゲームプログラミングデザインパターンを実装することで、クリーンで整理された読みやすいコードベースを効率的に構築・維持することができます。デザインパターンは、リファクタリングとテストの時間を短縮し、開発プロセスをスピードアップし、ゲーム、チーム、ビジネスを成長させるための強固な基盤に貢献します。
デザイン・パターンは、コードにコピー&ペーストできる完成されたソリューションではなく、より大規模でスケーラブルなアプリケーションを構築するのに役立つ追加ツールだと考えてほしい。
このページでは、コマンド・デザイン・パターンについて説明する。
ここでの内容は無料電子書籍に基づいています、 ゲーム・プログラミング・パターンでコードをレベルアップしよう.
Unityゲームプログラミングデザインパターンシリーズの他の記事は、Unityベストプラクティスハブまたは以下のリンクからチェックしてください:
コマンド・プログラミング・デザイン・パターンは、オリジナルのギャング・オブ・フォーのひとつであり、特定の一連のアクションを追跡したいときに便利である。アンドゥ/リドゥ機能を使ったり、入力履歴をリストに保存したりするゲームをプレイしたことがある人なら、このコマンドパターンを見たことがあるだろう。ユーザーが実際にターンを実行する前に、いくつかのターンを計画できる戦略ゲームを想像してみてほしい。それが指揮官のパターンだ。
コマンド・パターンでは、アクションをオブジェクトとして表現することができる。アクションをオブジェクトとしてカプセル化することで、ユーザー入力に応じてGameObjectの動作を制御する柔軟で拡張性の高いシステムを構築することができます。これは、メソッドを直接呼び出すのではなく、1つ以上のメソッド呼び出しを「コマンド・オブジェクト」としてカプセル化することで機能する。そして、これらのコマンド・オブジェクトを、キューやスタックのようなコレクションに格納し、小さなバッファとして機能させることができる。
このようにコマンドオブジェクトを保存することで、一連のアクションを遅延させて後で再生する可能性があるため、実行のタイミングをコントロールすることができる。同様に、やり直しや取り消しも可能で、各コマンド・オブジェクトの実行をコントロールするための柔軟性が追加されている。
ここでは、さまざまなゲームジャンルでよく使われるパターンを紹介する:
- リアルタイム戦略ゲームでは、コマンドパターンを使ってユニットや建物のアクションをキューに入れることができる。そして、リソースが利用可能になると、各コマンドを実行する。
- ターン制の戦略ゲームでは、プレイヤーはユニットを選択し、その移動やアクションをキューやその他のコレクションに保存することができる。ターンが終わると、ゲームはプレイヤーのキューにあるコマンドをすべて実行する。
- パズルゲームでは、コマンドパターンによって、プレイヤーは行動を元に戻したり、やり直したりすることができる。
- 格闘ゲームでは、特定のコマンドリストでボタンを押したり、ゲームパッドの動きを読み取ったりすることで、コンボや必殺技を繰り出すことができる。
GitHubにあるサンプル・プロジェクトで、コマンド・パターンを含む、ゲーム開発の文脈におけるさまざまなプログラミング・デザイン・パターンを試してみよう。
このサンプルでは、プレイヤーは左側のボタンをクリックして迷路を移動することができる。選手が動き回ると、その軌跡を見ることができる。しかし、もっと重要なのは、前の操作を取り消したり、やり直したりできることだ。
プロジェクトで対応するシーンを見つけるには、"9 Command "という名前のフォルダに行く。
コマンドパターンを実装するには、アクションを格納する一般的なオブジェクトが必要だ。このコマンドオブジェクトは、どのロジックを実行し、どのように元に戻すかを保持する。
これを実装する方法はいくつかあるが、ここではICommandと呼ばれるインターフェイスを使ったシンプルなバージョンを紹介しよう:
public interface ICommand
{
void Execute();
void Undo();
}
この場合、すべてのゲームプレイ・アクションはICommandインターフェイスを適用する(抽象クラスでこれを実装することもできる)。
各コマンド・オブジェクトは、それ自身のExecuteメソッドとUndoメソッドを担当する。そのため、ゲームにコマンドを追加しても、既存のコマンドに影響を与えることはない。
CommandInvokerクラスは、コマンドの実行と取り消しを担当する。ExecuteCommandメソッドとUndoCommandメソッドに加え、一連のコマンド・オブジェクトを保持するためのアンドゥ・スタックがある。
サンプル・プロジェクトでは、プレイヤーを小さな迷路の中で動かすことができます。選手の位置を移動させる簡単な方法は、PlayerMoverを作成することです。
そのためには、MoveメソッドにVector3を渡して、プレーヤーを4つのコンパスの方向に沿って誘導する必要があります。レイキャストを使用して、適切なレイヤーマスクで壁を検出することもできます。もちろん、コマンドパターンに適用したいものを実装するのは、パターンそのものとは別の話だ。
コマンド・パターンに従うには、PlayerMoverの Moveメソッドをオブジェクトとしてキャプチャする。Moveを直接呼び出す代わりに、ICommandインターフェイスを実装した新しいクラスMoveCommandを作成する。
public class MoveCommand :ICommand
{
プレイヤームーバー playerMover;
ベクター3の動き;
public MoveCommand(PlayerMover player, Vector3 moveVector)
{
this.playerMover = player;
this.movement = moveVector;
}
public void Execute()
{
playerMover.Move(movement);
}
public void Undo()
{
playerMover.Move(-movement);
}
}
達成したいロジックは何でもここに入るので、移動ベクトルでMoveを呼び出す。
ICommandには、シーンを以前の状態に戻すためのUndoメソッドも必要だ。この場合、Undoロジックは移動ベクトルを減算し、実質的にプレーヤーを逆方向に押す。
MoveCommandは、実行に必要なパラメータをすべて保存する。これらをコンストラクタで設定する。この場合、適切なPlayerMoverコンポーネントと移動ベクトルを保存します。
コマンドオブジェクトを作成し、必要なパラメータを保存したら、CommandInvokerのstaticExecuteCommandメソッドとUndoCommandメソッドを使ってMoveCommandを渡す。これはMoveCommandの ExecuteまたはUndoを実行し、アンドゥスタックでコマンドオブジェクトを追跡する。
InputManagerは PlayerMoverのMoveメソッドを直接呼び出さない。その代わりに、RunMoveCommandというメソッドを追加して、新しいMoveCommandを作成し、CommandInvokerに送信する。
それから、UIボタンのさまざまなonClickイベントを設定して、4つの動きベクトルでRunPlayerCommandを呼び出す。
InputManagerの実装の詳細については、サンプルプロジェクトをチェックしてください。また、キーボードやゲームパッドを使って独自の入力を設定することもできる。これでプレイヤーは迷路を移動できる。元に戻すボタンをクリックし、最初のマスに戻ることができます。
リプレイアビリティやアンドゥアビリティの実装は、コマンド・オブジェクトのコレクションを生成するのと同じくらい簡単だ。また、コマンドバッファを使用して、特定のコントロールでアクションを順番に再生することもできます。
例えば、格闘ゲームで特定のボタンを何回かクリックすると、コンボ技や攻撃が発動するような場合を考えてみよう。コマンドパターンでプレーヤーのアクションを保存することで、これらのコンボをより簡単に設定できる。
裏を返せば、コマンド・パターンは他のデザイン・パターンと同様に、より多くの構造を導入している。これらの余分なクラスやインターフェイスが、アプリケーションにコマンド・オブジェクトを配備するのに十分な利益をもたらすかどうかは、自分で判断しなければならない。
基本を学べば、コマンドのタイミングに影響を与えたり、文脈に応じて連続再生や逆再生ができるようになる。
コマンドパターンを取り入れる際には、以下の点を考慮すること:
- さらにコマンドを作成する:サンプル・プロジェクトには、MoveCommandという1種類のコマンド・オブジェクトしか含まれていません。ICommandを実装したコマンド・オブジェクトをいくつでも作成し、CommandInvokerを使って追跡することができる。
- やり直し機能を追加するには、別のスタックを追加すればいい:コマンドオブジェクトを取り消すときは、やり直し操作を追跡する別のスタックにプッシュする。こうすることで、取り消し履歴を素早く循環させたり、それらの操作をやり直したりすることができる。ユーザーがまったく新しい動作を呼び出したときに、やり直しスタックを消去する(サンプル・プロジェクトに実装があります)。
- コマンドオブジェクトのバッファには別のコレクションを使用する:先入れ先出し(FIFO)動作が必要な場合は、キューの方が便利かもしれない。リストを使用する場合は、現在アクティブなインデックスを追跡する。アクティブなインデックスより前のコマンドは取り消し可能である。インデックス以降のコマンドはやり直しが可能である。
- スタックのサイズを制限する:元に戻したりやり直したりする操作は、すぐに手に負えなくなる。スタックを最小限のコマンド数に制限する。
- コンストラクタに必要なパラメータを渡す:これは、MoveCommandの例に見られるように、ロジックをカプセル化するのに役立つ。
- CommandInvokerは、他の外部オブジェクトと同様に、コマンドオブジェクトの内部構造を見ることはなく、ExecuteやUndoを呼び出すだけである。コンストラクタを呼び出す際に、コマンドオブジェクトに動作に必要なデータを与える。
Unityアプリケーションでデザインパターンを使用する方法やSOLIDの原則については、無料の電子書籍「ゲームプログラミングパターンでコードをレベルアップ」で詳しく説明しています。
ベストプラクティスハブでは、Unityの高度なテクニカル電子書籍や記事をすべてご覧いただけます。電子書籍は、ドキュメンテーションの上級ベストプラクティスのページでもご覧いただけます。