うら干物書き

ゲームを作っています。

【Unity】ScriptableObjectを使ってパラメータテーブル作るよ

 ゲームを作っていると、いろんなパラメータが必要になってきますよね。
 敵の最大体力、各ステージの制限時間、デフォルトの移動速度、最大で植えれる髪の毛の数……、などなど。
 各キャラクターなど一つ一つ異なるパラメータを設定する必要があるときもあれば、共通で使いまわすパラメーターもありますよね。
 今回は共通で使いまわすパラメータを、ScriptableObjectを使ったパラメータテーブルを利用する方法について紹介します。

 僕がゲームジャムなどでよく使う手法の一つです。


この記事にはUnity5.5.2f1を使用しています。

ScriptableObjectって何だろう

 intVector3などのパラメータを格納できるクラスをアセットデータとして利用できるものです。
 詳しくは以下のリンクを参考にしてみてください。

docs.unity3d.com

tsubakit1.hateblo.jp

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()を記述している点です。

docs.unity3d.com

 CreateAssetMenu()を記述することで、Projectビュー右クリック時のメニューに、指定したクラスのScriptableObjectを作成する項目を手っ取り早く追加しています。

f:id:urahimono:20170328234954p:plain
f:id:urahimono:20170328235004p:plain

 他の作り方はこちらのリンクを参考にしてみてください。

anchan828.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

f:id:urahimono:20170328235037p:plain

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

f:id:urahimono:20170328235143g:plain
f:id:urahimono:20170328235219g:plain

 オリジナルをそのまま使ったほうが、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

f:id:urahimono:20170328235303g:plain
f:id:urahimono:20170328235347g:plain

 そう、オリジナルをそのまま使っている場合に、値を書き込んだ場合はアセットの値がそのまま書き換わってしまうので注意が必要です。
 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フォルダから全検索して探すなど方法はあるのですが、ビルド時に指定した場所にアセットが無ければ終わりだもんなー……。

 まだまだ改良する点は多くありそうですね。
 何かの参考になれば幸いです。