【Unity】新規ゲームのUI開発で気をつけた39のTips前編

本記事は[サムザップ #1 AdventCalendar 2020](https://qiita.com/advent-calendar/2020/sumzap1) の12/1の記事です。
**[※後編記事はコチラ](https://qiita.com/ohbashunsuke/items/4c8689410138e07a3461)**

株式会社サムザップで
Unityエンジニアをしているオオバ[@ohbashunsuke](https://twitter.com/ohbashunsuke)です。

前年に引き続き今年もサムザップ社員エンジニア
**全員参加**のアドベントカレンダーが始まりました。


新規ゲームのUI開発で気をつけた39のTips前編ということで、
昨年に引き続きUnityをつかったゲーム開発の中、
特にUIにフォーカスした内容でお届けします。


[昨年のアドベントカレンダー](https://qiita.com/advent-calendar/2019/sumzap1)では
2記事Unity関連のTipsを執筆しています。

- [開発初期に暇を持て余しているUnityエンジニアができる42のTips前編](https://www.shibuya24.info/entry/sumzap_adventcal20191211)
- [開発初期に暇を持て余しているUnityエンジニアができる42のTips後編](https://www.shibuya24.info/entry/sumzap_adventcal20191224)


あわせてどうぞ。


また、Unity UI初心者向けの情報をTwitterで発信しています。
興味ある方はフォローお願いします。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">【Unity】アニメーション装飾したUIの作り方を紹介<br>1⃣連番画像の用意<br>2⃣UnityにSpriteとしてインポート<br>3⃣ImageとAnimatorを作成<br>4⃣SpriteをAnimatorにドラッグ<br>5⃣アニメーションUIの完成<br>普通のUIと同じように配置するだけ。<br>手軽にリッチなUIを作る手法の1つとして覚えておくとよいかと <a href="https://twitter.com/hashtag/UniTips15s?src=hash&amp;ref_src=twsrc%5Etfw">#UniTips15s</a> <a href="https://t.co/vJpwMu823p">pic.twitter.com/vJpwMu823p</a></p>&mdash; オオバ@UIデザイナー支援型Unityエンジニア (@ohbashunsuke) <a href="https://twitter.com/ohbashunsuke/status/1461171050063011842?ref_src=twsrc%5Etfw">November 18, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


## 新規ゲームのUI開発は「気をつけないといけない」ことだらけ

39個の内容を執筆したわけですが、
正直自分でも多いなと思っています。

ただ、仕方ないんですよね。
新規ゲームの立ち上げは、
**気をつけないといけないことだらけ**。マジで。


新規開発の現場経験に基づいた内容を
ありのまま紹介しています。

ちなみに、環境の概要は以下。

- スケジュールは潤沢ではない
- 規模は中〜大
- Unityを使うスマホゲーム

ここ数年新規ゲーム開発の立ち上げに
多く携わりました。

そこで得たUI開発の知見を共有できればと思っています。
ひとつでもみなさまの為になる話があれば幸いです。



## 最初に、おまえ誰だよ？

軽く自己紹介。
大庭俊介（[オオバ](https://twitter.com/ohbashunsuke)）です。
グラフィックデザイナー、Flashデベロッパーを経て、
Unityでスマホゲームを作っています。

Unity歴は8年。
UI開発歴は12年ほど。

コマンドバトル、ピンボール、麻雀など
インゲーム・アウトゲーム・基盤問わず開発に携わってきました。

現在は新規開発に携わりつつ
サムザップエンジニア全体の組織運営に関わっています。

自分の開発思想として
**スピード** を何よりも重要視しています。
ただスピードと言っても雑に作るという意味ではありません。

ゲーム開発に限りませんが
**実機で触って初めて理解** できることが多く、
頭の中では完璧でも
さわってみるとイマイチな事ってたくさんありますよね。

**「作って評価して壊す」**
このサイクルをいかにくり返す事が出来るかが、
ゲームのクオリティに大きく関わってくると考えています。

そして**「作って評価して壊す」**を
くり返すためには**開発スピード**が必要です。


開発序盤は雑でも良いので
要件を満たして実機で触れる状態、
つまり適切に評価出来る状態にいち早く到達させるのです。

評価した結果大半を壊すことになったとしても大丈夫。
早く作っている事で残り時間には余裕があります。
作り直すことで更にブラッシュアップされたプロダクトになる可能性が高いです。

以上の理由から
**スピードを意識したモノづくり**は
利点しか無いです。

今回紹介するTipsは
上記の思想を大きく反映しています。

[弊社エンジニアブログ](https://tech.sumzap.co.jp/)に開発スピードについて
「[新規プロジェクトでスピード開発を実現するエンジニアの挑戦](https://tech.sumzap.co.jp/entry/challenging-development-speed)」
というタイトルで寄稿しています。
興味ある方はどうぞ。


※前置きが長くなりましたが、ここからが本題です。


## ■UI設計思想編

新規のゲームUI開発に限りませんが、
開発で最も重要なのは**「設計」**です。

設計次第で中盤から終盤にかける開発スピード、
発生するバグの数が雲泥の差で変わります。

また中〜大規模のゲーム開発の場合、
**多くの人が関わる**という点もポイントです。
設計次第で**投入した人数の割に成果が出ない**
という事はよくあります。

本セクションでは具体的な
技術というより設計者のマインド寄りの話に触れていきます。


## Tips.1 UI設計者はアプリ全体の設計者でもある

UI設計をするエンジニアは
**アプリ全体の設計をする意識** が必要です。

UI設計と聞いてパッと思いつくものは何でしょうか。

- ボタン
- ダイアログ
- リスト
- テキスト

このようなパーツの開発を思いつくかも知れません。
UI設計はそれだけではありません。

UIはゲーム起動直後から画面に映し出されます。

- シーン遷移
- 画面遷移
- 通信中
- アセットのロード

などなど。
ゲームが起動している最中UIが消えることはありません。
**適切なUXを常に提供** し続けなければならないのです。

その **「適切なUX」** は
「点」の設計では実現が難しく、
全体を包括した「線」、「面」でなくてはいけません。


何が言いたいかというと適切なUIは
仕様書やデザインデータからは伝える事が困難です。

UI設計を担当するという事は、
**ゲーム全体の挙動におけるUXの責任** が乗っかってくるという事です。


## Tips.2 多人数同時開発が可能であることは必須

本Tipsのターゲットは中〜大規模スマホゲーム開発という事で、
**多人数が同時開発できる必要性** があります。

というのも、プロジェクトが進行していくうちに
仕様変更によるスケジュールの遅延、
大量に発生するバグを収束させる為に
人を投入するという事はよくあります。

これらを見越して多くのメンバーが
同時に開発できる設計は必須です。

これは初期の設計段階から意識しておかねばなりません。
途中からでも可能ですが、
大きく手戻りする可能性があるのでオススメしません。

具体的にどうするか見ていきましょう。


例えば以下のような単位での作業分担です。

- 各画面単位
- UIパーツ単位
- シーン単位
- ダイアログ単位

画面であれば1人1画面を担当し、
複数人がそれぞれの画面を同時編集できる開発が望ましいと考えます。

ただし主要画面(例えばホーム、クエストなど)は
頻繁な修正・改修が入ります。

同時に編集する必要が出てくる場合があるため、
その対策も心がけたい所です。

つまり適切なパーツ分けですね。

UnityではPrefab単位にパーツを
切り分けておく事ができます。

1画面1Prefabにして、画面単位を独立させます。
さらにNestedPrefabを使い、
パーツを小分けにして作業分担できるような設計。
そうすることで更に開発の効率は上がります。


具体的な手法は一旦置いといて、
まずはUI設計の思想として
「同時に複数人が開発できる状態をつくる」ということを
念頭におきましょう。

他のメンバーと作業がコンフリクトしにくい環境づくりは、
思っている以上に重要なのです。


## Tips.3 一度決めた設計で無理して突っ走らない

UIの基本実装が落ち着いてきたとしても、
**定期的な設計の見直し** しましょう。

開発の序盤から全てを考慮することは難しく、
ゲーム仕様も変化していきます。

一度決めた設計のから少しずつずれていく事はよくあります。

ずれを感じた際に、思い切って見直す、
具体的には一度作り直すくらいの思い切りも大事です。

というのも、壊す事ができるのは
ゲームがリリースする前までです。

リリース後は壊す暇はなく、売上数字とともに突っ走っていくだけなので...。

繰り返しになりますが、
設計次第で **中盤から終盤にかける開発スピード、
発生するバグの数が雲泥の差** で変わってきます。

設計がゲーム開発にとって最も重要なファクターです。
リリース前に気づけたのならラッキー。
出来る限り良い状態に持っていくことをオススメします。

設計にはとことんこだわり、
今短期的に苦労するか、
長期的、継続的に苦労するか判断してください。


## ■具体的なUI設計編

今まで設計の思想について触れてきましたが、 
ここからは具体的なUIの設計について紹介していきます。

## Tips.4 エントリーポイントの設計で効率が左右する

エントリーポイントとはプログラムの始まりの場所です。

エントリーポイントの設計はかなり重要で、
開発効率を大きく左右します。


本Tipsのエントリーポイントとは、
Unityが隠蔽している厳密なプログラムのエントリーポイントの話ではありません。
Unityエンジニアが実際に書いたC#の始まりの部分を指しています。

エントリーポイントを設計する上で気をつけている事は2点。

1. どのシーンからでも起動できるようにする
2. エントリーポイントという事を分かりやすくする


### 1.どのシーンからでも起動できるようにする

各シーンから直接起動出来ると開発時のストレスが格段に減ります。

ここでは、

- タイトル
- アウトゲーム
- インゲーム

3つのシーンで構成したアプリについて考えてみます。
アウトゲーム、インゲームは担当者が別の場合が多いでしょう。

アウトゲーム担当者は、アウトゲームシーンから、
インゲーム担当者はインゲームシーンから起動して開発したいのです。

これを実現するためにゲーム起動時の
一連の処理をまとめておくと良いでしょう。

- マスターゲット
- ユーザー作成
- ログイン
- チュートリアルスキップ
- アセットダウンロード

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328771/78583eea-c161-8544-1a6a-6220ab5776ae.png" width=640px />
図にするとこんな感じ。

以下のようなエントリーポイントを処理する
**SceneEntryクラス** を定義して
各シーンのルートにAddComponentしておきます。

```csharp
public class SceneEntry : MonoBehaciour
{
	void Awake()
	{
		// Entry Point
		Initialize();
	}
	
	private async Task Initialize()
	{
		// ゲーム全体で最初の1度しか実行しない処理
		await InitializeIfBootScene();

		if (CurrentScene == "TitleScene")
		{
			// タイトルシーンの場合はユーザーの画面タップを待機する
			await WaitTap();
			// GUIと共に初期化処理を実行する
			
//~~~~~ 略 ~~~~~

		}
		else
		{
			// 画面タップを待機することなく初期化処理を一気に走らせる
			await MasterGet();
			await CreateUser();
			await Login();
			await SkipTutorial();
			await DownloadAssets();
		}
	}
}
```

**※ソースコードはあくまでイメージです**


起動フローを通常起動と開発用に分け、
各シーンから起動できるようにします。

もちろん開発が進む中で
**保守するコスト** は生まれます。

しかし実行時のテストのために
毎度タイトル画面から起動する無駄が省けます。


**開発中何度も繰り返される事** は効率化できるポイントです。
今回の場合、Unityエディタの実行といった、
1日になんども行う作業を各シーンから起動できることで
かなり短縮できました。


このように、各シーンから起動できるメリットは大きいのです。


### 2. エントリーポイントという事を分かりやすくする

新しく参画したメンバーがいち早く戦力になってもらうことも
UI設計者のミッションです。

戦力になるためには、
ソースコードの理解は必須。

「学習コストを下げる」ということを意識すると良いです。

エントリーポイントから読んでいくと、
プロジェクトのソースコードは理解しやすいです。


Unityの基本として、ヒエラルキー内コンポーネントの
AwakeやStartメソッドがシーンロード直後に実行されます。

この辺りに目星を付けて、ソースコードを追うのが一般的かなと思います。
しかしプロジェクトによっては **AwakeやStartが
複数存在する** という事もよくあります。

そんな時にひと目でエントリーポイントが分かるようにしておきます。
かなり地味なのですが開発スピードを上げるために大切な事だと思っています。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328771/3870f88b-2369-115c-4f27-d4fff0038eb3.png" width=450 />

上図のようにパット見でエントリーポイントが分かりますよね。
それにともない、Awake、Startの使用を制限もします。

この辺りの話題は、
後述の **「Tips.8 MonoBehaviourクラスのAwake、Startメソッドは極力使わない」** で
解説しているのでそちらをどうぞ。


## Tips.5 シーン遷移システムはラップする

シーン遷移はどのように実装していますか？
Unityが提供するSceneManagerクラスを使うことになりますが、
そのままベタ書きはせず、ラッパークラスを用意することをオススメします。


理由はシーン遷移中に下記のような
UIの表示・機能が必要になるためです。

- Tipsの表示
- ローディングの表示
- シーン間のデータ送信
- アセットのダウンロードまたはロード
- 現在のシーン、遷移するシーンの取得


ソースコードに落とすと以下のような感じです。
**■改善前**
```cs
// シーン遷移前処理
ShowTips();
ShowLoading();
// シーン遷移開始
SceneManaer.LoadScene("InGameScene");

//~~~~ 略 ~~~~

// シーン遷移後にシーン遷移中UIを非表示
HideTips();
HideLoading();
```

**■改善後**
```cs
public IEnumerator LoadScene(string sceneMane)
{
    // シーン遷移前処理
    ShowTips();
    ShowLoading();
    // シーン遷移開始
    SceneManaer.LoadScene("InGameScene");
    
    //~~~~ 略 ~~~~

    // シーン遷移後にシーン遷移中UIを非表示
    HideTips();
    HideLoading();
}
```
ベタで書かずに処理をラップします。

- タイトルシーン
- アウトゲームシーン
- バトルシーン
- バトルリザルトシーン
- ストーリーシーン

といった **多人数開発・リソース管理を考慮してシーン分け** をする事が多いと思います。


上記のような各遷移処理毎に同じようなコードを書きたくないため、
ラッパークラスの中に隠蔽するなどして処理をまとめた方が良いでしょう。

ここではSceneManagerをラップした
**SceneManagerWrapperクラス** を用意したとします。

- シーン遷移開始処理
- シーン遷移完了後の処理

👆 これらのタイミングで実行するデリゲートをセットできるようにします。

```cs:SceneManager.cs
public class SceneManagerWrapper
{
    // シーン遷移開始時に呼ばれる
    public static Action onStartLoad;
    
    // シーン遷移が完了したら呼ばれる
    public static Action onCompleteLoad;

    // シーンのロード開始
    public static async Task LoadSceneAsync(
        string sceneName, object data){ /*割愛*/ }

    // シーン間で渡すデータを取得する
    public static T GetData<T>(){ /*割愛*/ }
//~~~~~ 略 ~~~~~

}
```

👆 **※SceneManagerラッパークラスのソースコードのイメージです**


### ゲーム開始時のタイミングでイベント登録

以下のような感じでシーンの遷移開始・完了時のイベントに 
**ゲーム通して使われる処理を登録** しておきます。
ここではTip/ローディングの表示非表示処理です。

```csharp:
// シーン遷移開始処理
SceneManagerWrapper.onStartLoad += ()=> {
    ShowTips();
    ShowLoading();
};

// シーン遷移完了後の処理
SceneManagerWrapper.onCompleteLoad += ()=> {
    HideTips();
    HideLoading();
};
```

### シーン遷移時の処理

```cs
// シーン遷移開始
await SceneManagerWrapper
    .LoadSceneAsync("InGameScene", new ToInGameSceneData{ battleId = _battleId });
```
シーンをロードする際はこのような感じで実行するイメージです。
この例ではバトルに遷移する想定なので`バトルID`を引き渡しています。

### シーン間のデータ受け取り

```cs
// 遷移先シーンでシーン遷移間のデータを受け取る
var data = SceneManagerWrapper.GetData<ToInGameSceneData>();
```
シーン間でデータをやり取りすることはよくあるので、
SceneManagerWrapperに実装しておきます。

ここでは`GetData`というメソッドを定義しておき、
シーン遷移時に保持しておいたデータを受け取る
という想定で実装しています。

前述の **「Tips.4 エントリーポイントの設計」** で
現在のシーンを取得するAPIをこのラッパークラスで提供しています。

ここまではエンジニアリングな話でしたが後述の
**「Tips.7 画面遷移システムの設計」** も同様ですが、
遷移中のUIをどうするのかを
予めUIデザイナーと連携をとっておく必要があります。

- ローディングを表示する
- 通信中を表示する
- Tipsを表示する
- Tipsを表示するならどこからそのデータを取得するか？
- 表示のアニメーションのタイミング

などなど。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328771/05533d4f-521e-cc2a-a80d-2ccfbb1767e7.png" width=640 />
TitleSceneからOutGameSceneにシンプルに遷移してしまうと、
TitleSceneを破棄したタイミングで一瞬何も無い状態が画面に表示されてしまいます。

この辺りは後述の
**「Tips.6 シーン間を跨ぐものはDontDestroyOnloadへ」** で
説明しています。

以下のようにシーンの継ぎ目が見えないようにするため、
ローディングやTipsを表示させます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328771/eb34b46e-7692-bbb8-8ca4-71dbe628c669.png" width=640 />


この辺りはメモリ解放など技術的な話も多く、
エンジニア主導で話を進めていくことをオススメします。

**システム面、UI/UX面全てを包括** した形で
シーン遷移のシステムは作っていく必要があるということが
お分かりいただけましたでしょうか。


## Tips.6 シーン間を跨ぐものはDontDestroyOnloadへ

- ダイアログ
- ローディング
- タップ・スワイプエフェクト

上記のようなシーンをまたいで使用する共通オブジェクトはシーン遷移で破棄したくないので、DontDestroyOnload領域に生成するようにしています。



前述の **「Tips4. エントリーポイントの設計」** と関連して、 **ゲーム全体で最初の一度しか実行しない処理** の中で実行するような設計になっています。これにより各シーンから実行する際も共通オブジェクトは何も特殊な処理を挟む事なく使用できるようになっています。

注意点としてシーン遷移間で破棄されないため、メモリーリークする可能性があるため注意です。

## Tips.7 画面遷移システムの設計

前述の **「Tips.5 シーン遷移システムの設計」** と似ていますが、こちらは1つのシーン内で発生する画面遷移の話になります。前提の通り複数人で開発出来るよう、1画面1Prefabの単位で分けて設計しています。

### 画面遷移処理の流れ

1. ボタンなどのトリガーから画面遷移リクエストをマネージャに送る
2. マネージャから画面遷移管理へ次の画面遷移をリクエスト
3. 画面Poolerから次の画面を取得(新規生成 or キャッシュを返却)
4. 古い画面と次の画面を入れ替える

図にすると以下のようなイメージです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328771/e1034d5d-cfda-2c42-b4f3-a31c4b169a50.png" width=640 />

画面遷移システムを設計する上で以下のような事を考えておく必要が出てきます。

1. どの画面からも遷移でき、どの画面へも遷移できる
2. Androidバックキー対応
3. 画面遷移中のユーザー入力への対応

### 1. どの画面からも遷移でき、どの画面へも遷移できる

プロジェクト初期は
**「画面Bは画面Aからしか遷移することはありません」**
という仕様だったとしても、

開発中盤で **「やっぱり画面Cからも遷移することになりました!!」**
みたいな仕様変更はよく発生することです。

これは本当によくあることなので、
**予め画面はどの画面からでも遷移できる** ように作って
汎用性を高めておくのが良いです。

### 2. Androidバックキー対応

Androidバックキー対応も画面遷移システムを設計する上で重要なポイントになります。戻るの要件はプロジェクトごとに変わってきますが、基本的には遷移前の画面に遷移するという事になります。実装方法は様々ありますが、例として履歴を保持するやり方を挙げてみます。
戻るが実行されたら画面遷移履歴リストから一つ前の画面情報を取得して遷移させるというやり方です。そのために、画面遷移履歴情報と現在の画面情報を画面遷移マネージャに保持しておきます。


### 3. 画面遷移中のユーザー入力への対応

画面遷移中のユーザー入力に対してどこまで対応するのかは悩みどころです。画面Aに遷移中に画面Bに遷移するユーザー入力を受け取った場合、画面Aに遷移する処理を全てキャンセルする必要があります。

- アセットのロードキャンセル
- ロード済みアセットの破棄
- 各処理の中断

などなど。各画面実装の複雑さが増していき、学習コストが上がります。またこれをテストするコストも高くなります。
以上の事を踏まえると画面遷移中の割り込み処理対応はコスト高めです。
手っ取り早いやり方としては、遷移中はユーザー入力を受け付けないという割り切りも視野に入れて工数を見積もった方がよいと考えています。
画面遷移マネージャが遷移状態を管理するようにして、遷移中はユーザー入力をブロックする処理を実行するような仕組みで実装します。

```cs
// クエストトップへ画面遷移リクエスト
await DisplayManager.Goto(DisplayType.QuestTop, new ToQuestTopData{
    // 表示させるクエストのIDを指定
    questId = _questId
});
```
**※画面遷移サンプルコード**



### その他


また画面全体、一部で使うヘッダやフッターのような **共通UIパーツ** の扱いも設計する上では考えておく必要があります。出来る限り各画面のユニークな実装処理を減らすように基盤システムを構築できるかが、その後の開発効率につながると考えています。


## Tips.8 ダイアログシステムの設計

**「Tips.7 画面遷移システムの設計」** と同様、同時に複数人で開発出来るよう、1ダイアログ1Prefabの単位で分けて設計しています。

```csharp
// アラートダイアログを開く
var alertDialog = await DialogManager.Open(
	new AlertDialogData{ title = _title, message = _message }
);
```
**※ダイアログを開くサンプルコード**

大抵必要になる要件として以下。

1. Androidバックキー対応への配慮
2. 結局ダイアログの上にダイアログはいくつも重なる事になる

### 1. Androidバックキー対応への配慮

Androidバックキー対応は、単純に重なっているダイアログを順番に閉じていく。または閉じてはいけないものもあるので、閉じては行けないダイアログが最前面の場合はAndroidバックキーが効かなくなるといった制御が必要になります。

### 2. 結局ダイアログの上にダイアログはいくつも重なる事になる

デザイナー的にダイアログの上にダイアログを載せたくない要望が出てくる事がありますが、プロジェクトが進んでいくうちに、ショップ購入確認ダイアログや例外処理のエラーダイアログをどうしても重ねざるを得ない状態になったりします。

ということで、僕は理想は追いかけつつも最初からダイアログは重なっていくことを前提に設計を予めしておきます。




## Tips.9 MonoBehaviourクラスのAwake、Startメソッドは極力使わない

若干UI設計とは離れますが、大事な事なので差し込んでいます。
UnityエンジニアにとってのエントリーポイントとなるAwake、Startメソッドはとても便利な半面、多用しすぎると見通しが悪くなる、また処理順を保証できなくなってきます。

```csharp
// A.cs
void Awake()
{
	// Do Something.
}

// B.cs
void Awake()
{
	// Do Something.
}
```

A、Bとコンポーネントヒエラルキーに存在している場合、この2つのAwakeの処理順はどちらが先に実行されるかは保証されません。[^1]
[^1]: Script Execution Orderで指定は可能

処理順を把握できないという事は、 **処理順によるバグを生むリスク** も伴います。

例) `A => B` という順にAwakeが呼ばれている場合は成功していた処理が、`B => A`と呼ばれる事によって不具合が起きるといったものです。

### 改善の一例

下記のようにAwakeの回数を最小限に留めてそれに代わる処理(Initialize等)のメソッドを定義し、明示的に実行させる方が良いと考えています。

```csharp
// A.cs (EntryPoint)
void Awake()
{
	_b.Initialize();
}

// B.cs
public void Initialize()
{
	// Do something.
}
```

個人で開発する時は、逆にAwakeやStartを使わない事が面倒くさいと感じることが多いかもしれませんが、中〜大規模の集団開発となるとAwake、Startの乱用がカオス化を招きます。
Awake、Startなどを使わないルールを設けることで逆に開発の効率が上がるのではないかと思っています。


## Tips.10 Androidバックキーの事を忘れないで

今まで何度か出てきましたが、UIを設計する上で **Androidバックキー** の存在を忘れてはいけません。

常に **「Androidバックキーが今押されたらどうなるだろう？」** という事を頭の片隅に置きつつ設計しておかないと、後から大きく見直す必要が出てきます。

以下のような優先順位で実装しておくと良いかなと思っています。

1. ユーザー入力をブロックしていたら処理しない
2. ダイアログのAndroidバックキー処理
3. 画面のAndroidバックキー処理

```cs
// Androidバックキーが押された時の処理
void OnExecuteAndroidBackKey()
{
    // 画面遷移中などのユーザー入力をブロックしている時はAndroidバックキーは処理しない
    if (UIBlocking.IsEnabled()) return;

    // 画面全面のダイアログを優先して処理(処理を実行したらtrueを返却)
    if (DialogManager.OnExecuteAndroidBackKey()) return;

    // 画面側の戻る処理(処理を実行したらtrueを返却)
    if (DisplayManager.OnExecuteAndroidBackKey()) return;
}
```
Androidバックキー処理のサンプルコードですが、各マネージャクラスにAndroidバックキー処理をリクエストします。

### 常日頃からAndroidバックキーを実行する

画面遷移などの基盤に寄せた処理の場合はAndroidバックキー対応の抜け漏れは発生しづらいですが、 **各画面のユニークな演出中のAndroidバックキー対応は漏れがち** です。
(例 : 強化演出中、ガチャ演出中など)

UnityEditorで開発中はエスケープキーを日常的に押して挙動的に大丈夫か習慣づけておくと良いでしょう。

## Tips.11 戻るの設計

Androidバックキー対応と重複する部分がありますがUI上に表示する **戻るボタンの対応** についてです。
もちろんゲームの仕様によって戻るの要件は様々なので、一概に何が正しいはありませんが **出来るだけシンプルに実装** しておくのが良いと思っています。

例えば以下のような実装が考えられます。

- 各画面の履歴を保持しておく
- 戻るボタンが押されたら履歴から取り出して遷移
- 履歴がなくなったら最初の画面に強制的に遷移
- 画面毎の戻るの挙動を変更する場合は処理をオーバーライド

※画面遷移については前述の **「Tips.7 画面遷移システムの設計」** で説明しているので割愛します。

Unityで戻るを実装する時に複雑化しやすいのは **シーンを跨いだ時の戻るの挙動** です。
シーンを跨ぐと前のシーンの情報は基本的にメモリから破棄されます。履歴情報が破棄された状態から戻るということは、何かしらの情報を元に戻り先を作らなければなりません。

シンプルに最初の画面に戻すといった要件であれば簡単ですが、UX的にどうしてもそれでは不便という事で対応する必要も出て来る場合もあります。


このようなユニークな処理を実現するために、 **「画面毎の戻るの挙動を変更する場合は処理をオーバーライド」** が出来るような設計にして、ある程度の仕様変更に耐えうる状態にしておくのが良いと考えています。


## Tips.12 GameObjectはキャッシュして体感アップ

画面・ダイアログを表示する度にそれらを生成(Instantiate)していては体感が損なう場合があります。プロファイラーで計測すると分かりますがInstantiate処理はCPU負荷が高く低スペック端末だと顕著にフレームレートに響いてきます。

最低でもよく使う画面やダイアログはシーンロード時についでに生成してシーン内にキャッシュしておく設計にしてゲームプレイ中の生成コストを下げ、ゲームの触り心地を良くします。

キャッシュする事で、状態の管理が複雑化してしまいがちですが、そこは基板側の設計で吸収してあげるのが良いと考えています。表示の初期化や後始末忘れを回避するために開発者に提供するメソッドは絞ると良いでしょう。

```csharp
public abstract class DialogBase : MonoBehaviour
{
        // ダイアログを開く前に呼ばれる初期化メソッド
        // キャッシュを再利用した際のリセット処理も兼ねる
        public virtual async Task ReInitialize(){}
        
        // ダイアログを開くメソッド
        public virtual async Task Open()
        {
                // 共通のダイアログ表示アニメーション
                // 特殊ケースの場合は上書きする
        }

        // ダイアログを閉じるメソッド
        public virtual async Task Close()
        {
                // 共通のダイアログを閉じるアニメーション
                // 特殊ケースの場合は上書きする
        }

        // ダイアログが閉じてアセットの破棄などを実行するメソッド
        public virtual void CloseComplete(){}
}
```

ソースコードはイメージですが、上記のような感じでベースクラスで各種イベントで実行されるメソッドが定義されていて、それをサブクラス側で具体的な処理を書いていきます。

## Tips.13 UIアニメーションの実装方法基準

UIアニメーションをどうやって実装するかは悩み所です。

僕も毎度とても悩んでいます。ただ、何度か経験する中で一つの基準が出来たので紹介します。

結論から書きます。

- 再利用するものはスクリプト
- 再利用しないものは何でもいい

極端に聞こえるかもしれません。詳しくは今年11月のUnity勉強会「yokohama.unity#4」で発表した資料がありますので、ご参考にどうぞ。

<a href="https://speakerdeck.com/ohbashunsuke/yokohama-unity4"/>
<img src="https://files.speakerdeck.com/presentations/321ec356a53e4d71925b951dbc19a5de/slide_0.jpg" width=600 />
</a>[LT版クリエーターとUnityエンジニアの狭間でUIアニメーションを設計する3つのTips/yokohama-unity4 - Speaker Deck](https://speakerdeck.com/ohbashunsuke/yokohama-unity4)

※UIアニメーションを具体的にどう作るかといったワークフローについては後編で執筆予定です

## ■後回しにしない方が良い事編

当たり前だと思いますが **影響範囲が大きいもの** は早めに設計の目処をつけておきたいです。

この章では、明らかに最初に手を付けておかないといけないものから、 **パッと見後から実装しても大丈夫そうに見えるけど序盤にケリをつけておいた方が良さそうなUI周りの設計** について紹介していきます。


## Tips.14 UI解像度を決めてから本格的に動き出す

UIの解像度は開発序盤に決めておく必要があります。これが決まらないとデザイナーの元素材の解像度が決まらず、デザイナー・Unityエンジニア共に作業が出来ません。
**※作業出来なくもないですが、高確率で作り直し確定です。**

ちなみに僕は以下のような決め方をしています。

- 基準となるスマホ端末を決める
- 検証用の仮UIを用意して解像度毎に実機で確認

### 基準となるスマホ端末を決める

基準となるスマホ端末の決め方は、そのゲームのメインターゲットや戦略、その時その時の状況によって変わるためプロジェクト内で話あって決めます。

### 検証用の仮UIを用意して解像度毎に実機で確認

大事なのは **スマホ実機で実際に見て確認** するです。

ターゲットユーザー層に対してどのくらいのクオリティを提供しないといけないかプロデューサー・プランナー・デザイナーと相談することになります。

繰り返しになりますが大きな手戻りが発生する可能性があるため、UI解像度を決めてから本格的に動き出すことをオススメします。


## Tips.15 セーフエリア対応

iPhoneX以降対応する必要のあるセーフエリアですが、これも後回しにすると地獄を見る案件です。

**「セーフエリア対応処理を入れたらUIが重なって全画面調整する必要が出てきた」** といった事がプロジェクト後半UIが量産された状態で発生すると深刻な手戻りコストになります。

そうならないように、 **開発序盤** からセーフエリアに対する要件を決めて設計しておく方が良いでしょう。


## Tips.16 UIの階層関係を決めておく

経験上以下のような階層構造になることが多いです。
**※上から順に前面に表示されるもの**

1. タップエフェクト
1. ローディングなどの最前面表示UI
1. システム系ダイアログ
1. 通常ダイアログ
1. コンテンツUI
1. 背景

システムダイアログ、通常ダイアログはそれぞれの階層で複数ダイアログが重なるような設計にする事になります。

タップエフェクトもこの階層を決める段階で実装しておく方が良いと考えます。パッと見後から実装しても大丈夫そうではありますが、階層構造を後から手を加えるのはリスクなので、序盤に仕組みを固めておくことを心がけています。


## Tips.17 通信・アセットのダウンロード・ロード処理

**「Tips.1 UI設計者はアプリ全体の設計をする必要がある」** と関連した内容で、本来のUI開発の範疇なのかは微妙なラインですが、通信周りの設計は **UXのクオリティ** を上げるために無視できません。

開発序盤のタイミングで、エンジニア側から通信やロードタイミングのすり合わせをUIデザイナーとしておくと良いでしょう。UIデザイナー側、Unityエンジニア側共に譲れない、譲りづらい部分があると思います。その辺りを開発序盤から話し合っておくと後々のトラブルが少なくなる印象です。

よく発生しがちなケースとして、UIデザイナーが **想定していないタイミングで通信処理** が走っていて、 **予想していたタイミングで表示されない** といった認識のズレです。

仕様の変更やブラッシュアップで通信のタイミングが増加、変更する事は、開発中によくありますが、 **UXを損なっていないかという視点** も大切です。

通信やアセットのロードのタイミングなどに変更が入る場合はUIデザイナーと認識を合わせておくと良いでしょう。


## 最後に

いかがでしたでしょうか。スケジュールが潤沢ではないUnityを使った中〜大規模スマホゲーム開発におけるUI開発前編(Tips.1〜17)を紹介してきました。

- Tips.1 UI設計者はアプリ全体の設計をする必要がある
- Tips.2 多人数開発が可能であること
- Tips.3 一度決めた設計で無理して突っ走らない
- Tips.4 エントリーポイントの設計
- Tips.5 シーン遷移システムの設計
- Tips.6 シーン間を跨ぐものはDontDestroyOnloadへ
- Tips.7 画面遷移システムの設計
- Tips.8 ダイアログシステムの設計
- Tips.9 MonoBehaviourクラスのAwake、Startメソッドは極力使わない
- Tips.10 Androidバックキーの事を忘れないで
- Tips.11 戻るの設計
- Tips.12 GameObjectはキャッシュして体感アップ
- Tips.13 UIアニメーションの実装方法基準
- Tips.14 UI解像度を決めてから本格的に動き出す
- Tips.15 セーフエリア対応
- Tips.16 UIの階層関係を決めておく
- Tips.17 通信・アセットのダウンロード・ロード処理

何か1つでも役に立つものがあれば幸いです。
続きのTips.18〜39は以下の内容について書いていくつもりで[12/22](https://qiita.com/advent-calendar/2020/sumzap2)リリース予定。

**[※後編リリース](https://qiita.com/ohbashunsuke/items/4c8689410138e07a3461)しました。**

- UIレギュレーション編
- UIアセットワークフロー構築編
- UI担当Unityエンジニアマインド編
- UI開発Tipsおまけ編


2020年Qiitaアドベントカレンダーは始まったばかりです。
楽しんでいきましょう。

明日は[@Gaku_Ishii](https://qiita.com/Gaku_Ishii)さんの[「C#のネイティブ関数呼び出し（P/Invoke）時に行われていることを調べてみた」](https://qiita.com/Gaku_Ishii/items/8920f2ce1b880d161345)です。お楽しみに！！

サムザップのアドベントカレンダーは人数の関係上2つあります。
こちらもよろしくお願いいたします！
[サムザップ #2 Advent Calendar 2020 - Qiita](https://qiita.com/advent-calendar/2020/sumzap2)


↓↓初心者向けのUI開発Tips記事を書きました。↓↓
**こちらもオススメ**
👉[【初心者向け】Unityを使ったUI開発で大事にしている9つのTips](https://shibuya24.info/entry/unity_ui_9tips)