[Unity] [Editor拡張] EditorWindowを用いたデータの管理

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



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



# 概要
##### 解説内容 :
- ScriptableObjectを用いたデータの管理
- SerializedObjectの概要
- EditorWindowを用いたデータの管理

::: note warn
ここで、"データ"とは、エディタ上で管理される値で、ゲーム実行中は変更されないものを指す (マスターデータ)。
:::

##### 利点 :
- データの管理がEditorWindowで完結することによる、管理性の向上

::: note info
今回は、単一のデータの管理についてのみ言及するが、以下の実装を拡張することにより、複数のデータを一括で管理することが可能となる。
:::

##### 作成するもの :
- データを保持するScriptableObjectアセット
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/480241/2523d73f-dbcb-425c-8dae-858ba68a2d4b.png)

- データの設定ができるEditorWindow
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/480241/30a43370-3338-45ac-8491-2ed91d82c4c4.png)



# 実装概要
##### 1. LaunchModeSettings.csの作成
- データ(ゲーム起動時のモード)を定義する。
- エディタ上に、LaunchModeSettingsアセットの作成を可能とする。
- エディタ上に、LaunchModeSettingsアセットを作成する。


##### 2. SettingsTool.csの作成
- EditorWindow上で、ポップアップを用いて、起動モードの確認・変更を可能とする。


##### 3. Initializer.csを作成
- ゲーム開始時にデータの値を保持・反映する。
- エディタ上から、LaunchModeSettingsアセットを適用する。



# 実装方法
##### 1. LaunchModeSettings.csの作成
1.1. 以下のように、LaunchModeSettings.csを作成する :
<details><summary>LaunchModeSettings.cs (tap)</summary>

```
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif

namespace App.BaseSystem.DataStore.Settings
{
    [CreateAssetMenu(menuName = "MasterData/Settings/Launch Mode")]
    public class LaunchModeSettings : ScriptableObject
    {
        public enum LaunchModeType
        {
            Product = 0, // 製品版
            Demo,        // 体験版
            Debug,       // デバッグ用
        }

        public LaunchModeType LaunchMode => m_LaunchMode;
        [SerializeField, Header("起動モード")]
        private LaunchModeType m_LaunchMode = LaunchModeType.Product;

#if UNITY_EDITOR
        private const string ThisAssetPath = "Assets/.../LaunchModeSettings.asset";
        public static LaunchModeSettings GetThis() => (LaunchModeSettings)AssetDatabase.LoadAssetAtPath(ThisAssetPath, typeof(LaunchModeSettings)); // 自身をアセットとして取得
        public static string NameOfLaunchMode => nameof(m_LaunchMode);
#endif
    }
}
```

</details>

::: note info
- ScriptableObjectクラスを継承
- CreateAssetMenu属性 : エディタ上でアセットを作成可能としている。
- #if UNITY_EDITOR : エディタ上でのみ動作するコードを実装。
- AssetDatabase.LoadAssetAtPath() : 外部から参照しやすくしている。
:::

::: note warn
- NameOfLaunchMode : アセットの変更を適用するために、EditorWindow側で使用します。
:::

1.2. 以下の手順で、LaunchModeSettingsアセットを作成する :

1.2.1. エディタ上のProjectフォルダ内で左クリックを押し、メニューを表示
1.2.2. Create/MasterData/Settings/Launch Modeを選択し、アセットを作成
1.2.3. LaunchModeSettings.csのThisAssetPathの値を、アセットを作成した場所に変更する。

参考画像 : ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/480241/af6a7e50-cb11-40b2-b1cd-23e03b0fe613.png)


##### 2. SettingsTool.csの作成
以下のように、SettingsTool.csを作成する :
<details><summary>SettingsTool.cs (tap)</summary>

