【Unity】今更ScriptableObject入門

# 最初に

どうも、[ろっさむ](https://twitter.com/4_mio_11)です。

今回は「Unityを使っているなら皆知ってるよね？え？知らないの？なんで？」レベルの機能である「ScriptableObject」についてまとめていこうと思います。
僕は知りませんでした。

# ScriptableObjectとは

**ゲームやアプリの中で変化せず、あちこちで共用するデータを格納する時に便利なクラス**です。例えば、敵のパラメータがよく例として挙げられています。

**個別のゲームオブジェクト等にアタッチはせず、都度アセットをロードして使用する**ことになります。なので余計なオブジェクトがなく、エンジン側からのコールバックを殆ど受け取りません。

敵Aのステータスとして仕様書側でHP100と決まっていた場合、コード上でHPを定義する場合に以下の手法があります（実際パラメータとなるとHPだけじゃなくて色んな情報が入ってきます）。

- ハードコーディング（クラス内に直接書く）

```C#
class EnemyA
{
    // これだとEnemyAのインスタンスの数だけHP分のメモリも確保されていく
    const int MaxHP = 100;
}
```

```C#
class EnemyA
{
    // これだとEnemyAが画面上にいなくてもメモリ上にはEnemyAのHP分のメモリは確保されてしまう
    public static int MaxHP = 100;
}
```

- CSVで定義して読み込む

````csv
// これもEnemyAのインスタンスの数分csvを読み込む必要があったり、csvをコードで読み取りやすい形式に変更が必要
敵名,MaxHP,xxx.....
Goblin,100,xxx.....
````

他にもJsonなどで実装できますが、もう一つの手法として`ScriptableObject`を知っておくと良いかもしれません。
`ScriptableObject`を継承したパラメータ定義用クラスを作成し、Unityのアセットとして扱うことで`EnemyA`のインスタンスがどれだけ作られてもパラメータの数値等を参照する際にはパラメータ定義用アセットを一つ見ればわかるようになります。また、`EnemyA`が出現しないマップなど、`EnemyA`が画面上に出てこない場合はそもそも読み込まなければ**メモリも確保されません**。つまり、無駄にメモリを確保しなくてもよくなるわけですね。また、パラメータ部分だけ別アセットとして用意している状態となるので、**コンフリクトもしづらく、値の調整も行いやすく**なります。

パラメータ調整の他にも表情のblendshapeの名前リストの格納や、UIに使用するテキストデータ等にも使用することができるでしょう。応用すればイベントシステムも作成することができるようです。

ScriptableObject自体はUnityエディタでもよく使用されているようなので、使いこなすことができれば様々な恩恵が受けられそうです。

また、`ScriptableObject` は一応ゲーム中に変化するデータを扱う事も出来ますが、実機ではゲームが終了した後にデータは全て初期値に戻るため、セーブデータ的な使い方はできません（イベント期間中のステータスバフなどなら使えるかもしれませんが）。逆にエディタ上で値を変更すると、そのままアセットに直保存されます。**「エディタ上で実行中にパラメータ等の値を変更してゲームバランスを調整する」**という工程がこの仕様によって非常に楽に進めることができます。

具体的には、アセットファイルへの書き込みは `AssetDatabase`クラスから行われ、スクリプト側からパラメータ等の値を変更した際には明示的に `AssetDatabase` の `SaveAsset()` を呼び出す必要があります。ただ、この`SaveAsset()`は実機での起動中は呼び出すことができません（UnityEditor.dllへの参照がないため）。

このように、マスターデータの更新の頻度がそこまで高くなく、ゲーム内に組み込みたい場合には `ScriptableObject` は役に立ちます。逆に、ソシャゲのようなマスターデータの更新頻度が高く、チート対策も必要で…という場合にはデータが必要になるタイミングで都度Json形式などでサーバからデータをDLする仕組みの方が安心できます。

では実際にどのように作成して、使えば良いのか、というところですが、こちらもそこまで難しくはありません。


# ScriptableObjectの使用方法

流れとしては

1. `ScriptableObject`派生クラスの作成
2. `ScriptableObject`派生のクラスをアセット化
3. パラメータを設定
4. `ScriptableObject`派生のクラスを使用

となります。
順番に見ていきましょう。

## ScriptableObject派生のクラスを作成

### 単体のデータの塊だけを扱う（Enemy1種類につき1つのAsset）

こちらは簡単です。`ScriptableObject`を派生したクラスを作成していきましょう。

```C#
using UnityEngine;

// CreateAssetMenu属性を使用することで`Assets > Create > ScriptableObjects > CreasteEnemyParamAsset`という項目が表示される
// それを押すとこの`EnemyParamAsset`が`Data`という名前でアセット化されてassetsフォルダに入る
[CreateAssetMenu(fileName = "Data", menuName = "ScriptableObjects/CreateEnemyParamAsset")]
public class EnemyParamAsset : ScriptableObject
{
    // データ群の先頭をstringにして名前等に設定するとInspectorで見たときに項目TOPに表示されるので見やすくなります。
    public string EnemyName = "スライム";
    
    // privateでも[SerializeField]をつけることでInspectorで確認できるようになります。
    [SerializeField]
    int MaxHP = 100;

    ...
}
```

### 複数のデータの塊を扱う（Enemy数種類を1つのAssetに含む）

もしもパラメータ自体が増えたり、複数種類のパラメータを一つのAssetの中に持ちたい場合には、別途データ用のstructやclasssを用意すると良いでしょう。

```C#
using UnityEngine;

[CreateAssetMenu(fileName = "Data", menuName = "ScriptableObjects/CreateEnemyParamAsset")]
public class EnemyParamAsset : ScriptableObject
{
    public List<EnemyParam> EnemyParamList = new List<EnemyParam>();
}

// System.SerializeField属性を使用することで、Inspector上で変更した値がアセットに保存されるようになります
[System.Serializable]
public class EnemyParam
{
    public string EnemyName = "スライム";

    [SerializeField]
    int MaxHP = 100;

    ...
}
```

## ScriptableObject派生のクラスをアセット化

先ほどのコードであれば、Editor上から`Assets > Create > ScriptableObjects > CreasteEnemyParamAsset`という項目を押すことで`EnemyParamAsset`が`Data`という名前でアセット化されてassetsフォルダに入ります。

ただ、もし外部ファイル(json,csv...etc)からパラメータを読み込んで`ScriptabeObjects`側に流し込む場合には、アセット化する機能を持つクラスと`ScriptableObjects`派生クラスで分けて制作していく流れになるかと思います。

```C#
using UnityEngine;

public class EnemyParamAsset : ScriptableObject
{
    public List<EnemyParam> EnemyParamList = new List<EnemyParam>();
}

[System.Serializable]
public class EnemyParam
{
    public string EnemyName = "スライム";

    [SerializeField]
    int MaxHP = 100;

    ...
}
```

```C#
using System.IO;
using UnityEditor;
using UnityEngine;

// AssetDatabaseを使用しているため、ビルド時には含めないようにしないとビルドエラーが起きる
#if UNITY_EDITOR
public static class CreateEnemyParamDataAssetFromCsv
{
    private const string AssetPath = "Assets/Resources/Data/Enemy/";
    private const string CsvPath = "Assets/Data/Status/Enemy/xxxx.csv"

    // MenuItem属性を付けることでEditorの上部メニューに`ScriptableObjects > CreateEnemyParamAsset`が表示されます
    // 押下すると`CreateEnemyParamDataAsset()`が実行されます
    [MenuItem("ScriptableObjects/CreateEnemyParamAsset")]
    private static void CreateEnemyParamDataAsset()
    {
        var enemyParamAsset = CreateInstance<EnemyParamAsset>();
        
        // この辺で外部ファイルパスを用いてデータを読みこみ、
        // 作成したenemyParamAssetに値を流し込む処理を挟む....
        // 例えばenemyParamAsset.EnemyParamList.Add(hogeParam); 的な

        // 流し込んだ後は実際に作成します
        // ここで作ったアセットの置き場所であるパスの指定もできます
        var assetName = $"{AssetPath}{enemyType}Data.asset";
        AssetDatabase.CreateAsset(enemyParamAsset, assetName);

        // Asset作成後、反映させるために必要なメソッド
        AssetDatabase.Refresh();
    }
}
# endif
```

外部ファイルを読み込んで`ScriptableObjects`側に流し込む方法だと、外部ファイルの値がマスターデータ扱いで、`ScriptableObject`はそのマスターデータをゲームに流し込むためのクッション扱いになります。
この方法には以下のメリット・デメリットがあります。

メリット：

- 外部ファイルでの値がマスターデータとなり、**gitなどでの変更差分を確認することができる**。
- Editor上でアセット自体の値の書き換えを行うと即座にアセットに反映されて前の値は消されてしまうが、マスターデータは別にあるため、以前の値がわからなくなるということがなくなる。
- 複数人でのパラメータ調整がしやすくなる

デメリット：

- マスターデータから`ScriptableObject`に反映させるコード上の実装の手間がある
- Editor上でアセット自体の値の書き換えを行って調整した際に、値をマスターデータ側に反映させる作業が必要になる。

ちなみにcsv等の外部ファイルから`ScriptableObject`を作成する無料アセットも存在しているのでチェックしてみると良いかもしれません。


https://assetstore.unity.com/packages/tools/integration/csv-serialize-135763?aid=1101l4PmM&utm_source=aff
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/1c45f4b7-240f-0727-6ee8-33193ba324fc.png)



