公式アセット「Unity Samples: UI」Menu 3D シーンで uGUI の基礎を理解する

10連休も近いですし、Unity で自分の好きなシューティングゲームでも作成しようと考えました。

これまで Unity で [簡単なアニメーション](https://qiita.com/yamachan360/items/38b76a67ee0faef7e6b0) を試したり、[Unity ちゃんを動かしたり](https://qiita.com/yamachan360/items/d0d01bb644b81a0eb3e1) して遊んできましたし、他の環境で [ゲームを作成した](http://smilebasic.com/library/ogiri/) 経験はあるので、まあなんとか作れるのではないか、と。

とはいえ Unity の UI 周りがイマイチわかっていないので、無料アセットとして公開されている公式の [Unity Samples: UI](https://assetstore.unity.com/packages/essentials/unity-samples-ui-25468) の中身をみて、理解していきたいとおもいます。

うまく理解できたら、そのまま拡張して自作ゲームに組み込みたいな、などと。

# 初期設定

今回利用しているのは Windows 版の Unity 2018.3.12f1 (64-bit) です。新規にプロジェクトを作成し、アセットストアから [Unity Samples: UI](https://assetstore.unity.com/packages/essentials/unity-samples-ui-25468) をインポートします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/8f771bbd-3ba7-e318-64a9-0f677bbfa8ae.png)
多くのファイルが含まれていますが、そのまま「インポート」をクリックして全てプロジェクトに追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/462de9a2-27cd-aebe-75eb-0731afed4fab.png)
プロジェクトに以下のようにフォルダが追加されていればインポートは完了です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/729fbe33-23d1-4ee6-6bc9-3835740907c0.png)

# Menu 3D シーン

Scenes フォルダにある Menu 3D シーンを読み込んでみましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/4e400497-7610-e7b9-c2ea-bb4ed4bacccb.png)
ヒエラルキーには以下のように読み込まれます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/ef1e99cd-75f1-0453-1f0c-98c57cb237e8.png)
実行すると以下のような感じで、宇宙を背景としたゲームメニューが表示されて操作できます。
![qiita.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/3f8c9d4a-6043-4642-9efa-11d0394e89aa.gif)
シューティングゲームにそのまま使えそうな感じで良いですね。

## 背景部分

背景には星(?)が流れる宇宙空間が表示されていますが、これは簡単な仕組みなので先に見ておきましょう。

ヒエラルキーに表示されている `SF Scene Elements` が背景なのですが、Prefab フォルダに元のデータがあるので、そちらを参照したほうがわかりやすいです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/cdba6517-f26e-bcc0-b198-70aacb391409.png)
Background という一枚絵(スプライト)の前にパーティクルを動かして星(?)の動きを表現しています。そしてそれを Main Camera で表示しています。

実際のゲームでは、これをゲーム画面と置き換えます。なおこの Prefab を流用し、タイトル文字でも上に被せれば、そのままゲームのオープニング画面にすることもできそうですね。

## メニューが右から出てくる動作

さて、上記の背景の上に表示されるメニュー `MainManu` について見ていきましょう。シーンを開始するとメニューが右から出てきますが、これはどうやって実装されているのでしょうか？

インスペクタを確認すると、`MainMenu` という [アニメーター(アニメーションコントローラー)](https://qiita.com/yamachan360/items/38b76a67ee0faef7e6b0#%E3%82%A2%E3%83%8B%E3%83%A1%E3%83%BC%E3%82%BF%E3%83%BC%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6) が設定されているのがわかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/c705ddff-3aa4-f965-222f-fe92d1cc3a52.png)
このアニメーターの内容を確認すると、以下のような内容が設定されていることがわかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/2d3752ae-0a3a-227d-c6ed-599e77cc2ebd.png)
ここでわかることは

* Open と Closed という2つのアニメーションが追加されている
* Open と Closed は互いに遷移(白い矢印)する
* ゲーム開始時は Entry から遷移して Closed の状態から始まる

