【Unity(C#)】文字列リストをEnumっぽく使うエディタ拡張

##はじめに

Enumを毎回定義するのがめんどうなシチュエーションがありました。
例えば、事前にInspectorで登録しておいたマテリアル、あるいはResourcesからロードの中から
1つ選ぶようなシチュエーションです。

Enumで下記のように定義してInspectorで選択するようにすれば実装可能です。
もしくは辞書でMaterialとEnumを紐づけるとかもあるかと思います。

```C#:超簡易サンプル
enum UseMaterial
{
    Red,
    Blue,
    Green
}

public UseMaterial useMaterial = UseMaterial.Red;

private void ApplyMaterial()
{
    switch(useMaterial)
    //以下caseに応じた分岐処理
}

```

ただし、このやり方だとマテリアルを増やした際にEnumの定義を増やさないといけません。


そこで下記のようにマテリアルの名前リストからEnumっぽいプルダウンを出すエディタ拡張を作りました。
![StringListDemo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/609fc595-5f14-a039-6b57-d2268ada637f.gif)

プルダウンの中身が動的に反映されるのでコード変更の必要がなくなります。

## コード

今回はScriptableObjectでデータを定義し、そのデータをEditor拡張内でいじっていきます。

```C#
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

/// <summary>
/// 設定デモ用ScriptableObject
/// </summary>
[CreateAssetMenu(fileName = "MaterialSettings", menuName = "MaterialData/Demo")]
public class MaterialSettings : ScriptableObject
{
    /// <summary>
    /// Inspectorで設定するマテリアルのリスト
    /// </summary>
    [SerializeField,NonReorderable] private List<Material> _materialList;

    /// <summary>
    /// マテリアルのリスト
    /// </summary>
    public List<Material> MaterialList => _materialList;
    
    /// <summary>
    /// マテリアルの名前リスト
    /// </summary>
    [SerializeField,HideInInspector] public List<string> MaterialNameList = new List<string>();

    /// <summary>
    /// 選択したマテリアルの名前
    /// </summary>
    [SerializeField,HideInInspector] public string SelectedMaterialName;
    
    /// <summary>
    /// 選択したマテリアルのIndex
    /// </summary>
    [SerializeField,HideInInspector] public int SelectedMaterialIndex;
    
    private void OnValidate()
    {
        if(MaterialList != null) CreateMaterialNameList();
    }

    /// <summary>
    /// マテリアルの名前リストを作成する
    /// </summary>
    private void CreateMaterialNameList()
    {
        MaterialNameList.Clear();
        
        foreach (var material in MaterialList.Where(material=>material))
        {
            if (!MaterialNameList.Contains(material.name)) MaterialNameList.Add(material.name);
        }
    }
}
```

このコードによりメニューバー(あるいは右クリック)のCreateからScriptableObjectが作成できるようになります。
今回のデモではScriptableObjectを`Resources`に保存しておきます。

`NonReorderable`でリストの順番を固定しているのは、
動かす必要が無いからという理由と実装上都合が良かったからです。

実装上の都合についてですが、まずリストの値の変更をEditor拡張側にも伝えるために
OnValidate内で変更があるたびに新たにリストを作成しています。
しかし、リストのマテリアルを追加・削除した以外にも順番の入れ替えがOnValidateで呼ばれてしまいます。
後ほど出てくる`EditorGUILayout.Popup`と合わせると都合が悪いため順番は入れ替えられないようにしています。
(リストの順番を動かしてもエラーが出て壊れるわけじゃないので別に入れ替え可能のままでも良いと思います。)

---

次にエディタ拡張のコードです。

```C#:Editorフォルダに保存
using UnityEditor;

/// <summary>
/// 設定ファイルのInspectorを拡張
/// </summary>
[CustomEditor(typeof(MaterialSettings))]
public class ExtendEditor : Editor
{
    /// <summary>
    /// 設定
    /// </summary>
    private MaterialSettings _settings;

    /// <summary>
    /// 選択したマテリアルのインデックス
    /// </summary>
    private static int _selectedMaterialIndex;

    void OnEnable()
    {
        _settings = (MaterialSettings) target;
    }
    
    public override void OnInspectorGUI()
    {
        base.OnInspectorGUI();

        //変更があったときだけ
        if (EditorGUI.EndChangeCheck())
        {
            //リストが空の時、エラーを吐くのでここで拾う
            if (_settings.MaterialList == null || _settings.MaterialNameList.Count == 0)
            {
                EditorGUILayout.HelpBox("マテリアルを設定してください", MessageType.Error);
                return;
            }

            //リストの設定漏れは一番若い数字に
            if (_settings.MaterialNameList.Count <= _selectedMaterialIndex) _settings.SelectedMaterialIndex = 0;

            _selectedMaterialIndex = EditorGUILayout.Popup("使用するマテリアル", _settings.SelectedMaterialIndex, _settings.MaterialNameList.ToArray());
            
            //変更した値を設定側で保持
            _settings.SelectedMaterialName = _settings.MaterialNameList[_selectedMaterialIndex];
            _settings.SelectedMaterialIndex = _selectedMaterialIndex;

            Undo.RecordObject(_settings, "Settings Undo");
            EditorUtility.SetDirty(_settings);
        }
    }
}
```

マテリアル名前の文字列リストの中から選択したものを`EditorGUILayout.Popup`でプルダウン表示させています。
先ほどの`NonReorderable`でリストの順番を固定している理由は
このプルダウンが選択した要素のインデックスを返すからです。
リストの順番が変わると要素のインデックスも変わってしまうので考慮すると面倒でした。


プルダウンで選択した値はScriptableObjectの変数に保存します。

シリアライズされる状態にしたプロパティを`Undo.RecordObject`でUndoできるようになります。
`EditorUtility.SetDirty`でEditorを終了する際にも値を保存します。

これらの処理を描画中ずっと繰り返すのは不安だったので
`EditorGUI.EndChangeCheck()`で変更時のみ処理が走るようにしました。(心なしか軽くなったような気が？)

---

最後に使う側です。

```C#
using System.Linq;
using UnityEngine;

/// <summary>
/// 選択した色を適用
/// </summary>
public class ApplySelectColor : MonoBehaviour
{
    void Start()
    {
        //マテリアル設定用ScriptableObject読み込み
        var materialSettings = Resources.Load<MaterialSettings>("MaterialSettings");
        
        //選択したマテリアル名取得
        var materialName = materialSettings.SelectedMaterialName;
        
        //マテリアル名から使用するマテリアル取得
        var useMaterial = materialSettings
            .MaterialList
            .Where(material => material)
            .FirstOrDefault(material => material.name.Equals(materialName));
        
        if (useMaterial == null)
        {
            Debug.LogError("Occlusion用のMaterialが正しく設定されていません。");
            return;
        }

        //マテリアル適用
        transform.GetComponent<MeshRenderer>().material = useMaterial;
    }
}
```

最近、社のツヨツヨエンジニアからLINQを教えてもらったのでちょっと使ってみました。

これで選んだマテリアルが反映されるようになりました。

![StringListDemo2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/c9577aee-f75a-96bf-76b7-3f0acc2804a8.gif)

##参考リンク
[【Unity】択一選択式プルダウンメニューを動的に変化させる【エディター拡張】](https://tsu-games.hatenablog.com/entry/dynamic-popup)
[UnityEditorのUndo/Redoシステムについて【解決編】【最新】のコピーそして完結](https://sassembla.github.io/Public/2015:09:17%203-14-23/2015:09:17%203-14-23.html)
[第12章　Undo について](http://49.233.81.186/undo.html)