UnityのuGUIの表示パネル切り替えを簡単にできるように

この記事は [SLP KBIT Advent Calendar 2018](https://qiita.com/advent-calendar/2018/slp_kbit) の20日目の記事です。

メニュー画面みたいなもの作るときとか、1シーンだけで完結するようなものとか作ってたりすると必然的にUIの切り替えが多発するもので。
切り替えのときに簡易的な動きは欲しいけどいちいち書くのも面倒なのでInspectorで値いじれば簡単にできるようなものを作ってみようかなと思ったわけです。

というのが建前で本当はInspector内部の動的な切り替えにはエディタ拡張使うことを知って面白そうだなと思ったのが始まりです。

# はじめに

既存の便利なコンポーネントが存在するのかもしれませんがとりあえず自作を目指して。
ついでに簡単にエディタ拡張も使用してみます。

なお現段階はあくまでおためし気分で作った結果なのでいろいろと機能が不足しています。

# スクリプト

## パネル切り替え

パネルを切り替えるスクリプトです。
表示するパネルそれぞれにアタッチします。

※ 『戻る』、『キャンセル』に相当するメソッドはないです。『閉じる』はあくまで切り替えの際に切り替える対象がないものと同義となっています。

|メソッド|内容|
|:--|:--|
|void PanelAppear()|パネルを出現させます|
|void PanelChange(string _name)|同階層内の_nameと同じ名前のパネルに切り替えます|
|void PanelClose()|パネルを閉じます|

```cs:PanelChanger.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

[RequireComponent(typeof(CanvasGroup))]
public class PanelChanger : MonoBehaviour {
	public enum FadeMode {
		None,
		Fade
	}

	public enum FadeDirection {
		Top,
		Right,
		Left,
		Bottom,
		None
	}

	private enum PlayingState {
		Stop,
		FadeIn,
		FadeOut
	}

	private RectTransform rtf;
	private GameObject parent;
	private string panel_name;
	private bool isPlaying = false;
	private float progress = 0.0f;
	private PlayingState nowState = PlayingState.Stop;
	private Coroutine nowCoroutine = null;
	private int directionX = 0, directionY = 0;
	private CanvasGroup canvasGroup;

	public FadeMode fadeInMode = FadeMode.None;
	public FadeDirection fadeIn = FadeDirection.Top;
	public float fadeInTime = 1.0f;
	public float fadeInDelay = 0.0f;
	public float fadeInDistance = 800.0f;
	public bool fadeInAlpha = true;
	public AnimationCurve fadeInAnimationCurve = AnimationCurve.Linear(0,0,1,1);
	public UnityEvent onStart;
	public UnityEvent onFinishFadeIn;

	public FadeMode fadeOutMode = FadeMode.None;
	public FadeDirection fadeOut = FadeDirection.Bottom;
	public float fadeOutTime = 1.0f;
	public float fadeOutDelay = 0.0f;
	public float fadeOutDistance = 800.0f;
	public bool fadeOutAlpha = true;
	public AnimationCurve fadeOutAnimationCurve = AnimationCurve.Linear(0,0,1,1);
	public UnityEvent onStartFadeOut;
	public UnityEvent onFinish;

	void Awake () {
		rtf = this.gameObject.GetComponent<RectTransform>();
		parent = transform.parent.gameObject;
		canvasGroup = this.gameObject.GetComponent<CanvasGroup>();	
	}

	void OnEnable() {
		StartChange();
	}

	void OnDisable() {
		Stop();
	}
	
	void Update () {
		if ( !isPlaying ) { return; }
		switch(nowState) {
			case PlayingState.Stop : return;
			case PlayingState.FadeIn : {
				float curve = fadeInAnimationCurve.Evaluate(progress);
				rtf.anchoredPosition = new Vector2(directionX*fadeInDistance*(1.0f - curve), directionY*fadeInDistance*(1.0f - curve));
				if ( fadeInAlpha ) { canvasGroup.alpha = curve; }
				break;
			}
			case PlayingState.FadeOut : {
				float curve = fadeInAnimationCurve.Evaluate(progress);
				rtf.anchoredPosition = new Vector2(directionX*fadeInDistance*curve, directionY*fadeInDistance*curve);
				if ( fadeOutAlpha ) { canvasGroup.alpha = (1.0f - curve); }
				break;
			}
		}
	}

	//-- パネルの出現
	public void PanelAppear() {
		this.gameObject.SetActive(true);
		StartChange();
	}

	//-- パネルを切り替える(_name : 切り替え先のパネルオブジェクト名)
	public void PanelChange(string _name) {
		panel_name = _name;
		if ( fadeOutMode == FadeMode.None ) { FinishOutChange(); }
		else { FadeOutPanel(); }
	}

	//-- パネルを閉じる
	public void PanelClose() {
		panel_name = "";
		if ( fadeOutMode == FadeMode.None ) { FinishOutChange(); }
		else { FadeOutPanel(); }
	}

	public void changeFadeIn(FadeDirection _direction) {
		fadeIn = _direction;
	}

	public void changeFadeOut(FadeDirection _direction) {
		fadeOut = _direction;
	}

	public void Stop() {
		if ( nowCoroutine != null ) StopCoroutine(nowCoroutine);
		isPlaying = false;
		nowState = PlayingState.Stop;
		nowCoroutine = null;
	}

	private void StartChange() {
		if ( onStart != null ) onStart.Invoke();
		if ( fadeInMode == FadeMode.None ) { FinishInChange(); }
		else { FadeInPanel(); }
	}

	private void FinishInChange() {
		canvasGroup.alpha = 1.0f;
		canvasGroup.interactable = true;
		rtf.anchoredPosition = new Vector2(0, 0);
	}

	private void FinishOutChange() {
		canvasGroup.alpha = 0.0f;
		if ( panel_name != "" ) {
			parent.transform.Find(panel_name).gameObject.SetActive(true);
		}
		if ( onFinish != null ) onFinish.Invoke();
		if ( panel_name == "" ) {
			parent.GetComponent<PanelChangerRoot>().PanelClose();
		}
		this.gameObject.SetActive(false);
	}

	private void FadeInPanel() {
		progress = 0.0f;
		canvasGroup.alpha = 0.0f;
		canvasGroup.interactable = false;
		setDirection(fadeIn);
		nowCoroutine = StartCoroutine(PlayFadeInCoroutine());
	}

	private void FadeOutPanel() {
		progress = 0.0f;
		canvasGroup.alpha = 1.0f;
		canvasGroup.interactable = false;
		setDirection(fadeOut);
		nowCoroutine = StartCoroutine(PlayFadeOutCoroutine());
	}

	private void setDirection(FadeDirection _direction) {
		switch(_direction) {
			case FadeDirection.Top    : directionX =  0; directionY =  1; break;
			case FadeDirection.Right  : directionX =  1; directionY =  0; break;
			case FadeDirection.Left   : directionX = -1; directionY =  0; break;
			case FadeDirection.Bottom : directionX =  0; directionY = -1; break;
		}
	}

	IEnumerator PlayFadeInCoroutine ()
	{
		if ( fadeInDelay > 0 ) { yield return new WaitForSeconds(fadeInDelay); }
		if ( isPlaying ) { yield break; }
		nowState = PlayingState.FadeIn;
		isPlaying = true;

		while ( progress < 1.0f ) {
			progress += Time.deltaTime / fadeInTime;
			yield return null;
		}

		isPlaying = false;
		nowState = PlayingState.Stop;
		progress = 1.0f;
		if ( onFinishFadeIn != null ) onFinishFadeIn.Invoke();
		FinishInChange();
	}

	IEnumerator PlayFadeOutCoroutine ()
	{
		if ( fadeOutDelay > 0 ) { yield return new WaitForSeconds(fadeOutDelay); }
		if ( isPlaying ) { yield break; }
		nowState = PlayingState.FadeOut;
		isPlaying = true;
		if(onStartFadeOut != null) onStartFadeOut.Invoke();

		while ( progress < 1.0f ) {
			progress += Time.deltaTime / fadeOutTime;
			yield return null;
		}

		isPlaying = false;
		nowState = PlayingState.Stop;
		progress = 1.0f;
		FinishOutChange();
	}
}

```

## パネル管理用(親オブジェクト用)

このコンポーネント内のメソッドを通じてパネルの開閉を管理します。
```PanelChanger.cs``` をアタッチしたパネルオブジェクトの親オブジェクトにアタッチします。

|メソッド|内容|
|:--|:--|
|void PanelOpen()|パネルを開きます|

```cs:PanelChangerRoot.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

public class PanelChangerRoot : MonoBehaviour {

	public string Top;
	public UnityEvent onOpen;
	public UnityEvent onClose;

	//-- パネルを開く
	public void PanelOpen() {
		this.gameObject.SetActive(true);
		foreach ( Transform child in this.transform ) {
			child.gameObject.SetActive(false);
		}
		if ( onOpen != null ) { onOpen.Invoke(); }
		this.gameObject.transform.Find(Top).GetComponent<PanelChanger>().PanelAppear();
	}

	//-- パネルを閉じる
	public void PanelClose() {
		if ( onClose != null ) { onClose.Invoke(); }
		this.gameObject.SetActive(false);
	}
}

```

## エディタ拡張

```PanelChanger.cs``` のInspector上での変数表示変更を行います。
```Assets/Editor/``` 下に配置してください。

※ なくても動きます。

```cs:PanelChangerEditor.cs
using UnityEngine;
using UnityEngine.Events;
using System.Collections;
using UnityEditor;

[CustomEditor (typeof(PanelChanger))]
public class PanelChangerEditor : Editor
{
	public override void OnInspectorGUI() {
		PanelChanger _panel = target as PanelChanger;

		_panel.fadeInMode = (PanelChanger.FadeMode)EditorGUILayout.EnumPopup("fadeInMode", _panel.fadeInMode);
		if ( _panel.fadeInMode == PanelChanger.FadeMode.Fade ) {
			_panel.fadeIn = (PanelChanger.FadeDirection)EditorGUILayout.EnumPopup("fadeIn", _panel.fadeIn);
			_panel.fadeInTime = EditorGUILayout.FloatField("fadeInTime", _panel.fadeInTime);
			_panel.fadeInDelay = EditorGUILayout.FloatField("fadeinDelay", _panel.fadeInDelay);
			_panel.fadeInDistance = EditorGUILayout.FloatField("fadeinDistance", _panel.fadeInDistance);
			_panel.fadeInAlpha = EditorGUILayout.Toggle("fadeinAlpha", _panel.fadeInAlpha);
			_panel.fadeInAnimationCurve = EditorGUILayout.CurveField("fadeInAnimationCurve", _panel.fadeInAnimationCurve);
		}
		SerializedProperty onStart = serializedObject.FindProperty("onStart");
		EditorGUILayout.PropertyField(onStart);
		if ( _panel.fadeInMode == PanelChanger.FadeMode.Fade ) {
			SerializedProperty onFinishFadeIn = serializedObject.FindProperty("onFinishFadeIn");
			EditorGUILayout.PropertyField(onFinishFadeIn);
		}

		_panel.fadeOutMode = (PanelChanger.FadeMode)EditorGUILayout.EnumPopup("fadeOutMode", _panel.fadeOutMode);
		if ( _panel.fadeOutMode == PanelChanger.FadeMode.Fade ) {
			_panel.fadeOut = (PanelChanger.FadeDirection)EditorGUILayout.EnumPopup("fadeOut", _panel.fadeOut);
			_panel.fadeOutTime = EditorGUILayout.FloatField("fadeOutTime", _panel.fadeOutTime);
			_panel.fadeOutDelay = EditorGUILayout.FloatField("fadeOutDelay", _panel.fadeOutDelay);
			_panel.fadeOutDistance = EditorGUILayout.FloatField("fadeOutDistance", _panel.fadeOutDistance);
			_panel.fadeOutAlpha = EditorGUILayout.Toggle("fadeOutAlpha", _panel.fadeOutAlpha);
			_panel.fadeOutAnimationCurve = EditorGUILayout.CurveField("fadeOutAnimationCurve", _panel.fadeOutAnimationCurve);
			SerializedProperty onStartFadeOut = serializedObject.FindProperty("onStartFadeOut");
			EditorGUILayout.PropertyField(onStartFadeOut);
		}
		SerializedProperty onFinish = serializedObject.FindProperty("onFinish");
		EditorGUILayout.PropertyField(onFinish);

		if (GUI.changed) {
			serializedObject.ApplyModifiedProperties();
		}

		EditorUtility.SetDirty( target );
	}

}
```

# 使い方

## 準備

上記の3つのスクリプトをあらかじめ用意しておきます。
(なお ```PanelChangerEditor.cs``` に関しては ```Assets/Editor/PanelChangerEditor.cs``` となるように配置しておいてください。

表示させたいパネルを用意します。
今回は ```Menu``` を親として ```MenuTop``` と ```MenuNext``` を用意しました。
![a13a7caa83f52bce2bcaf9e4671fc406.png](https://qiita-image-store.s3.amazonaws.com/0/151989/d3fbafd2-33e6-eac4-f3fd-17847345d854.png)

### 親オブジェクト ```Menu```
今回は分かりやすくするために ```MenuTop``` や ```MenuNext``` それぞれに色付けを行うので透明にしてあります。
実際には ```Menu``` 下のパネル表示の際の背景となります。
![1d2f48ebe8e38afa62084efb72818f45.png](https://qiita-image-store.s3.amazonaws.com/0/151989/c077ad01-7da1-8341-c634-aad02d12cd9f.png)

### 子オブジェクト ```MenuTop```
```MenuTop``` 自体は透明な枠として使用しており、その下の階層に様々なUIを自由に配置してしまいます。
今回は ```MenuNext``` に遷移するためのボタンと閉じるボタンの2つのみを用意。
```Panel``` は分かりやすくするために赤色背景にしてあります。
![f9f3e0f7f6a6d88fcd4f719b57fd4386.png](https://qiita-image-store.s3.amazonaws.com/0/151989/f85f0fc8-8f57-8ba4-9c72-9bf35c43f90c.png)

### 子オブジェクト ```MenuNext```
上に同じく。
```Panel``` は分かりやすくするために青色背景にしてあります。
![6027b7c8744e78105e6a8e2a8e52b783.png](https://qiita-image-store.s3.amazonaws.com/0/151989/89aab886-5a07-9a90-64ad-43b0a56a53f7.png)

### メニューを開くためのボタン
メニューを開くためのイベントが必要なので今回はとりあえず ```UI``` という名前のパネルにボタンをのっけておきます。
![e1464c26d80b53a2bca36d3ca830b276.png](https://qiita-image-store.s3.amazonaws.com/0/151989/024d3709-e08f-960a-fe2c-c8b548e79132.png)


## コンポーネントのアタッチ

### 親オブジェクト ```Menu```
```PanelChangerRoot.cs``` をアタッチします。
Inspector上に ```Top``` という文字列変数が存在するのでここにパネルを開いた際に最初に表示されるパネルのオブジェクト名(今回だと ```MenuTop``` )を記述しておきます。
以下の2つのイベントがあります。

|イベント名|タイミング|
|:--|:--|
|OnStart()|そのオブジェクト以下のパネルが開かれた時|
|OnClose()|そのオブジェクト以下のパネルを閉じる時|

![6d39f6b949592c452b283d63cc959cd9.png](https://qiita-image-store.s3.amazonaws.com/0/151989/3f6b74fc-1f6f-d5f5-d957-24665aa5b639.png)

### 子オブジェクト
```PanelChanger.cs``` をアタッチします。
基本的にアタッチしておくだけで問題ないです。同時に ```CanvasGroup``` というコンポーネントが追加されます。
```MenuNext``` の方にもつけておきます。
初期状態では以下の2つのイベントがあります。

|イベント名|タイミング|
|:--|:--|
|OnStart()|そのパネルが開き始め、アクティブになる時|
|OnFinish()|そのパネルが切り替わり、非アクティブになる時|
![310b60ff281602a5b799be9b20a07d57.png](https://qiita-image-store.s3.amazonaws.com/0/151989/8f36de20-627d-d495-a61b-0cd630f1256b.png)

## メソッドをたたく

### パネル切り替え
パネルを切り替えるためのメソッドを叩きます。
ということで用意してあった ```Button``` のイベント ```OnClick()``` から ***そのタイミングで表示されているパネル( ```MenuTop``` )*** の ```PanelChanger.cs``` にある ```PanelChange(string _name)``` を叩きに行きます。
引数ですが切り替えたい対象のパネルのオブジェクト名( ```"MenuNext"``` )を与えてやります。
同様にして ```PanelNext``` の方にもつけてやります。
![878bdb5d9076fb9d258fb5e464ff37c4.png](https://qiita-image-store.s3.amazonaws.com/0/151989/325788e6-c530-41dd-b2ec-c1f02cb48746.png)

### パネルを閉じる
パネルを閉じるためのメソッドを叩きます。
といっても先ほどの切り替えとさほど変わりません。
同様にして ```ButtonClose``` のイベント ```OnClick()``` から ```PanelChanger.cs``` にある ```PanelClose()``` を叩きに行きます。
同様にして ```PanelNext``` の方にもつけてやります。
![1fbae71e9e726df4c871ea0810f7d712.png](https://qiita-image-store.s3.amazonaws.com/0/151989/b8bd561a-c94d-42f9-1d7c-e255ef8b2e1a.png)

### パネルを開く
パネルを開くためのメソッドを叩きます。
ということで用意してあった ```UI -> Button``` のイベント ```OnClick()``` から先ほどのパネルたちの親オブジェクト( ```Menu``` )の ```PanelChangerRoot.cs``` にある ```PanelOpen()``` を叩きに行きます。
![0695029ff6269bbfa2c771ec12091a49.png](https://qiita-image-store.s3.amazonaws.com/0/151989/dacb1668-23d2-9cc2-1d85-8b99f9952ee2.png)

## 動作確認
以上で動きます。
![9c953a64d48002b4bbe508f0718e0f4e.gif](https://qiita-image-store.s3.amazonaws.com/0/151989/a733ecbc-e46d-3978-f36a-75a890bf9c53.gif)

## 切り替えアニメーション
Inspector上で ```PanelChanger.cs``` の変数等をいじることで簡易なアニメーションをさせられます。
![310b60ff281602a5b799be9b20a07d57.png](https://qiita-image-store.s3.amazonaws.com/0/151989/8f36de20-627d-d495-a61b-0cd630f1256b.png)

```fadeInMode``` , ```fadeOutMode``` を ```None``` から ```Fade``` に切り替えるとにゅっと色々な要素が出てきます。***ここ~~だけ~~がエディタ拡張の仕事です***
![04b2c1c6d6e217ad5dee8f0e8651dfa8.png](https://qiita-image-store.s3.amazonaws.com/0/151989/f14e208e-1745-f0b4-fded-82a6054da5b6.png)

それぞれの変数は以下の通りです。
各パネルごとに別々に調整が可能なので好き勝手いじると動いてくれます。

|変数名|内容|
|:--|:--|
|fadeIn|フェードイン時に入ってくる方向|
|fadeInTime|フェードインにかかる時間
|fadeInDelay|フェードイン開始タイミング|
|fadeInDistance|フェードインの距離|
|fadeInAlpha|フェードインの際にalpha値(透明度)も変化させるか|
|fadeInAnimationCurve|フェードインの移動量変化の遷移|
|fadeOut|フェードアウト時に入ってくる方向|
|fadeOutTime|フェードアウトにかかる時間
|fadeOutDelay|フェードアウト開始タイミング|
|fadeOutDistance|フェードアウトの距離|
|fadeOutAlpha|フェードアウトの際にalpha値(透明度)も変化させるか|
|fadeOutAnimationCurve|フェードアウトの移動量変化の遷移|

またその他にも以下の2つのイベントが追加されます。

|イベント名|タイミング|
|:--|:--|
|OnFinishFadeIn()|フェードインが終了した時|
|OnStartFadeOut()|フェードアウトが開始した時|

### フェード例
![4f0369d95f2e1dc3101a056ea41c219a.png](https://qiita-image-store.s3.amazonaws.com/0/151989/b649f8d3-c24b-b7ae-5d5a-eefc8dc923f4.png)
![bad43b181452cdef82af840c718f8ed4.gif](https://qiita-image-store.s3.amazonaws.com/0/151989/4db43c06-acd6-8fb1-3097-fb82f8da79e6.gif)

# おわりに
正直なところ現状『戻る』、『キャンセル』ボタンに対応できるような ***フェードインの時とは逆向きに戻っていく*** 処理ができていませんし、何なら4方向のフェードしかできません。
ということで現状は

- 戻るメソッド
- 独自のアニメーションに対応
- その他切り替え方法の簡易設定

あたりを追加してみたいなと思ったりはしていますが予定は未定です。
特にAssetは使ってないので[github](https://github.com/olives0415/PanelState)に。


赤青背景を透明にして親オブジェクトを若干黒めな背景にしたりするとそれっぽい。
![77c39f95958b808d5622101635c12313.gif](https://qiita-image-store.s3.amazonaws.com/0/151989/73c18d35-d964-df14-07fd-d9d21de7e400.gif)


# 参考

- [インスペクタで設定できる値を動的に変更する【Unity】【エディタ拡張】](http://kan-kikuchi.hatenablog.com/entry/InspectorEx)
- [Inspectorが変わる！](http://marupeke296.com/UNI_ED_No1_Inspector.html)
- [UnityでアクティブでないGameObjectを取得する](https://spphire9.wordpress.com/2013/11/17/unity%E3%81%A7%E3%82%A2%E3%82%AF%E3%83%86%E3%82%A3%E3%83%96%E3%81%A7%E3%81%AA%E3%81%84gameobject%E3%82%92%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B/)
- [Canvas Group](https://docs.unity3d.com/ja/2018.1/Manual/class-CanvasGroup.html)