でしょうかね。そして追加された Closed のアニメーションがこちら
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/b49a8982-0ce7-6c56-dad9-36e943609535.png)
アニメーションといっても、オブジェクトの位置を指定しているだけで、時間で値を変化させているわけではありません。「長さ0のアニメーション」が「静止画」を意味するのと同じこと？かも。

同様に Open のほうのアニメーションも、オブジェクトの位置を指定しているだけのようです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/5ca1a56c-f98f-603c-cb38-028d97a25c2b.png)
何これ？これじゃアニメーションの意味が無いじゃん？って思いますよね。でもゲーム画面ではメニューが動いているわけで、やっぱりこれもアニメーション指定の方法のひとつなんですよ。

アニメーター(アニメーションコントローラー)の `MainMenu` に戻って、Closed アニメーションから、Open アニメーションに向かう白矢印、つまり遷移をよく見てみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/3b75bb82-0a5f-1fb8-daba-9fd3c116543a.png)
この **遷移** は2つのアニメーションを交換する動作であり、この遷移もまたアニメーションなのです。Closed という画面の外側にある位置を指定したアニメーションから、一定の時間をかけて、Open という画面内にある位置を指定したアニメーションへと状態を遷移していく。

その時に重なっているアニメーションを合成するので、その比率が時間で変化すれば、Close という状態から、Open という状態へだんだん変化するアニメーション的な表現となります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/7d836493-15d1-7752-65e1-c9ab876ef1c3.png)
つまりこの白矢印の遷移が、メニューを画面外から画面内に移動させるアニメーションを実装している実態なのでした。

ちなみに一番下にある `Conditions` はこの遷移の開始条件になります。つまりこの遷移、Closed から Open への遷移は、Open というパラメーターが True になると開始する、ということです。外部から遷移をコントロールするには、この Conditions の設定を利用します。

## 誰がメニューを開いたのか

アニメーター(アニメーションコントローラー)の `MainMenu` における遷移が、メニューを開くアニメーションを実施していることはわかりました。で、次の疑問は「で、誰が最初にメニューを開くの？」です。もっと言えば「誰がアニメーターの中の Closed から Open への遷移を開始させたの？」であり、更に言えば「誰が遷移の開始条件である Open パラメーターを True に設定したの？」という疑問です。

メニューの初期状態は Closed で、つまりは隠れています。通常のゲーム上では何かのボタンを押してメニューを開くわけですが、今回の Menu 3D シーンは、シーン開始時に自動的にメニューが開いています。この部分の処理を探してみましょう。

ヒエラルキーを見てみると `MenuManager` と、そのものズバリな感じのオブジェクトがあります。インスペクターを見ると、`PanelManager` というスクリプトが設定されており、例の `MainMenu` アニメーターが引数として指定されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/2d5856e1-b9b6-3649-6ccb-dff6e25047d0.png)
このスクリプトを開き、[シーン開始時に自動実行](https://docs.unity3d.com/ja/current/Manual/ExecutionOrder.html) される OnEnable 関数の定義を確認してみましょう。

```c#:PanelManager.cs
	public Animator initiallyOpen;
	private int m_OpenParameterId;

	public void OnEnable()
	{
		m_OpenParameterId = Animator.StringToHash ("Open"); // アニメーターのパラメーター名を指定
		OpenPanel(initiallyOpen); // 指定されたメニューを開く
	}
```

※ C#コードは主要でない部分、今回の処理に関係ない部分を少し削って紹介しています

`OpenPanel` 関数は以下のように定義されています。`anim.SetBool` 関数のところでさきほどの `MainMenu` アニメーターの Open というパラメータが True になり、これにより Closed アニメーションから Open アニメーションへの遷移が開始され、メニューが表示される(表示位置に移動する)わけです。

```c#:PanelManager.cs
	private GameObject m_PreviouslySelected;

	public void OpenPanel (Animator anim)
	{
		anim.gameObject.SetActive(true);
		var newPreviouslySelected = EventSystem.current.currentSelectedGameObject;

		anim.transform.SetAsLastSibling(); // Transformリストで最後の順番になるよう移動
		CloseCurrent();
		m_PreviouslySelected = newPreviouslySelected;

		anim.SetBool(m_OpenParameterId, true); // これが遷移を開始させる

		GameObject go = FindFirstEnabledSelectable(anim.gameObject);
		SetSelected(go); // メニューの最初の項目を選択状態にする
	}
```

マニュアルの [画面遷移の作成](https://docs.unity3d.com/ja/500/Manual/HOWTO-UIScreenTransition.html) ページにほぼ同じロジックが解説されていますので、コード理解の助けにしてみてください。

`PanelManager` スクリプトにはこれ以外にもメニューを操作する関数が幾つも定義されています。のちほど必要であれば、また戻って眺めてみましょう。

## メニュー構成

シーンを実行すると、以下のようなメインメニューが右から出現します。この出現に関してはもう仕組みがわかりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/1904c274-80a5-6752-b279-aaca6c58e7d7.png)
次は表示されたメニューそのものに対し、その仕組みを探っていきましょう。まず、ヒエラルキーを眺めてみると、表示とうまく対応したカタチでオブジェクトが階層構造に並んでいることがわかります。さすが公式サンプルです！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/5076eb5d-117e-d021-8446-7276993cf563.png)
まあ、今回はシンプルなデザインですからね。タイトルやボタンなど各要素ごとに、背景となる画像と、文字を表示するラベルを使用しています。

