このページでは、ScriptableObjectをロジックコンテナとして使用する方法を説明します。こうすることで、デリゲート・オブジェクト、つまり必要なときに呼び出せるアクションの小さなバンドルとして扱うことができる。
本書は、電子書籍に付属するデモでUnity開発者を支援するために作成された6つのミニガイドシリーズの第4弾です、 UnityでScriptableObjectsを使用してモジュラーゲームアーキテクチャを作成する.
このデモは、古典的なボールとパドルを使ったアーケード・ゲームのメカニズムにインスパイアされたもので、ScriptableObjectが、テスト可能でスケーラブル、かつデザイナーフレンドリーなコンポーネントの作成にどのように役立つかを示しています。
電子書籍、デモプロジェクト、そしてこれらのミニガイドを合わせると、UnityプロジェクトでScriptableObjectクラスを使用してプログラミングデザインパターンを使用するためのベストプラクティスが提供されます。これらのヒントは、コードを単純化し、メモリ使用量を減らし、コードの再利用性を促進するのに役立つ。
このシリーズには以下の記事が含まれる:
ScriptableObjectのデモ・プロジェクトやこの一連のミニ・ガイドに飛び込む前に、デザイン・パターンの核心は単なるアイデアに過ぎないということを覚えておいてほしい。すべての状況に当てはまるわけではない。これらのテクニックは、UnityとScriptableObjectの新しい使い方を学ぶのに役立ちます。
それぞれのパターンには長所と短所がある。特定のプロジェクトに有益なものだけを選ぶ。デザイナーはUnity Editorを多用していますか?ScriptableObjectベースのパターンは、開発者とのコラボレーションを助ける良い選択かもしれない。
結局のところ、最良のコード・アーキテクチャとは、あなたのプロジェクトとチームに合ったものなのだ。
ストラテジー・パターンを使えば、インターフェイスやベースとなるScriptableObjectクラスを定義し、実行時にそれらのデリゲート・オブジェクトを交換可能にすることができる。
その応用例のひとつが、特定のタスクを実行するためのアルゴリズムをScriptableObjectにカプセル化し、そのScriptableObjectを他の何かのコンテキストで使うというものだ。
例えば、EnemyUnitクラスのAIや経路探索システムを書く場合、経路探索テクニック(A*やダイクストラなど)を持つScriptableObjectを作成するかもしれない。
EnemyUnit自体にはパスファインディングのロジックは含まれていない。その代わりに、別の "戦略 "ScriptableObjectへの参照を保持する。この設計の利点は、オブジェクトを交換するだけで別のアルゴリズムに切り替えられることだ。これは、実行時に異なるビヘイビアを選択する一つの方法である。
MonoBehaviourはタスクを実行する必要がある場合、自身のメソッドではなく、ScriptableObjectの外部メソッドを呼び出します。例えば、ScriptableObjectにはMoveUnitや SetTargetのパブリックメソッドがあり、敵ユニットを駆動したり、目的地を指定したりすることができる。
抽象的な基底クラスやインターフェイスを使えば、このパターンを改良することができる。そうすることで、そのストラテジーを実装したScriptableObjectを別のものと入れ替えることができる。このホットスワップ可能なScriptableObjectは、それを参照するMonoBehaviourに「プラグイン」します。
ゲームの状況によってEnemyUnitの動作を変更する必要がある場合は、外側のコンテキスト(MonoBehaviour)でその状況をチェックできます。そして、別のScriptableObjectをレスポンスとして差し込むことができる。
実装の詳細をScriptableObjectに分離することで、チーム内の責任分担もスムーズになります。ある開発者はScriptableObject内のアルゴリズムに集中し、別の開発者はMonoBehaviourコンテキストに取り組む。
このプラグイン可能な動作を作るには、次のことを確認する:
- ストラテジーの基本クラスまたはインターフェイスを定義する:このクラスまたはインターフェースは、ストラテジーを実行するために必要なメソッドとプロパティを含んでいなければならない。
- ScriptableObject クラスを作成します:それぞれが異なる戦略の実行を提供することができる。例えば、単純なAIアルゴリズムを実装するクラスと、より複雑なアルゴリズムを実装するクラスを作ることができる。
- ストラテジーを実装する ScriptableObject を作成する:不足しているロジックを入力し、必要な値をインスペクタに入力します。
- 文脈の中で戦略を使う:MonoBehaviourで、ScriptableObjectに実装されているメソッドやプロパティを呼び出します。これらのメソッドを簡単に呼び出すには、依存関係をパラメータとして渡す。
このようにコードを整理することで、同じ戦略の異なる実装を簡単に切り替えることができる。このプラグイン可能な動作は、デバッグやメンテナンスが容易になる。
アルゴリズムや戦略は複雑である必要はない。例えば、PaddleBallSOプロジェクトでは、SimpleAudioDelegateでかなり基本的なオーディオ再生システムをデモしている。
抽象クラスAudioDelegateSOは、AudioSourceパラメータを受け付ける単一のPlayメソッドを定義している。具体的な実装はこれを上書きする。
SimpleAudioDelegateSOサブクラスは AudioClips の配列を定義します。ランダムにクリップを選び、オーバーライドされたPlayメソッドの実装を使って再生する。これは、ピッチとボリュームのバリエーションをカスタム範囲内で追加するものです。
わずか数行のコードですが、以下のコード・スニペットでさまざまなオーディオ・エフェクトを作ることができます。
この具体的な例は、オーディオを多用するのには適していないが、ストラテジー・パターンにおけるScriptableObjectの基本的な使い方のデモとして、ここで紹介する。
デザイナーはコードに触れることなく、サウンドエフェクトを表現する様々なScriptableObjectを作成できます。この場合も、ベースとなるScriptableObjectが完成すれば、開発者からのサポートは最小限で済む。
PaddleBallSOでは、レベルの壁にボールが当たった時に再生される新しいサウンドを誰でも設定できるようになりました。デザイナーは、完全にエディター内で仕事をするため、クリエイティブな独立性と柔軟性を得ることができる。このアプローチでは、開発者がすべての設計決定を支援する必要がなくなるため、プログラミング・リソースが解放される。
Patterns demoでオーディオの例も見ることができます。各サウンドは、インスタンス間で若干の違いがありますが、わずかに異なるSimpleAudioDelegateSOアセットから派生します。
この例では、各コーナーにAudioSourceが含まれている。カスタムAudioModifier MonoBehaviourは、ScriptableObjectベースのデリゲートを使ってサウンドを再生します。
ピッチの違いは、各ScriptableObjectアセットの設定(BeepHighPitched_SO、BeepLowPitched_SOなど)に起因するものです。
ScriptableObjectを使ってアクションロジックを制御すれば、デザインチームがアイデアを試しやすくなります。これにより、デザイナーは開発者からより独立して仕事をすることができる。
PaddleBallSOプロジェクトもまた、目的システムに戦略パターンを使用している。これは実行時に変化させる必要はないが、各オブジェクトをScriptableObjectにカプセル化することで、勝ち負けの条件を柔軟にテストできるようになる。
抽象的な基本クラスであるObjectiveSOは、目的の名前や完了したかどうかなどの値を保持します。
ScoreObjectiveSOのような具体的なサブクラスは、各目的を達成するための実際のロジックを実装する。ObjectiveSOのCompleteObjectiveメソッドをオーバーライドして、勝利条件のロジックを追加するのだ。
プレイヤーは特定のスコアを達成したり、一定数の敵を倒したりする必要があるのか?特定の場所に到着する必要があるのか、特定の品物を受け取る必要があるのか。これらは、ScriptableObjectベースの目標となりうる一般的な勝利条件である。
ObjectiveManagerは、ScriptableObjectの大きなコンテキストとして機能する。ObjectiveSOのリストを保持し、各ScriptableObjectが完了したかどうかを判断する。すべてのオブジェクティブSOが完了の状態を示したら、試合終了。
例えば、ScoreObjectiveSOは得点目標を実装する一つの方法を示している:
- カスタムPlayerScore構造体は、プレーヤーID、インターフェイスのUI要素、および実際のスコア値に一致します。
- ScoreManagerコンポーネントが更新されるたびに、目的は勝利条件をチェックします。
- プレイヤーのスコアがm_TargetScore以上であれば、勝利したPlayerScoreオブジェクトをイベントとして送信する。
ObjectiveManagerは、与えられた目標がすべて完了していることだけを気にする。各目標の細部そのものに気づいていないのだ。
繰り返しになるが、ここでの目標はモジュール化である。これにより、既存のものに影響を与えることなく、各ObjectiveSOをカスタマイズすることができます。
PaddleBallSOゲームの目的はただ一つ。どちらかのプレーヤーが勝利スコアのゴールに到達した場合、ゲームプレイは終了する。
しかし、これを拡張したり、目標を組み合わせてより複雑な目標システムを作ることもできる。新しいゲームモード(例えば、時間切れになる前に最低得点を獲得する)を構築できるか実験してみよう。
ロジックは ScriptableObject 内にカプセル化されているため、任意の ObjectiveSO を別のものと入れ替えることができます。新しい勝利条件を作るには、ObjectiveManagerのリストを再設定するだけでよい。ある意味、目標は周囲の文脈に "プラグイン可能 "なのだ。
ObjectiveSOの便利な点は、GameObject間でメッセージを送るためのイベントである。次に、このイベント・ドリブン・アーキテクチャーを実装するためにScriptableObjectsを使用する方法を探ります。
ScriptableObjectsを使ったデザインパターンについて、電子書籍で詳しく読む UnityでScriptableObjectsを使用してモジュラーゲームアーキテクチャを作成する.また、一般的なUnity開発のデザインパターンについては、以下をご覧ください。 ゲームプログラミングパターンでコードをレベルアップ.