【Unity】これ以上セーフエリア対応で消耗しないためのアセットを作った【AutoScreen】

<font color="red">2020/8/10追記</font>
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">AutoScreen<a href="https://t.co/1Udni4dqKa">https://t.co/1Udni4dqKa</a><br><br>Unity2019.3以降でエラーが出続ける問題があったので修正してv1.0.1になりました。 <a href="https://t.co/eFXaZ4maJG">https://t.co/eFXaZ4maJG</a></p>&mdash; su10@ハイパーカジュアルゲーム開発 (@su10_dev) <a href="https://twitter.com/su10_dev/status/1292833304194949120?ref_src=twsrc%5Etfw">August 10, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
<font color="red">追記ここまで</font>

----

**[AutoScreen](https://github.com/su10/AutoScreen)（GitHub）**

![AutoScreen_01.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27863/00c476b7-adc2-a981-8136-ec8d35fd8f28.gif)
![AutoScreen_02.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27863/19666be0-5fd5-2966-300e-6886f7beaf1d.gif)

# はじめに
Unityにおけるセーフエリアの対応に関しては`Screen.safeArea`というAPIがあるのですが、ビルドして実機で呼ばないと端末ごとの値が取れない＝エディタ上ではレイアウトの確認ができないという辛さがあります。

最近だとUnite Tokyo 2019でも紹介された[Device Simulator](https://docs.unity3d.com/Packages/com.unity.device-simulator@1.2/manual/index.html)が記憶に新しいところですが、このパッケージはUnity2019.3からでないと使用できず、まだプレビュー版となっています。

* [新機能「Device Simulator」でモバイル開発のイテレーションを加速させよう](https://blogs.unity3d.com/jp/2019/09/27/speed-up-mobile-iteration-with-the-new-device-simulator/)（Unity公式ブログ）
* [【Unity】Device Simulatorでノッチとセーフエリアの対策](http://tsubakit1.hateblo.jp/entry/2019/10/30/235150)（テラシュールブログ）

個人的な感覚ではUnityはたとえLTS版であってもある程度時間が経つまでは本番で使用するのが怖いので、自分が実際にDevice Simulatorを使えるようになるのは2020年の後半になる気がしています。

そんなに待ってられないよ！ということで[UniSafeAreaAdjuster](https://github.com/Goropocha/UniSafeAreaAdjuster)をありがたく使わせてもらってたのですが

* 解像度に合わせて端末を選択するのが面倒
  * 特に複数の`GameObject`に`SafeAreaAdjuster`コンポーネントをつけてるとき
* デバイスのフレーム表示機能がほしい
* 手軽に対応端末を追加・拡張できるようにしたい

という気持ちがピークに達して自分でアセットを作ってしまったのでご紹介します。

便宜上「アセット」と書いてますがアセットストアに公開するのはけっこう面倒なのでGitHubでだけ公開しています。

↓↓↓↓↓↓↓
**[AutoScreen](https://github.com/su10/AutoScreen)（GitHub）**

READMEがまだないのですが、そのうち本記事を元に英語で書くと思います。

# 機能
* モバイル実機上でセーフエリアに応じて`RectTransform`のアンカーを自動調整するコンポーネント
  * `SafeArea`：セーフエリア用（適用したい方向を上下左右自由に組み合わせ可）
  * `UnsafeArea`：非セーフエリア用（上下左右どれか選択）
  * `RuntimeSafeAreaUpdater`：画面の回転を自動検知して`SafeArea`,`UnsafeArea`を更新
* エディタ上でセーフエリアをリアルタイムプレビュー
  * 既存のGameウィンドウでセーフエリアあり端末の解像度を選択するだけ（追加作業なし）
  * 再生/非再生の状態に関係なく即時反映
* on/off可能なオプション
  * デバイスのフレームを表示
  * セーフエリアの境界線表示

# 対応状況
* Unity2018.3以降対応（[Device Simulator](https://docs.unity3d.com/Packages/com.unity.device-simulator@1.2/manual/index.html)は未対応）
* iOS：実機・エディタともに対応
  * iPhone X/XS/11 Pro
  * iPhone XS Max/11 Pro Max
  * iPhone XR
  * iPad Pro (第3世代, 11インチ)
  * iPad Pro (第3世代, 12.9インチ)
* Android：実機のみ対応

Android端末のエディタプレビューは以下の理由でオミットしています。

* 個人的にAndroid端末にビルドする必要性がない
* フレーム画像と解像度・セーフエリアデータの収集に手間がかかる
* 対応が必要な端末数が多そう

端末のマスターデータの追加自体は簡単なので、必要に応じて後述の手順を参考に自分で足してください。

# インストール方法
リポジトリ内に`*.unitypackage`ファイルがあるのでこちらを使用してください。

# 使い方
とりあえず動作を確認してみたい場合は`Demo`シーンを用意してあるのでいじってみたりビルドしてみてください。

## セーフエリアの自動調整機能
`Canvas`直下の`GameObject`に`SafeArea`/`UnsafeArea`コンポーネントを`AddComponent`すると`RectTransform`の`Anchor`が自動で調整されます。直下じゃなくても`Canvas`に至るまでの親`GameObject`の`RectTransform`がすべて縦横に完全にストレッチするようにしてあれば正常に動作します。

<img width="434" alt="スクリーンショット 2019-11-24 11.27.57.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27863/d5677e5d-2dba-f541-d310-51354004c9b3.png">
<I>親`GameObject`が存在する場合の`RectTransform`の設定</I>

## デバイスのフレーム表示/セーフエリアの範囲表示
セーフエリアありの端末解像度を選択するとGameウィンドウの左上に歯車アイコンが表示されるので、そこから表示・非表示を切り替え可能です。

# こだわった点
基本的には以下の３点を追求しました。

* uGUIのオートレイアウトのような簡単で自然な使い心地
* 使ってて細かい挙動を含めイライラしない
* シンプルかつ高い拡張性

## リアルタイムプレビュー
* エディタ上で
* 既存のGameウィンドウの解像度選択を変更するだけで
* リアルタイムに
* 再生/非再生の状態を問わず
* セーフエリアあり端末での見た目が自動で調整

されるので、ビルドしなくてもセーフエリアありの場合のレイアウトを手軽に確認できます。

## 設定変更がGameウィンドウから可能
通常アセットやエディタの拡張の設定は`[MenuItem]`を使用してグローバルメニュー（＋ショートカットキー）から行えるようにするのですが、Gameウィンドウ上に設定のUIを配置することでon/off切り替えの煩雑さを軽減しました。

ちなみにUIの配置にはUIElementsを使用しています。

## 高い拡張性
端末解像度とセーフエリアの情報は`ScriptableObject`を継承したアセットとして保持しているので、追加・拡張が簡単にできます。

`SafeArea`,`UnsafeArea`コンポーネントで満たせない複雑な要件の場合も、`SafeAreaBase`クラスを継承することでエディタ上でのリアルタイムプレビューの機能が簡単に実装できます。

## Gitフレンドリー
セーフエリアの対応は`RectTransform`のアンカーを自動調整することで実現していますが、シーンやプレハブの保存前に必ずアンカーをリセットしています。

これにより「Gameウィンドウで異なる解像度を選択して保存 → Gitでdiffが出る」ということが起きません。

# 実装の詳細
Gameウィンドウの情報を利用するにあたり、Gameウィンドウの実態である`GameView`クラスが公開されていないため、リフレクションを用いてそのデータにアクセスしています。

実際にリフレクションを用いているのは`GameViewProxy`クラスのみで、値の変更については`GameViewEvent`クラスが監視・イベント化しています。

実機のフレーム表示は前もって用意したフレーム画像を描画していて、セーフエリアの大きさは事前にシミュレータビルドで収集した値を`ScriptableObject`を継承した`GameViewScreen`アセットに保存・使用しています。

デバイスのフレーム表示とセーフエリアの境界線表示はそれぞれ`DeviceFrameDrawer`コンポーネントと`SafeAreaDrawer`コンポーネントが行っていて、それらのコンポーネントがアタッチされた`AutoScreenManager`プレハブが自動的にHierarchyに配置されるようになっています。このプレハブインスタンスはシーンやビルドには含まれず、Hierarchyにも表示されません。

# 対応端末追加の手順
エディタ上でのプレビューはマスターデータを追加するだけで簡単に対応機種を増やすことが可能です。

0. Gameウィンドウから手作業で解像度を追加します。自動化したい場合は[GameViewSizeHelper](https://github.com/anchan828/unity-GameViewSizeHelper)を使うと良いです。
0. Projectウィンドウで右クリックし、`Create` -> `ScriptableObjects` -> `GameViewScreen`からマスターデータを保持するためのアセットを作成します。
0. 2.で作成したアセットの各値をInspectorウィンドウから設定します。
  * 解像度やセーフエリアの値はDemoシーンをシミュレータや実機にビルドして確認すると楽です。
  * `Base Text`は1.で追加した解像度の`Label`と同じ文字列にしてください。
  * 必要であればデバイスのフレーム画像を追加し、アセットの`Frame`にセットしてください。
0. 解像度を適当に変更すると反映されます。Unityを再起動する必要はありません。

# Tips
* 自動で追加される`RuntimeSafeAreaUpdater`コンポーネントは画面の回転を許可していないアプリの場合は不要
* 11インチのiPadはGameウィンドウから解像度を登録するとプレビュー可能
  * デフォルトだとUnityに解像度が登録されてない
  * ラベル名は「iPadPro 2388x1668 Landscape」「iPadPro 2388x1668 Portrait」で登録する
* 12インチのiPadの解像度を選択するとセーフエリアあり（第３世代）としてプレビュー表示される
  * 第１〜２世代のセーフエリアなしレイアウトを確認したい場合は適当なラベル名で別途解像度を追加すればOK
* セーフエリア境界線の太さ・色は`AutoScreenManager`プレハブから調整可能
* `SafeAreaDrawer`コンポーネントは単体で実機でも動作可能

# ライセンス
MIT

# 参考
* [GameView.cs](https://github.com/Unity-Technologies/UnityCsReference/blob/master/Editor/Mono/GameView/GameView.cs)（UnityCsReference）
* [GameViewSizes.cs](https://github.com/Unity-Technologies/UnityCsReference/blob/master/Editor/Mono/GameView/GameViewSizes.cs)（UnityCsReference）
* [UniSafeAreaAdjuster](https://github.com/Goropocha/UniSafeAreaAdjuster)（GitHub）
* [NotchSolution](https://github.com/5argon/NotchSolution)（GitHub）

# 最後に
やりたいことや細かい挙動の調整を全部やろうとしたら結果的に

* 新しいプレハブAPIの使い方
* UIElements
* `[ExecuteAlways]`
* `HideFlags`

等々Unityのよく知らなかった様々な機能について詳しく知る良い機会になりました。個別の知見についてはアドベントカレンダーのときにでもまとめたいと思います。

コードにコメント書かないマンなのですがコード自体はシンプルで読みやすいと思うので、わからないことがあったらとりあえずコードを読んでみてください。

実は実践未投入なので、何か不具合があればPRかTwitterへ → [@su10_dev](https://twitter.com/su10_dev)