例えば `Window` の画像のところの背景色を青から緑に変えると、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/348e7d21-4b7c-d218-ab90-84652f3ab38a.png)
メニューの背景が問題なく緑色になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/e5790130-78a6-a150-8b47-59b759380e88.png)
これは背景に使用している画像が半透明の白で作成されたものなので、指定した色とうまく組み合わさってパーツの形が表現されるわけです。こういった「白黒の半透明画像に色を組み合わせるデザイン要素」の準備の仕方は、最近よく使われるようになりました。便利ですよね。

また `TitleLabel` オブジェクトに設定されたテキスト "GUI Demo" を適当に変更すれば…
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/b872b642-791e-a566-d4bf-3c4fe4dc465a.png)
実行すると表示が変更されているのがわかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/6eb0582b-61b6-60b5-a6d6-bd7c0bc7a82d.png)
このへん、シンプルで非常に流用し易くて助かります。

なお余談ですが、このメニュー全体の枠は Window オブジェクトに指定された1つの画像で実現されています。つまり上と下にある二重部分は、そうデザインされたものです。それに対してメニューのタイトル部分ですが、こちらはシンプルな一本線の枠の画像を使用しています。タイトル枠が二重に見えるのは、この画像を2つ、位置をズラして表示しているからです。ティルト機能でメニューを傾けると、この二種類の表現の違いがわずかに影響を与えているのを感じることができるとおもいます。

## 各ボタンの制御

各ボタンの動作ですが、インスペクタを確認するとボタン専用スクリプトが使用されています。ボタンに関しては[【uGUI】Buttonの使い方](https://qiita.com/2dgames_jp/items/b3d7d204895d67742d0c) の記事がわかりやすかったです。

ボタン専用スクリプトの遷移には幾つか指定方法があるのですが、今回は「アニメーション」が指定されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/e048f70f-d889-f122-daae-f4b465d39f21.png)
ここで各アクションに指定されているアニメーションと、そのトリガー(パラメーター)は、その下で指定されているアニメーター `SF Button` の中に含まれています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/2fc3394d-3a17-cda9-2021-1e7cbbcd58ed.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/4116e1c7-cf4a-da79-0094-b54dff2aa2f7.png)
例えば `Highlighted` アニメーションは以下のように選択されたボタンを少し上(z軸でマイナス方向)に持ち上げ、バックグラウンドの描画色を白に設定するようになってます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/8c65f6a7-f840-06ef-4094-cc37dd3d530b.png)
ボタンスクリプトはボタンの見た目をコントロールする汎用的なモジュールになっていて、非常に便利ですね。

## 各ボタンのアクション

