ゲームを作っていると、いろんなパラメータが必要になってきますよね。
敵の最大体力、各ステージの制限時間、デフォルトの移動速度、最大で植えれる髪の毛の数……、などなど。
各キャラクターなど一つ一つ異なるパラメータを設定する必要があるときもあれば、共通で使いまわすパラメーターもありますよね。
今回は共通で使いまわすパラメータを、ScriptableObjectを使ったパラメータテーブルを利用する方法について紹介します。
僕がゲームジャムなどでよく使う手法の一つです。
この記事にはUnity5.5.2f1を使用しています。
- ScriptableObjectって何だろう
- ScriptableObjectを使っていいことあるの?
- ScriptableObjectを作ってみよう
- staticのInstanceプロパティを追加して、どこからでも参照できる形に
- アセットはオリジナルを使うかコピーを使うか
- このシステムの現在考えられる欠点
ScriptableObjectって何だろう
int
やVector3
などのパラメータを格納できるクラスをアセットデータとして利用できるものです。
詳しくは以下のリンクを参考にしてみてください。
ScriptableObjectを使っていいことあるの?
例えば、敵の最大体力が共通のものならば、各Prefabのコンポーネントにパラメータを持ってしまうと、敵の数分そのパラメータの容量が必要になってしまいます。
ScriptableObjectのテーブルから参照すれば、データは一つでOK。
他にも、複数人で作業している場合に、シーンのHierarchy上に配置していあるGameObject
にアタッチしているコンポーネントのパラメータを変更する際に、バッティングが起こってしまいデータのマージが困難になってしまうこともあると思います。
ScriptableObjectのテーブルを使えば、パラメータのみが別アセットに分離しているので、複数人作業時でもバッティングしにくいし、バッティングしたとしても、ScriptableObject
にはTransform
や別コンポーネントの情報が無いから、マージも比較的にやりやすいです。
特に後者の理由から、僕はよく利用しています。
ScriptableObjectを作ってみよう
ではでは、ScriptableObject
を作っていきましょう。
ParameterTable.cs
using UnityEngine; [CreateAssetMenu( menuName = "MyGame/Create ParameterTable", fileName = "ParameterTable" )] public class ParameterTable : ScriptableObject { } // class ParameterTable
まずは、これだけで空のScriptableObject
が作成出来ますよー。
ポイントは、GameObject
にアタッチするコンポーネントを作る際に継承するMonoBehaviour
クラスからではなく、ScriptableObject
クラスを継承すること。
そして、アトリビュートとしてCreateAssetMenu()
を記述している点です。
CreateAssetMenu()
を記述することで、Projectビュー右クリック時のメニューに、指定したクラスのScriptableObject
を作成する項目を手っ取り早く追加しています。
他の作り方はこちらのリンクを参考にしてみてください。
https://anchan828.github.io/editor-manual/web/scriptableobject.htmlanchan828.github.io
あとはScriptableObject
を継承したクラスに、Public
や[SerializeField]
で宣言した変数を追加することで、作成されたScriptableObject
のアセットにもパラメータが追加されます。
ParameterTable.cs
using UnityEngine; [CreateAssetMenu( menuName = "MyGame/Create ParameterTable", fileName = "ParameterTable" )] public class ParameterTable : ScriptableObject { [SerializeField] public int m_maxLife = 100; [SerializeField] public Vector3 m_defaultPosition = Vector3.zero; [SerializeField] public float m_maxTime = 180.0f; } // class ParameterTable
staticのInstanceプロパティを追加して、どこからでも参照できる形に
先ほど作ったScriptableObject
のデータを使う場合、以下のように変数で所持して、Editor上でScriptableObject
のアセットをアタッチする方法があります。
Test.cs
public class Test : MonoBehaviour { [SerializeField] private ParameterTable m_table = null; }
確かにこの方法で問題なく使えますし、複数ScriptableObject
のアセットがあり、ものによってアタッチするアセットを変えるときは有効だと思います。
ただ、僕はScriptableObject
のアセットをアタッチするのさえ面倒なんだ!
という訳、ScriptableObject
のクラス上にstatic
でデータにアクセスできるプロパティを追加します。
using UnityEngine; [CreateAssetMenu( menuName = "MyGame/Create ParameterTable", fileName = "ParameterTable" )] public class ParameterTable : ScriptableObject { private static readonly string RESOURCE_PATH = "ParameterTable"; private static ParameterTable s_instance = null; public static ParameterTable Instance { get { if( s_instance == null ) { var asset = Resources.Load( RESOURCE_PATH ) as ParameterTable; if( asset == null ) { // アセットが指定のパスに無い。 // 誰かが勝手に移動させたか、消しやがったな! Debug.AssertFormat( false, "Missing ParameterTable! path={0}", RESOURCE_PATH ); asset = CreateInstance<ParameterTable>(); } s_instance = asset; } return s_instance; } } [SerializeField] public int m_maxLife = 100; [SerializeField] public Vector3 m_defaultPosition = Vector3.zero; [SerializeField] public float m_maxTime = 180.0f; } // class ParameterTable
これでParameterTable.Instance
経由で各パラメータを取得することが出来るようになりました。
ただ、ScriptableObject
クラスに直接Instance
を記述せず、このScriptableObject
のアセットを取得するする用のstatic
やシングルトンのクラスを作ってそちらにアクセスするようにし、ScriptableObject
クラスにはパラメータのみ記述するほうが、役割がきちんとなるような気もします。
今回は新しいクラスを作るのがめんどいという考えの名のものとに突き進んでいます。
アセットはオリジナルを使うかコピーを使うか
Instance
プロパティ内で、アセットを読み込む際にResources.Load()
を使う形で記述しました。
この部分はInstantiate( Resources.Load() )
に差し替えることも出来ます。
この二つの違いは、アセットのオリジナルを使うかコピーを使うかです。
Resources.Load()
を使ってオリジナルを使った際は、Editor実行中にアセットの値を書き換えた場合にも、現在のゲーム中に適応されます。
Instantiate( Resources.Load() )
を使ってコピーを使った際は、Editor実行中にアセットの値を書き換えた場合には、現在のゲーム中に適応されません。
以下のスクリプトを使って検証してみます。
TestComponent.cs
using UnityEngine; using UnityEngine.UI; public class TestComponent : MonoBehaviour { [SerializeField] private Text m_uiText = null; void Update() { m_uiText.text = ParameterTable.Instance.m_maxLife.ToString(); } } // class TestComponent
オリジナルをそのまま使ったほうが、Editor実行中に値が更新されるからお得じゃん!
確かにそのほうがパラメータ設定のイテレーション効率が上がると思います。
ただ、オリジナルをそのまま使う場合に気を付けねばならない点は、アセットのパラメータに値を書き込んだ場合です。
テスト用のコンポーネントを少し変更します。
TestComponent.cs
using UnityEngine; using UnityEngine.UI; public class TestComponent : MonoBehaviour { [SerializeField] private Text m_uiText = null; void Update() { if( Input.GetKeyDown( KeyCode.A ) ) { ParameterTable.Instance.m_maxLife = 10; } if( Input.GetKeyDown( KeyCode.S ) ) { ParameterTable.Instance.m_maxLife = 30; } if( Input.GetKeyDown( KeyCode.D ) ) { ParameterTable.Instance.m_maxLife = 50; } m_uiText.text = ParameterTable.Instance.m_maxLife.ToString(); } } // class TestComponent
そう、オリジナルをそのまま使っている場合に、値を書き込んだ場合はアセットの値がそのまま書き換わってしまうので注意が必要です。
Editorの実行を終了した際に、実行開始時の値に戻ることはありません。
これはEditorで実行時のみで、ipaやapkなどビルド後のものに関しては、書き換えたところで再起動時には元に戻っています。
そのためセーブデータみたいな使い方はできないので悪しからず。
まあ、そもそもアセットのパラメータは参照するだけの使い方のほうがいいと思うので、値を設定させないような作りにしてあげたほうがいいと思いますけどね。
以下のような。
[SerializeField] private int m_maxLife = 100; public int MaxLife { get { return m_maxLife; } } [SerializeField] private Vector3 m_defaultPosition = Vector3.zero; public Vector3 DefaultPosition { get { return m_defaultPosition; } } [SerializeField] private float m_maxTime = 180.0f; public float MaxTime { get { return m_maxTime; } }
ただ、それでも複数人で作業やる場合、その辺の知識の共有をしっかりしておかないと惨事になってしまう場合もあります。
オリジナルを使うかコピーを使うかはあなた次第です。
このシステムの現在考えられる欠点
ParameterTable.Instance
内のコメントにもあるのように、アセットのパスがスクリプトに直書きなので、アセットの場所を変えられると途端に動かなくなってしまいます。
一応Assert()
とデフォルトのクラス生成でごまかしてはありますが……。
この辺をどうしたものか……。
Editor中ならAssetフォルダから全検索して探すなど方法はあるのですが、ビルド時に指定した場所にアセットが無ければ終わりだもんなー……。
まだまだ改良する点は多くありそうですね。
何かの参考になれば幸いです。