これでAssetが指定したフォルダ下に作られるようになりました！
後は実際にInspectorなどから必要に応じてアセットの値を直接変更できます。


## ScriptableObject派生のクラスを使用

あとは使い方ですが、こちらもシンプルに`Resources.Load`等でデータを取得して使用するだけです。

```C#
public class EnemyBase : MonoBehaviour
{
    public enum EnemyType
    {
         Goblin,
         ...
    }

    EnemyType enemyType;

    public void ReadEnemyDataAsset()
    {
        var path = $"Data/Enemy/{enemyType.ToString()}";
        var enemyData = Resources.Load(path) as EnemyParamAsset;

        // あとは読み込んだ値を使って諸々セットなどの処理を行う
        ...

    }
}
```

もしくは可能そうならEditor上で`ScriptableObject`のアセットをアタッチする方法もあります。


# 最後に

`ScriptableObject`は知ってみると非常に使い勝手が良く、Unityの実行速度にも優しい応用のきく機能です。
データ管理と使用は使えそうなら `ScriptableObject` を使用して効率的に行っていきましょう！


# 参考

- [ScriptableObjectについて](http://tsubakit1.hateblo.jp/entry/2014/07/24/030607)
- [第4章　ScriptableObject](http://49.233.81.186/scriptableobject.html)
- [【Unity】ScriptableObjectを使ってパラメータテーブル作るよ](https://www.urablog.xyz/entry/2017/03/28/235739)
- [ScriptableObject(スクリプタブル オブジェクト)とは【Unity】【ScriptableObject】](https://kan-kikuchi.hatenablog.com/entry/What%27s_Scriptableobject)
- [【Unity】ScriptableObjectにマスタデータを持たせるメリットについて](https://ekulabo.com/scriptableobject-for-master-data)
- [【Unity】ScriptableObjectってなんなん? って時に読む記事【解説】](https://ekulabo.com/about-scriptable-object)