【Unity】ScriptableObjectってなんなん? って時に読む記事【解説】
- 2019.01.30
- ScriptableObject
- ScriptableObject, Unity
Unityを使っていると、いつかは行き当たるScriptableObject(スクリプタブル オブジェクト)の存在。
「ScriptableObjectを使いこなせると便利だぜ!」なんて声は数あれど、初心者のうちは手を出しづらい分野だと思います。
共有データとしてマスタデータを保持するために使用したり、設定用のパラメータを保持したりと、結構使える幅が広いので、ちょっとずつでも触れてみるといいかも。
という訳で、ここではScriptableObjectの概要と、ScriptableObjectの作り方の簡単なチュートリアルを扱います。
環境
macOS 10.14 Mojave
Unity2018.2.20f1
偉大なる先達
エディタ拡張を学ぶならエディタ拡張入門です。この中にScriptableObjectについての記述もあります。
テラシュールブログさん、いつも(一方的に)お世話になっています。ScriptableObjectについてはここで学びました。
kanさんのブログがとても分かりやすくまとまっています。
先人たちの知恵を拝借しつつ、ScriptableObjectのチュートリアルにつなげていきます。今回はScriptableObjectそのものについての説明と、実際に作ってみるところまで。
ScriptableObjectとは
ScriptableObjectとは、特定のゲームオブジェクトに紐付かない共有データを格納するのに使われるクラス、およびそのインスタンスとして作成されるアセットのこと。直訳すればスクリプティング可能なオブジェクト。いや、訳せてないな。
Unityで何らかのオブジェクトを扱う場合はUnityEngineの下にいるObjectクラスを拡張して使っています。例えば「新しくスクリプトを作ってゲームオブジェクトにアタッチしましょう!」といった時にはMonoBehaviourを継承したスクリプトをアタッチしているのですが、このMonoBehaviourの先祖にはObjectクラスがいます。
Object
↑
Component
↑
Behaviour
↑
MonoBehaviour
(ここにクラス図の継承関係が書かれているものとする)
Componentクラスおよびその派生クラスはゲームオブジェクトにアタッチする目的で使われています。大体はこれで十分なのですが、ゲームオブジェクトにアタッチしなくてもいいような、むしろしない方がいいようなオブジェクトも存在します。
冒頭に挙げた共有データが良い例で、後述する敵キャラのパラメータのように、複数のオブジェクトで使いまわしたいデータの場合は、ゲームオブジェクトにアタッチするよりアセットとして切り出しておいた方が使いやすかったりします。
その場合にObjectを継承したScriptableObjectクラスを使うと便利。
Object
↑
ScriptableObject
(ここにもクラス図の継承関係が書かれているものとする)
このScriptableObjectを継承して自分のクラスを作り、そこにマスタデータや設定用のパラメータを持たせたりします。Scriptableの名前の通り、管理したいデータを自分で記述できるので自由度が高くて良いんですよね。
そして個別のゲームオブジェクトにアタッチせず、アセットファイルとして保持しておくことができます。
なお、Objectクラスをダイレクトに継承してアセットを作ろうとしてもUnity側で認識してくれないので注意。ユーザ側で扱うときはScriptableObjectを継承していないとダメみたい。
ScriptableObjectのメリット
例えばRPGを作る時に、敵キャラをインスタンス化する場合を考えます。
個々のオブジェクトにパラメータを設定していると、その分だけメモリを使用することになります。モンスターの種族が違う場合、例えば以下の表のような敵グループが登場する場合は、それぞれのコントローラ用スクリプトのフィールドに個別にパラメータを設定してもいいかもしれません。個人的にはこの量ですらScriptableObjectを使いたいですが、例として。
名前 | 最大HP | 攻撃力 | 防御力 | 経験値 | ゴールド |
スライム | 4 | 2 | 2 | 1 | 2 |
ゴブリン | 8 | 4 | 2 | 2 | 3 |
ジャイアントマウス | 12 | 5 | 3 | 3 | 5 |
しかし、スライムが5匹出現する場合はどうでしょうか。
同じパラメータの敵キャラなのに、それぞれのインスタンスで個別にパラメータを持つと、5倍のメモリを使用します。
現在のHPやMPあたりは個別に持つ必要がありますが、攻撃力などの種族値は同じスライムなら一定なので、使いまわしたいですよね。
……なんだって? 5匹ならまだ大したことないって?
ほほう、そんな強情なあなたにはこの画像を見てもらいましょう。
その数どどーんと1000匹! 野原一面を埋め尽くすスライムの海!! キングダムハーツのハートレス1000体斬りを思い出します。
仮にパラメータ部分の使用メモリが10KBだったとして、1000匹ともなれば10MB。こうなると無視できないメモリ使用量です。
これをScriptableObjectを使ってアセットファイルとして定義しておけば、個々のインスタンスからはパラメータを持つScriptableObjectへの参照だけを持っておけばいいことになります。1000匹インスタンス化しても、パラメータ部分の使用メモリは参照されるScriptableObjectひとつ分の10KBで済みます。
……流石に1000匹をインスタンス化することはそうそうないですが、共通して使うデータを別に切り出しておくのは使用メモリの観点からメリットが大きいのです。
実際に作ってみる
やっぱり手を動かしてやってみるのが一番ですね。ScriptableObjectを継承したクラスを作って、アセットファイルの作成までやってみます。
いつものように右クリック -> [Create] -> [C# Script]でスクリプトファイルを作ります。
スクリプトファイル名は好きな名前で。作りたいクラス名と合わせておくのはMonoBehaviourの時と一緒です。今回は『EnemyData』にしてみました。
ファイルができたらテキストエディタを使ってファイルを開き、以下のように編集します。
クラス名の定義の前にAttributeを付けています。[CreateAssetMenu]を付けることで、このクラスのアセットをメニューから作成することができます。
menuNameにパスを設定することでその通りに表示してくれるため、自分で作ったScriptableObjectのメニューはまとめておくといい感じです。
クラスの継承元は、いつものMonoBehaviourからScriptableObjectに変更。これは上の『ScriptableObjectとは』で説明していた内容です。
それに伴い、Start()とUpdate()も消しておきます。これらのイベント関数はMonoBehaviourで定義されていることからScriptableObjectでは使えません。MonoBehaviourとは使えるイベント関数が異なるので、詳しくはスクリプトリファレンスをご参照ください。
あとはクラスの中に使いたいパラメータを定義します。外部のスクリプトからアクセスする場合はpublicで。私の場合はScriptableObjectの値は参照元のスクリプトから書き換えない自分ルールにしているのでpublicにしちゃってますが、スクリプトからの書き換えが心配な場合はフィールドに[SerializeField]を付けて、publicでGetHp()みたいなメソッドを用意しておくのもアリかも。ぶっちゃけめんどくさいですけどねいえ、何でもありません。
上記のスクリプトを保存し、コンパイルが終わるとメニューにアセット作成の項目が追加されます。Projectウィンドウで右クリック-> [Create] -> [MyScriptable] -> [Create EnemyData] と選択することでScriptableObjectのアセットファイルが作成されます。コードの中で記述した通りのパスになってますね。
上のメニューバーからも作成できます。
どちらの方法でもこのような外見のファイルが作成されます。今回はスライムのデータを入れたかったので『SlimeData』という名前にしてみました。
作成されたファイルを選択し、Inspectorウィンドウを見ると、スクリプトに記載したフィールドが作成されていることが分かります。ScriptableObject内で初期値を定義していない場合は型の初期値が入力された状態になります。
試しに上の表で決めたスライムのパラメータを入れてみました。
これで一通りの流れはOK。
念の為スライムにコントローラーを付けて、ScriptableObjectの値を表示してみます。
新しいスクリプトファイルを作成し、以下のような処理を記述しました。
Start()の中でShowScriptableObjectData()というメソッドを呼び出しています。
このShowScriptableObjectData()では、フィールドで参照されているEnemyDataが持つパラメータをコンソールに表示させています。
このスクリプトを保存した後、任意のゲームオブジェクトにアタッチし、ゲームを実行してみます。
するとコンソールに以下のメッセージが表示されました。
『私の名前はスライム, 最大HPは4, 攻撃力は2, 防御力は2, 経験値は1, ゴールドは2です。』と意図した通りに表示されています。よしよし、うまくいっていますね。
値自体はこのScriptableCharaControllerでは持たず、参照先のEnemyDataのフィールドにアクセスしています。なのでScriptableCharaControllerを持ったゲームオブジェクトが1000個あったとしても、EnemyDataが持っているデータだけあればいいのでメモリ使用量が増えずに済むのです。ScriptableObjectのちからってすげー!!
バランス調整に便利
このScriptableObject、実はテストプレイ中にも値を変更できるんです。
通常、MonoBehaviourを継承したスクリプトでInspectorに表示しているフィールドは、Unityエディタでゲームを実行中に値を変えても、ゲームを終了した時に元の値に戻ります。これはこれで便利なのですが、新しい値を保持したい時でもゲーム終了後に改めて入れ直す必要があるのでちょっと面倒。
しかしScriptableObjectの値は、Unityエディタでゲームを実行中にInspectorから変更したら、ゲームを終了させてもそのままになっています。Unityエディタで値を変えた場合には、アセットファイルの内容も書き換えてくれるんです。
これの何が嬉しいかって、タイトル通りバランス調整が楽になるんです。特にRPGなんかではバランス調整はかなり重要です。ロンダルキアの悲劇を繰り返してはならないのです。
例えば敵との戦闘中にダメージ量を調整したい場合、ScriptableObjectで作った敵のパラメータを変更すればすぐに反映されますし、その値を保持したままテストプレイを終了できます。
個々のインスタンスにパラメータを持たせず、参照だけ渡していたメリットもここで活きてきます。
セーブデータを記録するのには使えない
「ゲーム実行中に値を変えてそれが保持されるのであれば、セーブデータを記録するのに使えるのでは?」と思った方もいるかもしれませんが、残念ながらセーブ用途では使えません。
というのも、上でしつこく「Unityエディタで値を変えても」と言っていたように、ScriptableObjectから作られたアセットファイルの値はUnityエディタから変更できても、実機(iPhoneとかAndroidのスマホ、ゲーム機など)で実行された場合は変更できないんです。
厳密に言えばメモリに読み込んだ値は変更できるかもしれませんが、アセットファイルへの書き込みができないことから、再度ファイルから値を読み込んだ時にはビルドした時の値に戻っています。これではセーブデータを記録するのに使えません。
アセットファイルへの書き込みはUnityEditorの下にいるAssetDatabaseクラスから行われます。Inspectorから値を変更する場合は内部でアセットファイルへの変更を書き込んでくれている(っぽい)のですが、スクリプトから変更した場合には明示的にAssetDatabaseのSaveAssetメソッドを呼ばないとファイルに書き込んでくれません。
実機で呼ぼうにも、ビルド時にはUnityEditor.dllへの参照が行われないので、実機ではUnityEditorの下にあるAssetDatabaseのSaveAssetメソッドが呼べず、アセットファイルへの書き込みができないんですよね。無理やり持って行こうとしてもUnityEditorの名前解決ができなくてビルドがこけますし、持っていくのは無理っぽいです。
なのでセーブするならScriptableObjectを使う以外の方法がおすすめです。
まとめ
ScriptableObjectは特定のゲームオブジェクトにアタッチする必要のないデータを保持するのに便利なクラス、およびそのアセットファイルです。共有データなどに使うことで、メモリを効率的に使うことができます。
Unityエディタでゲームを実行している場合には、直接アセットファイルの値を書き換えることができ、ゲーム終了後にもその設定値が保持されるため、バランス調整がしやすい利点も。
ただし、実機ではアセットファイルへの書き込みができないため、セーブデータを記録する用途では使えないのでご注意を。
私は主にマスタデータを扱う時に使っています。エディタ拡張と組み合わせることでInspectorでの表示も自由にカスタマイズできるので便利です。
全力でオススメしたいScriptableObject、ぜひ使ってみてね。
ScriptableObjectにマスタデータを持たせる場合のメリット・デメリットについても検討してみました。
ゲーム開発の攻略チャートを作りました!
-
前の記事
今作っているUnity製アプリ『空とにゃんこと風船と』の紹介をさせてくださいな 2019.01.17
-
次の記事
【Unity】ScriptableObjectにマスタデータを持たせるメリットについて 2019.02.01
[…] 今回「ScriptableObject」というUNIT独自?の概念がでてきました。これについては多少理解する必要があると思います。「【Unity】ScriptableObjectってなんなん?って時に読む記事【解説】」を参考に基本的な知識を得ました。パラメタ設定など共有設定値を保持するには便利なクラスのようできっとUnity使っていく上では当たり前のモノなのでしょう。こういったモノはあまり深く考えずに利便性を享受したほうが無難です。 […]