
このページでは、ゲームコード内のロジックからデータを分離するデータコンテナとして ScriptableObject を使用する方法について説明します。
これは、eBook「Create modular game architecture in Unity with ScriptableObjects」に付属するデモで、Unity開発者を支援するために作成された6つのミニガイドシリーズの第2弾です。
このデモは、古典的なボールとパドルを使ったアーケードゲームのメカニクスに着想を得たもので、ScriptableObject がテスト可能でスケーラブル、かつデザイナーにとって使いやすいコンポーネントの作成にどのように役立つかを示しています。
この e ブック、デモプロジェクト、およびこれらのミニガイドは、Unity プロジェクトで ScriptableObject クラスを使用してプログラミングデザインパターンを使用するためのベストプラクティスを提供します。これらのヒントは、コードを簡素化し、メモリ使用量を削減し、コードの再利用性を促進するのに役立ちます。
このシリーズには、次の記事が含まれています。
ScriptableObject デモプロジェクトとこのミニガイドシリーズを掘り下げる前に、その中核をなすデザインパターンは単なるアイデアに過ぎないことを覚えておいてください。すべての状況に当てはまるわけではありません。これらのテクニックは、Unity と ScriptableObject の新しい使い方を学ぶのに役立ちます。
それぞれのパターンには長所と短所があります。特定のプロジェクトにとって意味のあるものだけを選びましょう。デザイナーは Unity エディターに大きく依存していますか?ScriptableObject ベースのパターンは、開発者と共同作業するのに良い選択肢です。
最終的には、プロジェクトやチームに合ったコードアーキテクチャがベストです。

ソフトウェア開発者は、多くの場合、アプリケーションを小さな自己完結型のユニットに分割するモジュール性に懸念を持っています。各モジュールは、アプリケーションの機能の特定の側面を担います。
Unity では、ScriptableObject はロジックからデータを分離するのに役立ちます。
ScriptableObject は、特に静的なデータを格納するのに適しています。これにより、ゲームの統計、アイテムや NPC の設定値、キャラクターの会話などに最適です。
ゲームプレイデータをビヘイビアロジックから分離することで、プロジェクトの独立した各パーツのテストとメンテナンスが容易になります。この「懸念の分離」により、必要な変更を加える際に、意図しない副作用や望ましくない副作用を減らすことができます。

ScriptableObject ワークフローの復習をご希望の場合は、こちらの Unity Learn 記事が役立ちます。そうでない場合は、以下の簡単な説明をご覧ください。
ScriptableObject を定義します。作成するには、格納するデータのフィールドとプロパティを持つ ScriptableObject 基本クラスを継承する C# クラスを定義します。ScriptableObject は、MonoBehaviour で利用可能な同じデータ型を格納できるため、汎用性の高いデータコンテナとなります。 エディターから CreateAssetMenuAttribute を追加して、プロジェクトでのアセットの作成を容易にします。
アセットの作成:ScriptableObject クラスを定義したら、その ScriptableObject のインスタンスをプロジェクト内に作成できます。これはディスクに保存されたアセットとして表示され、異なるゲームオブジェクトやシーンで再利用できます。
設定値:アセットを作成したら、Inspector でフィールドとプロパティの値を設定することで、アセットにデータを入力します。
アセットの使用:アセットがデータを保持したら、変数またはフィールドからデータを参照します。ScriptableObject アセットに加えられた変更は、プロジェクト全体に反映されます。
ScriptableObject は、ゲームのさまざまな部分でデータコンテナとして再利用できます。例えば、ScriptableObject 内で武器やキャラクターのプロパティを定義し、プロジェクト内のどこからでもそのアセットを参照できます。
注:また、実行時に CreateInstance メソッドを使用して ScriptableObject を生成することもできます。ただし、データストレージについては、通常は CreateAssetMenuAttribute を使用して事前に ScriptableObject アセットを作成します。

ScriptableObject が MonoBehaviour よりもデータストレージに適している理由をよりよく理解するために、それぞれの空のバージョンを比較します。「Asset Serialization」を「Mode」に設定します。「Project Settings」で「Force Text」を選択すると、YAML マークアップがテキストとして表示されます。
それ以外の場合は空の MonoBehaviour で新しいゲームオブジェクトを作成します。次に、空の ScriptableObject アセットと比較します。これらを横に並べると、上の画像のような見た目になります。
ScriptableObject は MonoBehaviour と比較して軽量で、Transform コンポーネントのような MonoBehaviour に伴うオーバーヘッドを持ちません。これにより、ScriptableObject のメモリフットプリントが小さくなり、データストレージ用に最適化されます。