さきほど紹介した [【uGUI】Buttonの使い方](https://qiita.com/2dgames_jp/items/b3d7d204895d67742d0c) の記事にもありますが、ボタン専用スクリプトは選択時のアクションも定義できます。

例えば一番下の「Quit」ですが、クリック時のアクションとして以下のように定義されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/9ca359e4-38a9-4da7-1857-ab08a5ba0cb1.png)
これは順に

* Editor実行時、ランタイム実行時共に
* 呼び出し元は ApplicationManager で
* ApplicationManager.Quit 関数を呼び出す

と設定されています。実際に呼び出される関数の定義はこちら。まあ、普通の終了処理ですよね。

```c#:ApplicationManager.cs
	public void Quit () 
	{
		#if UNITY_EDITOR
			UnityEditor.EditorApplication.isPlaying = false;
		#else
			Application.Quit();
		#endif
	}
```

もうひとつ、その上にある「Settings」ボタンのアクションも見てみましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/c26c6559-e715-af6d-79f6-0e4f8875da4b.png)

* Editor実行時、ランタイム実行時共に
* 呼び出し元は MenuManager で
* PanelManager.OpenPanel 関数を呼び出す
* その際の引数はアニメーターの Settings で

これによって Settings サブメニューに制御が移るわけですね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/767afe88-72b2-cb36-eff7-7714cf8c4a59.png)
サブメニューも原理は同じなので深くは追いませんが、Settings サブメニューにある「Back」アクションの設定は見ておきましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/35b99c6e-3668-4965-f67e-6c517ef2036d.png)
先ほどと同じメニュー遷移で、ただ引数が元のメニューである MainMenu であることだけが異なることがわかります。PanelManager にある OpenPanel 関数が汎用的に利用できることがわかりますね。素晴らしいです。

## ボタン以外のUI部品

「Settings」サブメニューの更に中にあるサブメニュー、例えば「Video」にはボタン以外の部品が使われています。
![qiita.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/5c0664ad-4107-700e-79e9-c7ce9893c599.gif)
Quality の項目ではスライダーが使用されていますし、Anti-Aliasing にはチェックボックスが使用されていますね。これらについても見ておきましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/0fd08ce8-a1ef-0a14-cc6a-24b394311ad2.png)
ヒエラルキー上の表示はこちら。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/0faeaff0-09eb-7c43-7c3c-1a01726a4322.png)

構成を眺めていて驚いたのが、これらスライダーとチェックボックスの実現に、画像を2枚しか使っていないこと！チェックボックスで使用している `Checkmark` 画像を除くと、以下の3つのオブジェクトは同じ画像を使用しています。

* スライダーの背景部分となる Handle Slide Area オブジェクト
* スライダーのツマミ部分となる Handle オブジェクト
* チェックボックスの外箱である Background オブジェクト

これらで共通で使われている画像が、単なる白い、角を丸めた正方形の `SF Generic_0` だったりします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/0cb862ee-e560-c97a-bbdc-f55fc18232b5.png)
この正方形のサイズを変更して、上記の3つのパーツの一部を作成しているわけです。なんと器用な！

そして実際の動作ですが、スライダーには専用のスクリプトがあり、以下のように使用されています。実際にゲームに組み込む場合には、値の変化時のアクションを設定する必要がありそうですね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/57e7865a-c583-c734-8ca0-d172929331e1.png)
またチェックボックス(トグル)にも専用のスクリプトがあり、以下のように使用されています。こちらも実際に組み込む場合には、値の変化時のアクションを設定する必要がありそうです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/b2586a47-6533-8f4b-b36b-909f943f8285.png)
なお今回使用しているこれらのUI部品はアセットの Prefab にまとめられています。これらを利用するだけでokです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/20da89d1-0b89-d413-12a0-25d9dcf35c35.png)
ただ、用意されていない部品は自作する必要があります。これについては別途チャレンジしたいな、と考えていまして、もし何かまとめられたら投稿しますね。

## UI部品間の移動