```
#if UNITY_EDITOR
using UnityEngine;
using UnityEditor;
using App.BaseSystem.DataStore.Settings;
using System.Collections.Generic;

namespace App.EditorExtension
{
    public class SettingsTool : EditorWindow
    {
        private readonly List<GUIContent> _guiContentForLaunchMode = new List<GUIContent>(); // ポップアップの内容
        private int _laucnModeIndex; // 選択中の値

        private LaunchModeSettings LaunchModeSettings => LaunchModeSettings.GetThis(); // LaunchModeSettingsアセットの取得

        [MenuItem("Tools/Settings Tool")]
        public static void OpenWindow()
        {
            GetWindow<SettingsTool>("Settings Tool");
        }

        private void OnEnable()
        {
            Initialize();
        }

        /// <summary>
        /// 初期化処理
        /// </summary>
        private void Initialize()
        {
            foreach (var e in System.Enum.GetValues(typeof(LaunchModeSettings.LaunchModeType))) // ポップアップの内容を生成
            {
                var content = new GUIContent(e.ToString());
                _guiContentForLaunchMode.Add(content);
            }
            _laucnModeIndex = (int)LaunchModeSettings.LaunchMode; // 現在の値を反映
        }

        /// <summary>
        /// 描画処理
        /// </summary>
        private void OnGUI()
        {
            DrawLaunchModeGUI();
        }

        /// <summary>
        /// 起動モードに関する描画処理
        /// </summary>
        private void DrawLaunchModeGUI()
        {
            GUILayout.Label("[起動モードの設定]");

            var index = EditorGUILayout.Popup(_laucnModeIndex, _guiContentForLaunchMode.ToArray());
            if ((int)LaunchModeSettings.LaunchMode == index) { return; }
            _laucnModeIndex = index;

            ApplyEnumValueIndex(new SerializedObject(LaunchModeSettings), LaunchModeSettings.NameOfLaunchMode, index);
        }

        /// <summary>
        /// 値の反映
        /// </summary>
        private static void ApplyEnumValueIndex(SerializedObject serializedObject, string propertyName, int value)
        {
            serializedObject.FindProperty(propertyName).enumValueIndex = value; // 
            serializedObject.ApplyModifiedProperties(); // 値をアセットへ反映
            AssetDatabase.SaveAssets();                 // アセットの保存
        }
    }
}
#endif
```

</details>

::: note info
- MenuItem属性, GetWindow() : エディタ上部のToolsタブから、EditorWindowを表示できるようにしている。
- GUILayout.Label() : 説明文の実装
- EditorGUILayout.Popup() : ポップアップの実装
- SerializedObject : これによって、LaunchModeSettingsアセットの内部の値を直接変更できるようになる。
- serializedObject.FindProperty(nameof(m_LaunchMode)).enumValueIndex : LaunchModeSettingsのm_LaunchMode(Enum)を参照している。
:::

::: note warn
SerializedObjectに関して :
- SerializedObjectとは、オブジェクトの値を直接編集可能なクラスで、エディタ上で予め値を設定しておきたい場合に使用することが多い。([スクリプトリファレンス](https://docs.unity3d.com/ja/560/ScriptReference/SerializedObject.html))
- アセットは、YAML言語で記述されており、エクスプローラーなどから直接ファイルを開くことで、その値を確認できる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/480241/a0fad5ec-ebfa-40df-9963-f2a51a68b97d.png)
(LaunchModeSettingsアセット, 起動モードの値は"m_LaunchMode")

- スクリプトに記載した変数を、アセットファイルに保存するためには、SerializeField属性を付与する必要がある。
:::


##### 3. Initializer.csの作成
3.1. 以下のように、Initializer.csを作成する :
<details><summary>Initializer.cs (tap)</summary>

```
using UnityEngine;
using App.BaseSystem.DataStore.Settings;

namespace App.BaseSystem
{
    [DefaultExecutionOrder(-10)]
    public class Initializer : MonoBehaviour
    {
        [SerializeField, Header("起動モードアセット")]
        private LaunchModeSettings m_LaunchModeSettings;

        public static LaunchModeSettings.LaunchModeType LaunchMode { get; private set; }

        private void Awake()
        {
            SetLaunchMode();
        }

        private void SetLaunchMode()
        {
            var mode = m_LaunchModeSettings.LaunchMode;
            LaunchMode = mode;
            Debug.Log($"起動モード -> {mode}");
        }
    }
}
```

</details>

::: note
- DefaultExecutionOrder属性 : 実行順を早めている
- LaunchMode : public staticとして保持しておくことで、様々な箇所から参照しやすくしている。
:::

::: note warn
- 実行順を早めることで、LaunchModeが参照された場合に、値の反映が間に合っていないというリスクを減らす。
:::

3.2. Initializer.csをオブジェクトに付与し、LaunchModeSttingsアセットを適用する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/480241/05944fc5-3f2f-4b54-9a20-146b42bfdc8a.png)



# 最後に
どうでしたか、分かりやすかったでしょうか。

データ管理の際に、我々が参照すべき対象をEditorWindowに集約することで、アセットを探す手間が減り、管理がしやすくなることで、ミスの低減が期待されます。また、管理するデータが増えた場合でも、SettingsTool.csを拡張することで、一括して複数のデータの管理ができます。
今回は、ゲーム起動時のデータを例に実装をしましたが、他のScriptableObjectを用いて管理されるデータ(例 : 敵のステータス, 音源データ, ショップのアイテム)に対しても、この実装は有用です (参考 : [ScriptableObjectを用いたデータ管理](https://qiita.com/marthallll/items/d2072a41e864bc62dacb))。

また気が向いたら何か書きます。それでは。



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