ScriptableObject はアセットとして保存されるため、再生モード以外でも保持されるので便利です。例えば、ScriptableObject のデータは、新しいシーンをロードした場合でもどこからでも利用できます。
Patterns デモの例には、自分でテストできる基本的なクレジット画面があります。Credits_Data ScriptableObject を変更し、Update を押して、保存されているテキストを確認します。
大量のダイアログを含む RPG や、あらかじめ用意されたスクリプトを含むチュートリアルシーンがある場合、これは大量のデータを保存する一般的な方法です。
ScriptableObject 内のデータは変更されると即座に更新されますが、私たちのプロジェクトでは、手動で画面を更新するには Update ボタンが必要です。UI Toolkit ベースの画面は 1 回のみビルドされ、データが変更されたときに通知を受ける必要があります。
更新を自動的に同期したい場合は、ScriptableObject 内にイベントを作成します。たとえば、この ExampleSO スクリプトは、ExampleValue が変更されるたびに OnValueChanged イベントを呼び出します。以下のコード例をご覧ください。
次に、リッスンしている UI オブジェクトに OnValueChanged をサブスクライブさせ、適宜更新します。

ScriptableObject は、多くのオブジェクトが同じデータを共有するときに輝きます。例えば、同じ攻撃速度と最大ライフを持つユニットが多数あるストラテジーゲームを制作している場合、それらの値をすべてのゲームオブジェクトに個別に保存するのは非効率的です。
代わりに、共有データを 1 か所に統合し、各オブジェクトがその共有場所を参照するようにできます。ソフトウェア設計では、これはフライウェイトパターンと呼ばれる最適化です。このようにコードを再構築することで、大量の値をコピーすることがなくなり、メモリフットプリントを削減できます。
PaddleBallSO では、GameDataSO ScriptableObject は共有データストレージとして機能します。
Paddle スクリプトと Ball スクリプトは、一般的な設定(速度、質量、物理演算の跳ね返りなど)のコピーを別に保持するのではなく、可能な限り同じ GameDataSO インスタンスを参照します。各ゲーム要素は位置や入力イベントなどの一意のデータを保持しますが、可能な場合はデフォルトで共有データに設定されます。
オブジェクトが 2 つか 3 つあるだけでメモリの節約は目立たないかもしれませんが、共有データを編集する方が、手動で編集するよりも速く、エラーも少なくなります。
例えば、パドルの速度を変更する必要がある場合、1 か所で調整すると、すべてのシーンで両方のパドルが更新されます。MonoBehaviour に一意のフィールドとして格納している場合、1 回のクリックで 2 つの値が簡単に同期しなくなります。
ScriptableObject にデータをオフロードすることで、バージョン管理にも役立ち、チームメイトが同じシーンやプレハブで作業する際のマージ競合を防ぐことができます。

GameDataSO は、ScriptableObject をデータコンテナとして使用する方法を示しています。PaddleBallSO には、ゲームプレイを構成するさまざまな設定が含まれています。
これらの設定とデータがすべて 1 か所にあるため、GameDataSO ではあらゆるオブジェクトがこの共有データにアクセスできます。これにより、これらのオブジェクトの管理方法が簡素化され、プロジェクト全体で一貫性が高まります。パドルの物理演算を変更しますか?複数のスクリプトを調整する代わりに、ここで 1 つの変更を加えます。

時には、ケーキを食べて食べることもできます。デュアルシリアライズでは、データを ScriptableObject に格納しつつ、別の形式で維持することができます。
LevelLayoutSO スクリプトはこの概念を示しています。パドルとボールの開始位置を保持するだけでなく、壁とゴールのトランスフォームデータをカスタム構造体に格納します。
これらの値は、ExportToJson メソッドを介してディスクに書き込むことができます。JSON ファイルは人間が読めるテキストであり、Unity の外部で直接修正できます。これにより、エディターで ScriptableObject を操作し、JSON ファイルや XML ファイルなどの別の場所にデータを保存できます。
JSON や XML のようなファイル形式は、エディターでの作業が難しい場合がありますが、Unity の外部ではテキストエディターで簡単に変更できます。これにより、カスタムまたはユーザーが修正したレベルの可能性が広がります。
その後、GameSetup スクリプトは LevelLayout ScriptableObject または外部 JSON ファイルを使用してゲームレベルを生成できます。
カスタム修正したレベルを読み込むために、セットアップスクリプトは CreateInstance を使用してランタイムに ScriptableObject を生成します。次に、JSON ファイルからテキストを読み取り、ScriptableObject にデータを入力します。
カスタムデータによって ScriptableObject の内容が置き換えられ、この外部で変更されたレベルを他のレベルと同じように使用できるようになります。アプリケーションの他の部分は、スイッチを意識することなく正常に機能します。

パドルボールを使ったミニゲームでは、ScriptableObject データコンテナのすべてのユースケースを実演することはできませんが、ご自身のアプリケーションでは、以下のことを考慮してください。
ScriptableObject を深く掘り下げ、独自のプロジェクトに合わせて調整することで、さらに多くの応用例が見つかります。特にデータ管理に役立ち、さまざまなゲーム要素にわたって一貫性を維持しやすくなります。

ScriptableObject によるデザインパターンの詳細については、e ブック「Create modular game architecture in Unity with ScriptableObjects」を参照してください。また、「ゲームプログラミングパターンによるコードのレベルアップ」では、Unity 開発の一般的なデザインパターンについて詳しく知ることができます。