各UI部品の動作の仕組みはだいたい理解できたと思います。では、メニュー内で、ボタンやスライダーなど各UI部品の間を移動する仕組みはどこで実装されているのでしょうか？

uGUI を利用しようとすると自動的に作成される `EventSystem` に、そのための実装がありました。[Standalone Input Module](https://docs.unity3d.com/ja/current/Manual/script-StandaloneInputModule.html) です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/7feeccd9-ba0a-9b95-2028-bd9b016ae2eb.png)
このモジュールがキーボード、マウス、コントローラーなどの入力を受け、UI部品間の移動や、送信/キャンセルなどのアクションをサポートしてくれているようです。

なおその上にある [Event System マネージャー](https://docs.unity3d.com/ja/current/Manual/script-EventSystem.html) が、イベント開始時の最初の選択位置(今回はMainMenuの「Conrinue」)を指定してくれているようですね。

※ 開始直後に OpenPanel 関数が呼ばれて最初の項目が選択状態になるので、上記 Event System マネージャーの設定は必須ではない気もしています

## 傾くWindow

コントローラー等で操作していると気がつきませんが、マウスで操作するとメニューの Window がマウスの位置によって少し傾くのがわかります。
![qiita.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/2692bd7c-0cdb-a95c-d189-0cf8736f2f8c.gif)
この部分の実装を確認してみましょう。メニュー中の `Window` オブジェクトのインスペクタを見ると、`TiltWindow` スクリプトが指定されているのがわかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/26669827-fa26-7ceb-46e4-a21f8c190119.png)
以下がそのコードです。わりと汎用的に使えそうなので、省略なしでコピペしてみました。

```c#:TiltWindow.cs
using UnityEngine;

public class TiltWindow : MonoBehaviour
{
	public Vector2 range = new Vector2(5f, 3f);

	Transform mTrans;
	Quaternion mStart;
	Vector2 mRot = Vector2.zero;

	void Start ()
	{
		mTrans = transform;
		mStart = mTrans.localRotation; // 傾きの初期値
	}

	void Update ()
	{
		Vector3 pos = Input.mousePosition; // マウスの位置

		float halfWidth = Screen.width * 0.5f;
		float halfHeight = Screen.height * 0.5f;
		float x = Mathf.Clamp((pos.x - halfWidth) / halfWidth, -1f, 1f); // 画面中のマウスのx位置
		float y = Mathf.Clamp((pos.y - halfHeight) / halfHeight, -1f, 1f); // 画面中のマウスのy位置
		mRot = Vector2.Lerp(mRot, new Vector2(x, y), Time.deltaTime * 5f); // 実際の傾きを線形補間

		mTrans.localRotation = mStart * Quaternion.Euler(-mRot.y * range.y, mRot.x * range.x, 0f); // 3次元的に実際に回転させる
	}
}
```

スクリプトに範囲指定の欄があるので、値を10倍にしてみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/008fdb6e-5202-a39b-63b6-456d45978716.png)
すると実行画面で Window の傾きがかなり派手に変化したのが確認できました。
![qiita.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/098b4094-907c-4519-c46d-3dae6fb4e890.gif)

# 参考リンク

uGUI を学ぶ上で参考になった初心者向け資料をご紹介しますね。

* [uGUIの簡単な使い方](https://thinkit.co.jp/article/8420)
* [UIデザイナーが知っておきたいUnityの３つの機能](https://creator.game.cyberagent.co.jp/?p=4478)
* [【本】UnityゲームUI実践ガイド 開発者が知っておきたいGUI構築の新スタンダード](https://www.amazon.co.jp/gp/product/B00U0ZUETW/)

# まとめ

今回は公式アセットの [Unity Samples: UI](https://assetstore.unity.com/packages/essentials/unity-samples-ui-25468) を対象に、含まれている Menu 3D シーンを中心に中身を眺めてみました。

シンプルかつ汎用性のある作りで、これをベースに拡張して自分のゲームのメニューを作成するのは容易だとおもわれます。私も自分のゲームに組み込んでみたいと思いますので、その際の経験なども別途まとめられたらいいな、と考えています。

それではまた！