[Unity] ScriptableObjectを用いたデータ管理

三回目の投稿です。よろしくお願いします。



# 目次
1. [概要](#概要)
2. [実装概要](#実装概要)
3. [作業手順](#作業手順)
4. [実装方法](#実装方法)
5. [最後に](#最後に)



# 概要
以下のことを解説します :
1. ScriptableObjectを用いたデータの作成方法
2. ScriptableObjectを用いたデータベースの作成方法
3. データベースを管理するクラスの作成方法と使用方法

以下のような利点があります : 
1. コードとデータが分離され、データの参照性と改変容易性が向上する。
2. データを一括で管理できる。(例 : 登録されているデータの名前一覧の取得が容易となる)
3. 上記より、ステータスやアイテム、ステージ、BGMなどの管理が容易になる。(アイテムショップでアイテムの名前を列挙するといった際にも有用)

:::note warn
ここで言う、データやデータベースとは"ScriptableObjectクラスから生成されるアセット"のことである。
:::


##### サンプル :
1. データの例 : 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/480241/1654d5de-882e-ff56-010f-3a0d4f0bc374.png)
2. データベースの例 : 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/480241/8d2e50dc-b3da-0aba-9682-3b294532b9a2.png)



# 実装概要

1. [データの作成方法の概要](#データ作成方法の概要)
2. [データベースの作成方法の概要](#データベースの作成方法の概要)
3. [データベースストアの作成方法の概要](#データベースストアの作成方法の概要)
4. [データの検索方法の概要](#データの検索方法の概要)


#### データの作成方法の概要
要点は「ScriptableObjectクラスを継承したクラスを作成し、扱いたい項目を記述し、実際にデータを作成すること」なので、最低限以下をすればよい：
1. 具体的なデータの作成の基盤となる抽象クラス(BaseData.cs)を作成する。
2. BaseData.csを継承したクラス(StatusData.cs)を作成する。
3. StatusData.csに扱いたい項目を記述する。
4. エディター上で実際にデータを作成する。

:::note warn
抽象クラスを作成しておくことで、様々(ステータス・アイテム・BGMなど)な具体的なクラスを作りやすくしている。
:::


#### データベースの作成方法の概要
要点は「ScriptableObjectを継承したクラスを作成し、実際にデータベースを作成し、データを登録すること」なので、最低限以下をすればよい：
1. 具体的なデータベース作成の基盤となる抽象クラス(BaseDataBase.cs)を作成する。
2. BaseDataBase.csを継承したクラス(StatusDataBase.cs)を作成する。
3. エディター上で実際にデータベースを作成し、データを登録する。


#### データベースストアの作成方法の概要
要点は「任意のクラスでデータベースを取得できるようにすること」なので、最低限以下をすればよい：
1. 具体的なデータベースストア作成の基盤となるデータベースクラス(BaseDataStore.cs)を作成する。
2. BaseDataStore.csを継承した具体的なクラス(StatusDataStore.cs)を作成する。
3. 空のゲームオブジェクトにStatusDataStore.csを付与し、StatusDataBaseデータベースを登録する。


#### データの検索方法の概要
要点は「データベースストアを取得し、そこに登録されているデータベースからデータを取得すること」なので、最低限以下をすればよい：
1. データを使用したいクラス内で、データベースストアを取得する。
2. "FindWithName()"もしくは"FindWithId()"を用いて、指定したデータを取得する。

:::note warn
"FindWithName()"や"FindWithId()"とは、BaseDataStore.csに定義されている関数で、名前もしくはIDからデータを取得する。
:::



# 作業手順
[1. データの作成の実装](#1-データの作成の実装)
　1.1. BaseData.csの作成
　1.2. StatusData.csの作成
　1.3. エディター上で、StatusData.csからデータ(ScriptableObjectアセット)を作成
　1.4. 作成したデータに値を入力
[2. データベースの作成の実装](#2-データベースの作成の実装)
　2.1. BaseDataBase.csの作成
　2.2. StatusDataBase.csの作成
　2.3. エディター上で、StatusDataBase.csからデータベース(ScriptableObjectアセット)を作成
　2.4. データベースに1.3で作成したデータを登録
[3. データベースストアの作成の実装](#3-データベースストアの作成の実装)
　3.1. BaseDataStore.csの作成
　3.2. StatusDataStore.csの作成
　3.3. 空のゲームオブジェクトを作成し、それにStatusDataStore.csを付与し、そこのデータベースとして2.3で作成したデータベースを登録
[4. データの検索の実装](#4-データの検索の実装)
　4.1. データを使用するクラスを作成し、StatusDataStoreを取得し、そこに登録されているデータベースから指定したデータを取得する。
 
 

# 実装方法

#### 1. データの作成の実装
##### 1.1. BaseData.csの作成
以下のようにBaseData.csを作成する：
<details><summary>BaseData.cs (tap)</summary>

```
using UnityEngine;

namespace App.BaseSystem.DataStores.ScriptableObjects
{
    /// <summary>
    /// ScriptableObjectで管理されるデータの基盤
    /// </summary>
    public abstract class BaseData : ScriptableObject
    {
        public string Name
        {
            get => name;
            set => name = value;
        }
        [SerializeField]
        private new string name;

        public int Id => id;
        [SerializeField]
        private int id;
    }
}
```

</details>

:::note info
ポイント：
* ScriptableObjectクラスを継承することで、このクラスをScriptableObjectとしている。
* 名前やIDといった、データ管理に必要となるものを予め定義しておくで、継承したクラスでは、差分のみを記述すればよくなる。
* [SerializeField]属性でエディターでの書き換えを可能とし、プロパティを用いることでクラス間での受け渡しを可能としている。
:::


##### 1.2. StatusData.csの作成
以下のようにStatusData.csを作成する：
<details><summary>StatusData.cs (tap)</summary>

```
using UnityEngine;

namespace App.BaseSystem.DataStores.ScriptableObjects.Status
{
    /// <summary>
    /// ステータスを持つオブジェクトのデータ群 (対象: プレイヤー、敵、破壊可能オブジェクトなど)
    /// </summary>
    [CreateAssetMenu(menuName = "ScriptableObject/Data/Status")]
    public class StatusData : BaseData
    {
        public int Level
        {
            get => level;
            set => level = value;
        }
        [SerializeField]
        private int level;

        public int Exp
        {
            get => exp;
            set => exp = value;
        }
        [SerializeField]
        private int exp;

        public int MaxHp
        {
            get => maxhp;
            set => maxhp = value;
        }
        [SerializeField]
        private int maxhp;

        public int Hp
        {
            get => hp;
            set => hp = Mathf.Clamp(value, 0, maxhp); // 0 <= hp <= maxhp
        }
        [HideInInspector]
        [SerializeField]
        private int hp;

        public int MaxMp
        {
            get => maxmp;
            set => maxmp = value;
        }
        [SerializeField]
        private int maxmp;
        
        public int Mp // use only player
        {
            get => mp;
            set => mp = Mathf.Clamp(value, 0, maxmp); // 0 <= mp <= maxmp
        }
        [HideInInspector]
        [SerializeField]
        private int mp;

        public int Attack
        {
            get => attack;
            set => attack = value;
        }
        [SerializeField]
        private int attack;

        public int Defence
        {
            get => defence;
            set => defence = value;
        }
        [SerializeField]
        private int defence;
    }
}
```

</details>

:::note info
ポイント：
* [CreateAssetMenu]属性を用いることで、エディター上にScriptableObjectアセットを作成できるようにしている。
* データの基盤となるBaseDataクラスを継承しているので、ScriptableObjectである。
* BaseDataに含まれるnameやidに加え、level、exp、hpなどのステータス管理に使用する値を新規で定義している。
:::

##### 1.3. エディター上で、StatusData.csからデータ(ScriptableObjectアセット)を作成
以下のようにエディターのProjectフォルダ内で右クリックを押し、Create/ScriptableObject/Data/StatusよりScriptableObjectアセットを作成する : 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/480241/4fa7cce2-25d7-5446-eba5-a8b9b7acb283.png)


##### 1.4. 作成したデータに値を入力
以下のように作成したデータに好みの値を入力する : 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/480241/1654d5de-882e-ff56-010f-3a0d4f0bc374.png)


#### 2. データベースの作成の実装
##### 2.1. BaseDataBase.csの作成
以下のようにBaseDataBase.csを作成する：
<details><summary>BaseDataBase.cs (tap)</summary>

```
using System.Collections.Generic;
using UnityEngine;

namespace App.BaseSystem.DataStores.ScriptableObjects
{
    /// <summary>
    /// データベースの基盤
    /// </summary>
    public abstract class BaseDataBase<T> : ScriptableObject where T : BaseData
    {
        public List<T> ItemList => itemList;

        [SerializeField]
        private List<T> itemList = new List<T>(); // T型データベース
    }
}
```

</details>

:::note info
ポイント：
* ScriptableObjectクラスを継承することで、このクラスをScriptableObjectとしている。
* ジェネリッククラスにすることで、任意のBaseDataクラスを継承したクラスをデータの対象としている。
* リストを用いて、任意の個数のデータを登録できるようにしている。
* ItemList変数により、すべてのデータを一括で取得できるようにしている。
:::


##### 2.2. StatusDataBase.csの作成
以下のようにStatusDataBase.csを作成する：
<details><summary>StatusDataBase.cs (tap)</summary>

```
using UnityEngine;

namespace App.BaseSystem.DataStores.ScriptableObjects.Status
{
    [CreateAssetMenu(menuName = "ScriptableObject/DataBase/Status")]
    public class StatusDataBase : BaseDataBase<StatusData> { }
}
```

</details>

:::note info
ポイント：
* [CreateAssetMenu]属性を用いることで、エディター上にScriptableObjectアセットを作成できるようにしている。
* データベースの基盤となるBaseDataBaseクラスを継承しているので、ScriptableObjectである。
* BaseDataBaseのT型としてStatusDataを適用することで、StatusData専用のデータベースとしている。
:::


##### 2.3. エディター上で、StatusDataBase.csからデータベース(ScriptableObjectアセット)を作成
以下のようにエディターのProjectフォルダ内で右クリックを押し、Create/ScriptableObject/DataBase/StatusよりScriptableObjectアセットを作成する : 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/480241/fd7f5382-0283-1e9d-c9c5-3272bd4495fa.png)


##### 2.4. 作成したデータベースにデータを登録
以下のように2.3で作成したデータベースに、1.4で作成したデータなどを登録する : 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/480241/8d2e50dc-b3da-0aba-9682-3b294532b9a2.png)

#### 3. データベースストアの作成の実装
##### 3.1. BaseDataStore.csの作成
以下のようにBaseDataStore.csを作成する：
<details><summary>BaseDataStore.cs (tap)</summary>

```
using UnityEngine;

namespace App.BaseSystem.DataStores.ScriptableObjects
{
    /// <summary>
    /// データベースを外部から参照できるようにする
    /// </summary>
    public abstract class BaseDataStore<T, U> : MonoBehaviour where T : BaseDataBase<U> where U : BaseData
    {
        public T DataBase => dataBase;
        [SerializeField]
        protected T dataBase; // エディターでデータベースを指定

        /// <summary>
        /// 文字列を用いてデータベース内のデータを取得
        /// </summary>
        public U FindWithName(string name)
        {
            if (string.IsNullOrEmpty(name)) { return default; } // null回避

            return dataBase.ItemList.Find(e => e.name == name);
        }

        /// <summary>
        /// idを用いてデータベース内のデータを取得
        /// </summary>
        public U FindWithId(int id)
        {
            return dataBase.ItemList.Find(e => e.Id == id);
        }
    }
}
```

</details>

:::note info
ポイント：
* ジェネリッククラスにすることで汎用性を増している。
* [SerializeField]によりエディター上からデータベースを設定でき、DataBaseにより参照できるようにしている。
* FindWithName()やFindWithId()により、名前やIDを用いて指定したデータを取得できるようにしている。
:::


##### 3.2. StatusDataStore.csの作成
以下のようにStatusDataStore.csを作成する：
<details><summary>StatusDataStore.cs (tap)</summary>

```
namespace App.BaseSystem.DataStores.ScriptableObjects.Status
{
    public class StatusDataStore : BaseDataStore<StatusDataBase, StatusData> { }
}
```

</details>

:::note info
ポイント：
* BaseDataStoreのT型としてStatusDataBaseを、U型としてStatusDataを適用することで、StatusDataBase専用のデータベースストアとしている。
:::


##### 3.3. 空のゲームオブジェクトを作成し、それにStatusDataStore.csを付与し、そこのデータベースとしてStatusDataBaseのScriptableObjectを登録
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/480241/44955270-28f5-9f6e-f8b5-f4d7d529e63e.png)
(プレハブ化するかは好み)

:::note info
ポイント：
これによって、Object.FindObjectOfType\<StatusDataStore>()でStatusDataStoreが取得できるようになる。
:::


#### 4. データの検索の実装
##### 4.1. データを使用するクラスを作成し、StatusDataStoreを取得し、そこに登録されているデータベースから指定したデータを取得する。
以下のようなクラスを作成し、ゲームオブジェクトに付与することで、指定したデータを取得できる : 
<details><summary>DataBaseTest.cs (tap)</summary>

```
using App.BaseSystem.DataStores.ScriptableObjects.Status;
using UnityEngine;

public class DataBaseTest : MonoBehaviour
{
    private StatusDataStore statusDataStore;

    private void Awake()
    {
        statusDataStore = FindObjectOfType<StatusDataStore>();
    }

    private void Start()
    {
        var foxData = statusDataStore.FindWithName("Fox"); // 名前が"Fox"であるデータを取得
        Debug.Log($"{foxData.Name} : (HP : {foxData.MaxHp}, ATK : {foxData.Attack}, DEF : {foxData.Defence})"); // データの出力
    }
}
```

</details>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/480241/b67101b9-183d-7b0a-8df4-1edd22acac1a.png)
(1.4で作成した"Fox"に関するデータを取得し、その中のデータの一部を出力)

また、以下のように全データの名前一覧を取得することもできる : 
<details><summary>DataBaseTest.cs (tap)</summary>

```diff_c_sharp
using App.BaseSystem.DataStores.ScriptableObjects.Status;
using UnityEngine;
+ using System.Linq;

public class DataBaseTest : MonoBehaviour
{
    private StatusDataStore statusDataStore;

    private void Awake()
    {
        statusDataStore = FindObjectOfType<StatusDataStore>();
    }

    private void Start()
    {
        var foxData = statusDataStore.FindWithName("Fox"); // 名前が"Fox"であるデータを取得
        Debug.Log($"{foxData.Name} : (HP : {foxData.MaxHp}, ATK : {foxData.Attack}, DEF : {foxData.Defence})"); // データの出力

+       var nameList = statusDataStore.DataBase.ItemList.Select(data => data.Name).ToList(); // 全データの名前一覧
+       Debug.Log($"名前一覧 : {string.Join(", ", nameList)}"); // データの出力
    }
}
```

</details>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/480241/acfa0f18-c2f4-7183-8782-fbd056bc0e53.png)



# 最後に
[前回](https://qiita.com/marthallll/items/8481de5c863c30a734e3)から一年ほど空いてしまいましたが、気が向いたので役に立ちそうな記事を書いてみました。データベースもセーブデータと同様にアプリ開発においてほぼ必須となる技術のように思います。
データベースを作る利点はマスターデータを作成する場合に内部実装を気にしなくてもよくなるという点です。コードに直接値を代入することはデータの参照や閲覧のしにくさ、コードの冗長化のような管理面でのデメリットが多いです。また、プログラマーでなくとも編集しやすいというメリットもあります(より快適さを求めるなら、[Googleスプレッドシートからデータを流し込むといった方法](https://qiita.com/marthallll/items/ebee64b1ccec189900c9)もある)。
また、今回作成したStatusDataを用いることで、プレイヤーや敵のステータス管理が容易になることから、戦闘システムを作りやすくなるといったメリットもあります (実際に用いる際には、ScriptableObject.CreateInstance()を用いてコピーしたものを扱うようにした方が良い (値の書き換えによりマスターデータ側が書き換えられてしまうため))。
また気が向いたら何か書きます。それでは。

宣伝 : CUPLEXという時間差アクションパズルゲームを作成しています。私は主にプログラムとプロジェクト管理を担当しています。よかったら、覗いてみてください↓
https://store.steampowered.com/app/2499100/CUPLEX/