Bocchi Games

Individual Game Developer in Japan

【Unity】Odin - Inspector and Serializerでマスターデータを作る!

この記事はアセットストア 真夏のアドベントカレンダー - 2019 Summer - 23日目の記事です。

ここでは、Odin - Inspector and Serializerを使ったマスターデータの作り方を紹介します。

例として、以下のようなモンスターデータを管理するデータベースを作成していきます。

モンスターデータベースの例

Odin - Inspector and Serializerとは

Unityのインスペクターの表示を簡単にカスタマイズできるアセットです。

本来、Unityのインスペクターをカスタマイズしようとすると、エディタ拡張という機能を使って専用のスクリプトを作らないといけないのですが、Odinは属性([SerializeField]とかでお馴染みの[ ]で囲まれたもの)を追加するだけで簡単にインスペクターをカスタマイズすることができます。

Odinの基本的な使い方はコガネブログさんの以下の記事がわかりやすいので、そちらをご覧ください。

baba-s.hatenablog.com

モンスタークラスを作る

まずはモンスターのパラメーターを表すクラスを作ります。

using Sirenix.OdinInspector;
using Sirenix.Serialization;
using UnityEngine;

[CreateAssetMenu]
public sealed class Monster : SerializedScriptableObject
{
    [OdinSerialize]
    public string Name { get; private set; }
    [OdinSerialize, PreviewField(Height = 128)]
    public Sprite Graphic { get; private set; }
    [OdinSerialize]
    public int Hp { get; private set; }
    [OdinSerialize]
    public int Mp { get; private set; }
    [OdinSerialize]
    public int Atk { get; private set; }
    [OdinSerialize]
    public int Def { get; private set; }
    [OdinSerialize]
    public int Agi { get; private set; }
    [OdinSerialize]
    public int Exp { get; private set; }
    [OdinSerialize]
    public int Gold { get; private set; }
}

Monsterのインスペクター表示

各パラメーターがpublicフィールドではなくプロパティになっていますが、OdinのSerializedScriptableObjectを使うことでプロパティもシリアライズできるようになります。

また、PreviewFieldでスプライトをインスペクターでプレビュー表示にしています。

モンスターデータベースクラスを作る

次にモンスターデータを保持するデータベースクラスを作ります。

using Sirenix.OdinInspector;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

#if UNITY_EDITOR
using UnityEditor;
#endif

[CreateAssetMenu]
public sealed class Monsters : ScriptableObject, IEnumerable<Monster>
{
    [SerializeField]
    [ListDrawerSettings(ShowIndexLabels = true, ListElementLabelName = "Name", CustomAddFunction = "CustomAddFunction",
        OnBeginListElementGUI = "OnBeginListElementGUI", OnEndListElementGUI = "OnEndListElementGUI", CustomRemoveIndexFunction = "CustomRemoveIndexFunction")]
    private Monster[] items;

    public Monster this[int index]
    {
        get { return items[index]; }
    }

    public IEnumerator<Monster> GetEnumerator()
    {
        return items.AsEnumerable().GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

#if UNITY_EDITOR
    private Monster CustomAddFunction()
    {
        var monster = CreateInstance<Monster>();
        AssetDatabase.AddObjectToAsset(monster, this);
        AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(this));
        return monster;
    }

    private void OnBeginListElementGUI(int index)
    {
        EditorGUI.BeginDisabledGroup(true);
    }

    private void OnEndListElementGUI(int index)
    {
        EditorGUI.EndDisabledGroup();
    }

    private void CustomRemoveIndexFunction(int index)
    {
        DestroyImmediate(items[index], true);
        items = items.Where((v, i) => i != index).ToArray();
        AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(this));
    }
#endif
}

Monstersのインスペクター表示

追加ボタンを押すと、MonsterクラスのScriptableObjectを作成してMonstersのScriptableObjectに内蔵、削除ボタンが押されたら対象のScriptableObjectをDestroyします。

MonsterクラスのScriptableObjectをMonstersに内蔵することで、プロジェクトにファイルが散らばらないようにしています。

モンスターデータベースを参照する方法

あらかじめ、モンスターデータベースをエディタ上のみ参照できるAssetDatabaseUtilという便利クラスを作っておきます。

using Sirenix.OdinInspector;
using System.Linq;

#if UNITY_EDITOR
using UnityEditor;
#endif

#if UNITY_EDITOR
public static class AssetDatabaseUtil
{
    public static Monsters LoadMonsters()
    {
        return AssetDatabase.LoadAssetAtPath<Monsters>("Assets/Monsters.asset");
    }

    public static ValueDropdownItem<int>[] LoadMonsterIds()
    {
        return LoadMonsters().Select((v, i) => new ValueDropdownItem<int>($"{i:D3} {v.Name}", i)).ToArray();
    }
}
#endif

シンプルに、モンスターデータベースとモンスターIDからモンスター名とHPを表示してみます。

using Sirenix.OdinInspector;
using UnityEngine;

public sealed class NewBehaviourScript : MonoBehaviour
{
    [SerializeField]
    private Monsters monsters;
    [SerializeField, ValueDropdown("@AssetDatabaseUtil.LoadMonsterIds()")]
    private int monsterId;

    private void Start()
    {
        var monster = monsters[monsterId];
        Debug.Log($"{monsterId:D3} {monster.Name} HP:{monster.Hp}");
    }
}

モンスターデータベースからモンスターパラメーターを取得

今回は、なるべくわかりやすくするためにデータベースの参照にシンプルな例を出しましたが、正直言うと、この方法微妙ですよね。

他に考えられる方法としては、

  1. データベースをシングルトンとかにして、どこからでも参照できるようにする

  2. コンストラクタや関数経由で、データベースorデータベースから取得したデータを渡す

  3. Zenjectでデータベースをインジェクトする

あたりでしょうか。

自分は3の方法をとってます。(また機会があれば、そのやり方を紹介します)

おわりに

最初はマスターデータの作成にテラシュールブログさんのExcel Impoter Makerを使ってExcelでやろうとしてました。

tsubakit1.hateblo.jp

しかしExcelだとクラスの入れ子が表現できなかったり、値域の定義の設定が面倒だったりで、なかなかしんどかったりします。

Odinでマスターデータを作れば、Excelでやるより柔軟なデータ構造が使えるようになって、個人的にはだいぶ楽になった気がします。