【Unity(C#)】ReorderableListでパラメータをひとまとめにする方法

##複数のパラメータをひとまとめにしたい！
だいぶ前に書いた[フェードイン、アウト](https://qiita.com/OKsaiyowa/items/85b167cccf811882bf1b)の記事をベースに話を進めます。

フェードに使うパラメータは

・フェードする際の色
・フェードにかかる時間
・フェード終了時の透明度

この3種類です。

上記のパラメータをひとまとめにして設定しやすくしようと頑張ってました。

もう少し詳しく順を追って説明します。

###Inspector上でパラメータを調節したい

```C#
    [SerializeField]
    Color panelColor;

    [SerializeField]
    float fadeTime;

    [SerializeField]
    float alpha_Panel;
```

![ExInspector.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/a59652bb-d628-498c-90f0-f2b2df3be00d.png)

これで可能です。
フェードのパターンが1種類であれば、これで十分です。

しかし、"フェードのパターンが複数ほしい"となりました。

###Inspector上で複数パターンのパラメータを調節したい
```C#
    [SerializeField]
    Color[] panelColor;

    [SerializeField]
    float[] fadeTime;

    [SerializeField]
    float[] alpha_Panel;
```
![ExInspector.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/00e4da1f-8428-eded-b139-fa72913d8dc2.png)


フェードのパターンが増え、それに応じてパラメータの数も増えます。
コードに落とし込むとこうなります。

```C#
using System.Collections;
using UnityEngine;
using UnityEngine.UI;

public class MyFade : MonoBehaviour
{
    [SerializeField]
    Color[] panelColor;

    [SerializeField]
    float[] fadeTime;

    [SerializeField]
    float[] alpha_Panel;

    [SerializeField, Tooltip("リスト内のテストしたい要素数を入力")]
    int debugElementNum;

    [SerializeField, Tooltip("デバッグ時はTrueに(OでFadeOut、IでFadeIn)")]
    bool debug;

    const float FIXEDUPDATE_DELTATIME = 0.02f;

    Image facePanel;

    Coroutine coroutine;


    void Awake()
    {
        //シーンをロードするたびに新しいカメラを生成
        if (GameObject.Find("OnlyUIRenderingCamera"))
        {
            Destroy(GameObject.Find("OnlyUIRenderingCamera"));
        }
    }


    void Start()
    {
        //カメラ自動生成
        GameObject camera_G = new GameObject("OnlyUIRenderingCamera");
        Camera faceCamera = camera_G.AddComponent<Camera>();
        faceCamera.clearFlags = CameraClearFlags.Depth;
        faceCamera.cullingMask = (1 << LayerMask.NameToLayer("UI"));

        //キャンバス生成＆設定
        GameObject canvas_G = new GameObject("FaceCanvas");
        Canvas faceCanvas = canvas_G.AddComponent<Canvas>();
        canvas_G.AddComponent<CanvasRenderer>();


        //キャンバスのポジションを調整
        Vector3 canvasPosition = canvas_G.transform.position;
        canvasPosition.x = 0;
        canvasPosition.y = 0;
        canvasPosition.z = 0.1f;
        canvas_G.transform.localPosition = canvasPosition;

        //レンダリングをfaceCameraに
        faceCanvas.renderMode = RenderMode.ScreenSpaceCamera;
        faceCanvas.worldCamera = faceCamera;

        //パネル生成＆設定
        GameObject panel_G = new GameObject("FacePanel");
        facePanel = panel_G.AddComponent<Image>();

        Color tmpColor = facePanel.color;
        tmpColor.a = 0f;
        facePanel.color = tmpColor;

        //パネルをキャンバスの子に設定
        panel_G.transform.parent = canvas_G.transform;

        //パネルのポジションを正面、スケールをいい感じに調整
        Vector3 panelPosition = panel_G.transform.localPosition;
        Vector3 panelScale = panel_G.transform.localScale;
        panelPosition.x = 0;
        panelPosition.y = 0;
        panelPosition.z = 0;
        panelScale = new Vector3(22, 24, 22);
        panel_G.transform.localPosition = panelPosition;
        panel_G.transform.localScale = panelScale;

        //キャンバスをカメラの子に設定
        canvas_G.transform.parent = faceCamera.transform;

        //Layerを変更
        canvas_G.layer = LayerMask.NameToLayer("UI");
        panel_G.layer = LayerMask.NameToLayer("UI");

    }

    void Update()
    {
        //Fixed Timestepを固定
        Time.fixedDeltaTime = FIXEDUPDATE_DELTATIME;

    }

    void FixedUpdate()
    {
        //キー押してない間はreturn
        if (Input.anyKey == false)
        {
            return;
        }

        Test(debugElementNum);

    }

    void Test(int num)
    {
        if (coroutine == null && debug)
        {
            //テスト用　フェードアウト
            if (Input.GetKeyDown(KeyCode.O) && panelColor[num].a == 0)
            {
                FadeOut(num);
            }

            if (Input.GetKeyDown(KeyCode.I) && panelColor[num].a == alpha_Panel[num])
            {
                FadeIn(num);
            }
        }
    }


    public void FadeOut(int num)
    {
        if (panelColor[num].a == 0)
        {
            print("フェードアウト開始");

            coroutine = StartCoroutine(FadeOutCoroutine(num));
        }
    }


    public void FadeIn(int num)
    {
        if (panelColor[num].a == alpha_Panel[num])
        {
            print("フェードイン停止");

            coroutine = StartCoroutine(FadeInCoroutine(num));
        }
    }

    IEnumerator FadeOutCoroutine(int num)
    {

        yield return new WaitForFixedUpdate();
        while (facePanel.color.a < alpha_Panel[num] - 0.00005f)
        {
            yield return new WaitForFixedUpdate();
            panelColor[num].a += alpha_Panel[num] / (fadeTime[num] * 50);
            facePanel.color = panelColor[num];
        }

        panelColor[num].a = alpha_Panel[num];

        StopCoroutine(coroutine);
        coroutine = null;
    }

    IEnumerator FadeInCoroutine(int num)
    {

        yield return new WaitForFixedUpdate();
        while (panelColor[num].a > 0 + 0.00005f)
        {
            yield return new WaitForFixedUpdate();
            panelColor[num].a -= alpha_Panel[num] / (fadeTime[num] * 50);
            facePanel.color = panelColor[num];
        }

        panelColor[num].a = 0;

        StopCoroutine(coroutine);
        coroutine = null;
    }
}

```

例)`FadeIn、FadeOutの引数に0を渡した場合`

・panelColorのElement0のパラメータを利用
・fadeTimeのElement0のパラメータを利用
・aipha_PanelのElement0のパラメータを利用

という形になります。

少なければまだ耐えられますが、増えれば増えるほど
"非常に見づらく設定しづらいのでなんとかしたい"となりました。


##ReorderableListでひとまとめにする

UnityのEditor拡張で、その名の通り自由に順番を入れ替えられるリストです。
[参考記事](https://qiita.com/OKsaiyowa/items/91b45916c4902a9b2394)

ReorderableListを使えばパラメータをひとまとめにして設定画面が一目瞭然になるのでは？
と思い試してみました。

すでに[とてもわかりやすい解説記事](https://anchan828.github.io/editor-manual/web/reorderblelist.html)があったので助かりました。

実際にまとめたものがこちらです。
![CustomFadeInspector.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/68e1ab85-7e3b-1a09-7d08-34366ac990e6.png)

パラメータがリスト内のひとつの要素に収まっているので
非常にわかり易くなったかと思います。

Editor拡張を利用するとコード自体は長くなりますが、
一度書いてしまえばその後なんども使う度に快適さを実感できる上に、
パラメータが増えても見やすさを維持しやすいので
労力分のメリットはあるかと思います。

##パラメータをまとめたクラスを作成
では、実装に移ります。
まずは、ひとまとめにしたいパラメータをつめこんだクラスを作成します。

```C#
 //パラメータをひとまとめにしたクラス
    [Serializable]
    public class FadeParameter
    {
        [SerializeField]
        public Color panelColor;

        [SerializeField]
        public float fadeTime;

        [SerializeField]
        public float alpha_Panel;
    }
```

`[Serializable]`、`[SerializeField]`を使うことで
Editor拡張で利用できます。

ひとまとめにしたクラスをReorderableListで使うために
パラメータを利用したいクラスへ配列として宣言します。

```C#
   [SerializeField]
    FadeParameter[] fadeParameters;
```

##ReorderableList
先程配列として宣言したクラスを利用して
`ReorderableList`を作成します。

```C#
    [CustomEditor(typeof(CustomFade))]
    public class CustomFadeEditor : Editor
    {
        ReorderableList reorderableList;

        void OnEnable()
        {
            SerializedProperty prop = serializedObject.FindProperty("fadeParameters");

            reorderableList = new ReorderableList(serializedObject, prop);
            reorderableList.elementHeight = 78;
            reorderableList.drawHeaderCallback = (rect) => EditorGUI.LabelField(rect, "フェードのリスト");
            reorderableList.drawElementCallback = (rect, index, isActive, isFocused) =>
            {
                SerializedProperty element = prop.GetArrayElementAtIndex(index);
                rect.height -= 4;
                rect.y += 2;
                EditorGUI.PropertyField(rect, element);
            };
        }

        public override void OnInspectorGUI()
        {
            base.OnInspectorGUI();

            serializedObject.Update();
            reorderableList.DoLayoutList();
            serializedObject.ApplyModifiedProperties();
        }
    }
```

##PropertyDrawer
次にReorderableListの要素内のパラメータを描画します。

```C#
    [CustomPropertyDrawer(typeof(FadeParameter))]
    public class CharacterDrawer : PropertyDrawer
    {
        public override void OnGUI(Rect position,SerializedProperty property, GUIContent label)
        {
            //元は 1 つのプロパティーであることを示すために PropertyScope で囲む
            using (new EditorGUI.PropertyScope(position, label, property))
            {
                //ラベル領域の幅を調整
                EditorGUIUtility.labelWidth = 100;

                position.height = EditorGUIUtility.singleLineHeight;

                //各プロパティーの Rect を求める
                Rect panelColorRect = new Rect(position)
                {
                    y = position.y + EditorGUIUtility.singleLineHeight + 1
                };

                Rect fadeTimeRect = new Rect(panelColorRect)
                {
                    y = panelColorRect.y + EditorGUIUtility.singleLineHeight + 2
                };

                Rect alpha_PanelRect = new Rect(fadeTimeRect)
                {
                    y = fadeTimeRect.y + EditorGUIUtility.singleLineHeight + 2
                };

                //各プロパティーの SerializedProperty を求める
                SerializedProperty panelColorProperty = property.FindPropertyRelative("panelColor");
                SerializedProperty fadeTimeProperty = property.FindPropertyRelative("fadeTime");
                SerializedProperty alpha_PanelProperty = property.FindPropertyRelative("alpha_Panel");

                //各プロパティーの GUI を描画
                panelColorProperty.colorValue =EditorGUI.ColorField(panelColorRect,"フェードの色",panelColorProperty.colorValue);
                fadeTimeProperty.floatValue= EditorGUI.FloatField(fadeTimeRect, "フェードの時間", fadeTimeProperty.floatValue);
                alpha_PanelProperty.floatValue = EditorGUI.Slider(alpha_PanelRect, "透明度",alpha_PanelProperty.floatValue, 0, 1);
            }
        }
    }
```

##最終的なコード
ひとつのScriptに

・パラメータをひとまとめにしたクラス
・Editor拡張コード
・プロパティ拡張コード

全てまとめてます。

下記コードをコピペして
適当なゲームオブジェクトにアタッチしたら動かせます。

```C#
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using System;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditorInternal;
#endif

public class CustomFade : MonoBehaviour
{
    //パラメータをひとまとめにしたクラス
    [Serializable]
    public class FadeParameter
    {
        [SerializeField]
        public Color panelColor;

        [SerializeField]
        public float fadeTime;

        [SerializeField]
        public float alpha_Panel;
    }

    [SerializeField, HideInInspector]
    FadeParameter[] fadeParameters;

    [SerializeField, Tooltip("リスト内のテストしたい要素数を入力")]
    int debugElementNum;

    [SerializeField, Tooltip("デバッグ時はTrueに(OでFadeOut、IでFadeIn)")]
    bool debug;

    const float FIXEDUPDATE_DELTATIME = 0.02f;

     Image facePanel;

    Coroutine coroutine;


    void Awake()
    {
        //シーンをロードするたびに新しいカメラを生成
        if (GameObject.Find("OnlyUIRenderingCamera"))
        {
            Destroy(GameObject.Find("OnlyUIRenderingCamera"));
        }
    }


    void Start()
    {
        //カメラ自動生成
        GameObject camera_G = new GameObject("OnlyUIRenderingCamera");
        Camera faceCamera = camera_G.AddComponent<Camera>();
        faceCamera.clearFlags = CameraClearFlags.Depth;
        faceCamera.cullingMask = (1 << LayerMask.NameToLayer("UI"));

        //キャンバス生成＆設定
        GameObject canvas_G = new GameObject("FaceCanvas");
        Canvas faceCanvas = canvas_G.AddComponent<Canvas>();
        canvas_G.AddComponent<CanvasRenderer>();


        //キャンバスのポジションを調整
        Vector3 canvasPosition = canvas_G.transform.position;
        canvasPosition.x = 0;
        canvasPosition.y = 0;
        canvasPosition.z = 0.1f;
        canvas_G.transform.localPosition = canvasPosition;

        //レンダリングをfaceCameraに
        faceCanvas.renderMode = RenderMode.ScreenSpaceCamera;
        faceCanvas.worldCamera = faceCamera;

        //パネル生成＆設定
        GameObject panel_G = new GameObject("FacePanel");
        facePanel = panel_G.AddComponent<Image>();

        Color tmpColor = facePanel.color;
        tmpColor.a = 0f;
        facePanel.color = tmpColor;

        //パネルをキャンバスの子に設定
        panel_G.transform.SetParent(canvas_G.transform);

        //パネルのポジションを正面、スケールをいい感じに調整
        Vector3 panelPosition = panel_G.transform.localPosition;
        Vector3 panelScale = panel_G.transform.localScale;
        panelPosition.x = 0;
        panelPosition.y = 0;
        panelPosition.z = 0;
        panelScale = new Vector3(22, 24, 22);
        panel_G.transform.localPosition = panelPosition;
        panel_G.transform.localScale = panelScale;

        //キャンバスをカメラの子に設定
        canvas_G.transform.SetParent(faceCamera.transform);

        //Layerを変更
        canvas_G.layer = LayerMask.NameToLayer("UI");
        panel_G.layer = LayerMask.NameToLayer("UI");

    }

    void Update()
    {
        //Fixed Timestepを固定
        Time.fixedDeltaTime = FIXEDUPDATE_DELTATIME;

    }

    void FixedUpdate()
    {
        //キー押してない間はreturn
        if (Input.anyKey == false)
        {
            return;
        }

        Test(debugElementNum);

    }

    void Test(int num)
    {
        if (coroutine == null && debug)
        {
            //テスト用　フェードアウト
            if (Input.GetKeyDown(KeyCode.O) && fadeParameters[num].panelColor.a == 0)
            {
                FadeOut(num);
            }

            if (Input.GetKeyDown(KeyCode.I) && fadeParameters[num].panelColor.a == fadeParameters[num].alpha_Panel)
            {
                FadeIn(num);
            }
        }
    }


    public void FadeOut(int num)
    {
        if (fadeParameters[num].panelColor.a == 0)
        {
            coroutine = StartCoroutine(FadeOutCoroutine(num));
        }
    }


    public void FadeIn(int num)
    {
        if (fadeParameters[num].panelColor.a == fadeParameters[num].alpha_Panel)
        {
            coroutine = StartCoroutine(FadeInCoroutine(num));
        }
    }

    IEnumerator FadeOutCoroutine(int num)
    {

        yield return new WaitForFixedUpdate();
        while (facePanel.color.a < fadeParameters[num].alpha_Panel - 0.00005f)
        {
            yield return new WaitForFixedUpdate();
            fadeParameters[num].panelColor.a += fadeParameters[num].alpha_Panel / (fadeParameters[num].fadeTime * 50);
            facePanel.color = fadeParameters[num].panelColor;

        }

        fadeParameters[num].panelColor.a = fadeParameters[num].alpha_Panel;

        StopCoroutine(coroutine);
        coroutine = null;
    }


    IEnumerator FadeInCoroutine(int num)
    {

        yield return new WaitForFixedUpdate();
        while (fadeParameters[num].panelColor.a > 0 + 0.00005f)
        {
            yield return new WaitForFixedUpdate();
            fadeParameters[num].panelColor.a -= fadeParameters[num].alpha_Panel / (fadeParameters[num].fadeTime * 50);
            facePanel.color = fadeParameters[num].panelColor;

        }

        fadeParameters[num].panelColor.a = 0;

        StopCoroutine(coroutine);
        coroutine = null;
    }

#if UNITY_EDITOR
    [CustomEditor(typeof(CustomFade))]
    public class CustomFadeEditor : Editor
    {
        ReorderableList reorderableList;

        void OnEnable()
        {
            SerializedProperty prop = serializedObject.FindProperty("fadeParameters");

            reorderableList = new ReorderableList(serializedObject, prop);
            reorderableList.elementHeight = 78;
            reorderableList.drawHeaderCallback = (rect) => EditorGUI.LabelField(rect, "フェードのリスト");
            reorderableList.drawElementCallback = (rect, index, isActive, isFocused) =>
            {
                SerializedProperty element = prop.GetArrayElementAtIndex(index);
                rect.height -= 4;
                rect.y += 2;
                EditorGUI.PropertyField(rect, element);
            };
        }

        public override void OnInspectorGUI()
        {
            base.OnInspectorGUI();

            serializedObject.Update();
            reorderableList.DoLayoutList();
            serializedObject.ApplyModifiedProperties();
        }
    }
#endif

#if UNITY_EDITOR
    [CustomPropertyDrawer(typeof(FadeParameter))]
    public class CharacterDrawer : PropertyDrawer
    {
        public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
        {
            //元は 1 つのプロパティーであることを示すために PropertyScope で囲む
            using (new EditorGUI.PropertyScope(position, label, property))
            {
                //ラベル領域の幅を調整
                EditorGUIUtility.labelWidth = 100;

                position.height = EditorGUIUtility.singleLineHeight;

                //各プロパティーの Rect を求める
                Rect panelColorRect = new Rect(position)
                {
                    y = position.y + EditorGUIUtility.singleLineHeight + 1
                };

                Rect fadeTimeRect = new Rect(panelColorRect)
                {
                    y = panelColorRect.y + EditorGUIUtility.singleLineHeight + 2
                };

                Rect alpha_PanelRect = new Rect(fadeTimeRect)
                {
                    y = fadeTimeRect.y + EditorGUIUtility.singleLineHeight + 2
                };

                //各プロパティーの SerializedProperty を求める
                SerializedProperty panelColorProperty = property.FindPropertyRelative("panelColor");
                SerializedProperty fadeTimeProperty = property.FindPropertyRelative("fadeTime");
                SerializedProperty alpha_PanelProperty = property.FindPropertyRelative("alpha_Panel");

                //各プロパティーの GUI を描画
                panelColorProperty.colorValue = EditorGUI.ColorField(panelColorRect, "フェードの色", panelColorProperty.colorValue);
                fadeTimeProperty.floatValue = EditorGUI.FloatField(fadeTimeRect, "フェードの時間", fadeTimeProperty.floatValue);
                alpha_PanelProperty.floatValue = EditorGUI.Slider(alpha_PanelRect, "透明度", alpha_PanelProperty.floatValue, 0, 1);
            }
        }
    }
#endif
}
```

---
2019/09/20 追記

RectTransformの場合は`SetParent`で親子関係を設定する必要があるとのことだったので
修正しました。

##デモ
![Gif_Fade.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/de71388d-e838-a5e9-71e2-15d78cd36493.gif)

デバッグ用の機能も用意しました。要素のIndexを指定してあげる必要があります。
  
  

Postprocessingを使いこなしていないのでそっちも使ってみようと思います。