ScriptableObjectとは何かと色々と悩んできましたが、答えは簡単でした。データを格納するオブジェクトです。もう少し言えば、Unityがデータをシリアライズ・デシリアライズしパラメータを格納するための仕組みです。色々な事が出来る割に用途が分かりにくいのでけっこう悩んでしまいました。
ScriptableObjectの用途
基本的にはScriptableObjectはパラメータや参照を格納する用途で使用します。
例えば、沢山の敵が居たとします。その敵は大量に配置する予定のため、オブジェクトプールのように「再生成」ではなく「パラメータ初期化して再登場」させる物とします。その場合、何処かに「初期化時のパラメータを保持する場所」が必要です。
この時、初期化時のパラメータを敵各々が持つのは少し勿体無いです。1体が1MB分のパラメータを初期化に使うなら100体いたら100MB。概ね同じパラメータなら汎用データで何とかしたいのが心です。
しかしstaticを使うのも少し考えどころです。static変数はゲーム内で絶対に開放されないパラメータを指します。その為、他のステージ等では余計な資産となります。また、少しパラメータが違う敵と同時に登場させたい場合、敵の数だけスクリプトが必要になります(もしくはパラメータのリスト化が必要)。
そこでScriptableObjectとしてパラメータを外出しオブジェクトから参照するように設定する事で、全体として単一のインスタンスで済み、なおかつ少し異なるパラメータを持つ敵を出したい場合は、参照先を変えるだけで済むという訳です。
また、ScriptableObjectはパラメータだけでなくアセットへの参照する機能も保持しているため、同じコードから生成したScriptableObjectであっても、異なる機能を持たせる事も可能です。
例えば敵の攻撃方法が「ScriptableObjectに登録されているGameObjectをInstantateする方式」の場合、ScriptableObjectに登録するスキルを変更するだけで、単純な弾・ホーミング・レーザーといった切り替えが可能となります。
これは複数人で開発する際やエディタを拡張した際、少ない手順で様々なパラメータの敵を量産する際に便利です。逆に、複数種類の敵に同じ攻撃プレハブを渡したいケースでも参照関係を構築したScriptableObject1個渡せば済むので、楽に参照関係を構築出来ます。
さらにScriptableObjectは所謂ポリモーフィズムも可能で、クラスをオーバーライドして動作自体を上書きし、特殊な挙動を行う敵を用意する…といった事も可能になっています。
ドラッグ&ドロップが面倒な場合は、Resourcesフォルダから取得するような形で実装すると、登録の自動化等も可能です。ApplicationSettingsのような形で使いたい場合は、こちらの方が良いかもしれません(どこからでも呼べるし)
他のパラメータ格納方法との違い
しかしパラメータを格納・取得するだけならば他の方法でも可能です。パッと思いつくだけでも以下の3つがあります。
GameObjectとの違い
まず単純に思いつく物としてGameObjectとの比較があります。正しくは「GameObjectコンテナに格納されたコンポーネントから値を取得する方法」との違いです。
GameObjectはシーンに所属するオブジェクトの為、複数のシーンやエディタ拡張に密接に接続することが出来ません。それはつまり、GameObjectの持つコンポーネントも複数シーンやエディタに密接に関わりにくい事を意味します。
ではプレハブはと言えば、プレハブをデータの格納に使用するのはTransformが無駄です。また専用のコールバックが不足しており、ScriptableObjectと比べて若干使いにくさがあります。
またGameObjectのインスタンス化は結構色々な事をしているっぽいので、量が増えてくるとScriptableObjectによる単純インスタンス化の方がパフォーマンス的に有利になるケースが出るかもしれません。
とは言え、とりあえず使うならGameObjectコンテナをプレハブ化したものでも問題はありません(どうせReadOnlyな使う方ですし、イベントとか無くても特に問題も)。複数のコンポーネントを詰め込める特徴もあるので、AssetStoreで販売するアセット等ではコチラのほうが都合が良いケースもあるかもしれません。
XML・JSON・CSVとの違い
テキスト・バイナリベース問わず、通常のシリアライザとの違いは、リソース参照の有無にあります。
ScriptableObjectやコンポーネントのSerializeFieldでシリアライズしたオブジェクトは、シーン内の大抵のアセットに対する参照を持つ事ができます。しかし汎用シリアライザの場合、こういった参照を持つ為にはResourcesから取得するコードを経由して呼び出す必要があります。
通常はそれで問題ありませんが、階層や参照の参照といったデータ構造を持ちたい場合、ScriptableObjectのUnityに特化した参照能力が役に立ちます。
またScriptableObjectはエンジン内部(C++)で実装されており、割と高速にデータを読み込めます。このため、EXCEL等の読込負荷が厳しくランタイムで読むのが難しいデータをScriptableObjectへ変換するワークフローを用意しておき、入力はExcel・読込は高速で効率的なScriptableObjectとするのは割と良い手だと思います。
なおデータの更新はAssetBundleのような物が必要になるので、基本的に更新できるパラメータの場合はScriptableObjectではなく汎用フォーマットの方が向いてます。
またセーブデータのように更新するデータに関しても、実行時は読み取り専用として動作するScriptableObjectよりテキストやバイナリの方が向いています。
ソースコードにハードコーディングする物との違い
ソースコードにデータをハードコーディングする事はCやC++では良くある事だし構造的に簡単で酔い感じなのですが、限度を超えるとバイナリサイズを超肥大化させ起動時間を伸ばす事になるケースが有ります。特にデータベースの中身をC#コードとして出力するようなマクロを組んでいる場合は注意が必要です。
また、少し異なるパラメータのオブジェクトを作る場合、その度にスクリプトを作る必要があります。特定の機能に特化した物を作る場合には問題無いですが、派生していくと若干面倒になっていきます。
ただ全データをメタデータ化(ScriptableObjectへ出力)するのはソースコード的に非常に見難い所があるので、その辺りはケースバイケースで選択するのが良さそうです。
なお、ScriptableObjectのような参照の共有は、serializeableとクラスの共有で似たような事が可能です。ただ、Inspectorの見た目的に「違うオブジェクトの持つ異なるパラメータ」に見えてしまうので、その辺り注意が必要です。下の画像にある2つの異なるコンポーネントに付与しているHogeオブジェクトは完全に同一のオブジェクトです。
Serialization Best Practices - Megapost
ld: Unable to insert branch island. No insertion point available. for architecture armv7
ScriptableObjectは静的なデータの変わりになるか?
エディタで操作するとScriptableObjectに登録したデータが反映し登録できているように見えてしまう為、ScriptableObjectはデータ保存の変わりに使えるように見えますが、結論を言うと出来ません。
ScriptableObjectのデータはゲームにビルドした際は静的な物であるため、ゲーム実行時にリセットされてしまいます。その為、セーブデータの保存には向きません。
ScriptableObjectを更新させない為にはOnEnableのイベントで
hideFlags = HideFlags.DontSave;
を付けるのが良さそうとありますが、正直エディタの場合は1個レイヤーを挟みInstantateで独立化してしまう方が健全な気がします。その辺りはあんまりイケてないので、ゲームのデータを保持するような用途では使用せず、ゲーム内では静的なパラメータ置き場としたほうが良いかもしれません。
またセーブデータの管理にはPlayerPrefsが手軽ですが、これ本来は名前の通り「プレイヤー環境設定」を保存する為の物です。簡単なパラメータやステータス等ではこれで十分ですが、セーブデータを保存したい場合はIO機能を使って保存するのが良さそうです。
例えば、PreviewLabs様のPlayerPrefs実装や、データをJSONとして保存する等々。
ScriptableObjectは正直無くても何とかなる物ですが、あると便利なケースがあるので覚えていても良いかな−程度の認識で良いかと。
ドット絵はえるるのだいあり様よりお借りしました。