Unity5 マルチシーン編集によるUI開発事例

スクリーンショット 2017-04-10 15.40.34

はじめに

この投稿では、Unity でのネイティブアプリゲーム開発において、UI実装をマルチシーン編集で行った経緯と、その方法について紹介します。

上図の画面は現在開発中のものですが、複数のシーンによって構築されていることが分かります。

 

開発環境

この投稿を執筆した段階での Unity バージョンは 5.5.1f1 になります。

UIフレームワークは uGUI を使用しています。

マルチシーン編集とは

マルチシーン編集は Unity5.3 から導入された機能です。

マルチシーン編集の大きな特徴として、以下が挙げられます。

  • エディタ上で複数シーンを開くことができる
  • 実行時に複数シーンを管理できる

上記の特徴から、オープンワールドのような巨大な環境をシームレスに遷移するための仕組み作りや、巨大な一枚岩のシーンを分割して開発を分担するワークフロー改善の用途が見込まれます。今回の投稿では、このマルチシーン編集機能をUI開発に導入することで、その有用性を見ていきます。

UI開発の問題

シンプルなUIであれば、単一シーン内で完結できるため問題にはなりませんが、複雑なUIを開発するときに以下の問題が発生します。

  • シーン間で同じUI部品が混在する
    • 複雑なゲームになると、UIは画面単位で設計されるため、各画面を個別のシーンで管理するようになります。しかし画面間で共通のUI部品を使用したい場合に対応できません。
  • 作業分担ができない
    • シーンは基本的にマージができないため、巨大な単一シーンになると複数人での開発ができなくなります。

UIをプレハブで管理する

マルチシーン編集が登場する以前は、上記の問題を解決するため、ほとんどの場合はプレハブを使用していました。UI部品をプレハブに分解し、部品単位で担当を割り当てて開発を行い、完成したプレハブを納品するというフローです。

しかし、プレハブでの管理には以下の問題があります。

プレハブを入れ子にできない

例えば以下のヒエラルキーがあったとして、Parent オブジェクトをプレハブ化した時点で、Child のプレハブの情報は失われてしまいます。プレハブを入れ子にした時点で Child オブジェクトの変更は反映されなくなります。

スクリーンショット 2017-04-10 16.59.52

スクリーンショット 2017-04-10 17.22.47

これを回避する方法は二つで、親のプレハブ化を行わないか、子のプレハブを動的に生成するかのいずれかです。

プレハブを格納するシーンが増える

UI部品を全てプレハブで管理すると、プレハブ編集用のシーンを各担当者が作成することになり、管理が煩雑になります。

UIをマルチシーンで管理する

プレハブでのUI管理から、シーン管理に切り替えることで、どのような効果があるのかについて説明します。

レイアウト編集しやすい

UI部品をプレハブで作ると、入れ子問題を回避するためにUI部品を動的に生成するようになります。しかし、UI部品が動的にシーン上に生成するようになると、エディタでの細かいレイアウト調整ができなくなるため、アーティストの作業効率が低下します。

マルチシーン編集では、エディタ上で複数シーンを同時にプレビューできるため、最終的なアウトプットを確認しながら作業が可能です。

作業分担が容易

シーンがUI部品単位に分離されることで、開発担当を明確に区分できるようになります。

ファイル管理がシンプル

プレハブでUI部品を管理していると、プレハブと編集シーンを同時に編集する必要がでてきます。複雑なUIになるほど、プレハブの Apply 忘れやシーンのコミット漏れなどのヒューマンエラーが増加します。

マルチシーン編集を使用すると、編集するのはシーンだけになるため手違いが減ります。

UIマルチシーン開発の方法

では、実際にUI開発をマルチシーン編集で行う方法について説明します。

シーンの構成

シーンを作成する前に、複数のシーンをどのように構成していくのかについて説明します。

シンプルなゲームであれば1つの画面で完結できますが、画面がいくつもあるような複雑なゲームになると、UIは画面単位で作る必要があります。この1画面という単位を1シーンとして対応させます。

次に、各画面で共通で使用するUIを分割します。例えば、共通で使うダイアログやヘッダーが該当します。これらをそれぞれ1シーンとして管理します。なお、同じ加算シーンを複数生成することはできないことに注意してください

上記の手順で分解したシーンを二つに大別します。

  • 親シーン
    • 画面に対応
    • ゲーム上で常に1つで、2つは存在しない
    • 親シーンを呼び出すと、既存の全シーンをゲームから消去する
  • 加算シーン
    • UI部品に対応
    • ゲーム上に複数存在する

UIシーンの作成

UIシーンをどのように作成していくかについて説明します。

ヒエラルキー上にルートとなるキャンバスを作成します。このとき、キャンバスの設定をシーン間で共通にします。

スクリーンショット 2017-04-10 17.37.41

キャンバスの Render Mode は Screen Space – Camera を使用します。Screen Space – Overray という選択肢もありますが、UIエフェクトをパーティクルで実装する際に問題になります。

Render Mode を Screen Space – Camera にした場合は、 Render Camera にカメラの指定が必要になります。シーン上でUI開発する上でカメラは必須なので、各シーンでカメラを配置します。ただし、ここで配置したカメラはUIシーンを結合するときに削除することに注意してください。

UI部品をシーンで分割したときに、シーン毎にカメラを持ってしまうと、カメラの管理が煩雑になるだけでなく、描画パフォーマンスに影響が出てしまいます。そのため、UIで使用するカメラは共通で1つだけにします。
以下は、加算シーンのルートキャンバスのカメラを親シーンのルートキャンバスのカメラに差し替えるサンプルコードになります。

void Awake () {

// 親シーン(Root)のルートキャンバスを取得する
var rootCanvasParentScene = SceneManager.GetSceneByName ("Root").GetRootGameObjects ()
.First (obj => obj.GetComponent<canvas> () != null)
.GetComponent<canvas> ();</canvas></canvas>

// 自身のシーン(Additive)のルートキャンバスを取得する
var rootCanvas = GetComponent<canvas> ();</canvas>

// 自身のシーン(Additive)のルートキャンバスのUIカメラを削除する
if (rootCanvas.worldCamera != null) {
Object.Destroy (rootCanvas.worldCamera.gameObject);
rootCanvas.worldCamera = null;
}

// 自身のシーン(Additive)のルートキャンバスのUIカメラを親シーン(Root)のカメラに置き換える
rootCanvas.worldCamera = rootCanvasParentScene.worldCamera;
}

シーンの呼び出し

作成したシーンをどのように呼び出すかについて説明します。基本はシンプルで、シーン読み込み時時に LoadSceneMode を切り替えるだけです。親シーンは LoadSceneMode を Single に、加算シーンは Additive にします。

// 親シーンの読み込み
UnityEngine.SceneManagement.LoadScene("Parent", LoadSceneMode.Single );

// 加算シーンの読み込み
UnityEngine.SceneManagement.LoadScene("Additive", LoadSceneMode.Additive );

検証のため、空のシーンを2つ作成して、それぞれシーン名を ParentAdditive にします。さらに別の Root シーンを用意して上記のスクリプトをそれぞれ実行すると、ヒエラルキーは以下のような結果になります。

スクリーンショット 2017-04-10 18.26.56

スクリーンショット 2017-04-10 18.21.25

親シーン(Parent)を呼び出したときは元シーン(Root)は破棄され、加算シーン(Additive)を呼び出したときは破棄されませんでした。

加算シーンの中でも、親シーンの読み込みに関わらず破棄せずに残したい場合があります。この場合には加算シーンのルートオブジェクトに DontDestroyOnLoad フラグを付与します。こうすることでシーンをまたいでもUI部品はメモリから破棄されなくなります。

IEnumerator Start () {
// 加算シーン読み込み
SceneManager.LoadScene ("Additive", LoadSceneMode.Additive);

// シーン読み込み待機
var scene = SceneManager.GetSceneByName ("Additive");
yield return new WaitUntil (() => scene.isLoaded);

// ルートオブジェクトを取得して DontDestroyOnLoad フラグを付与
var objs = SceneManager.GetSceneByName ("Additive").GetRootGameObjects ();
foreach (var obj in objs) {
Object.DontDestroyOnLoad (obj);
}
}

1点注意すべきなのは、ヒエラルキー上では DontDestroyOnLoad シーンの管理下になります。元々の加算シーン(Additive)に含まれていたオブジェクトが全て DontDestroyOnLoad 直下に移動していることに注意してください。

スクリーンショット 2017-04-10 18.38.30

導入の障害と対策

UI開発をマルチシーンで行なう際の障害とその対処法について紹介します。

EventSystem の扱い

EventSystem は、基本的にシングルトンとして使用することを期待しているので、マルチシーン編集で EventSystem を使用する際に問題になります。加算シーン上で UIイベントの動作を検証するために EventSystem をヒエラルキー上に配置する必要があるので、EventSystem インスタンスが存在しなかった場合に動的に生成するよう対応します。

void Awake () {

// EventSystem シングルトンインスタンスが存在しない場合、
// EventSystem を動的に生成する
if (EventSystem.current == null) {
var instance = new GameObject ("EventSystem");
EventSystem.current = instance.AddComponent ();

// InputModule も同じタイミングで追加する
instance.AddComponent ();
}
}

アセットバンドルの管理

シーンをアセットバンドル化する際に起こりやすい問題について説明します。

ネイティブアプリ開発では、ユーザにアプリをダウンロードしてもらいやすくするために、アプリサイズの削減を行ないます。そのため、画像やサウンドデータなどのサイズが大きいデータについてはアセットバンドル化してビルドから除外します。UI部品で使用しているアトラス画像は解像度が高くなる傾向があるので、アセットバンドル化してビルドから除外する第一候補になります。

アトラス画像のアセットバンドル化で注意する必要があるのは、ビルドに含めたシーンがアトラスを使用していた場合、 Sprite のリンクが切れて Missing が発生してしまうことです。リンクが切れる理由は、アセットバンドルの参照解決の仕様で、アセットバンドルの参照は参照元もまたアセットバンドルでないと自動では参照解決できないためです。

上記のリンク切れの問題を解決する方法は以下の二つです。

  • 自力で参照解決を行なう
  • 参照元のシーンもアセットバンドル化する

我々のプロジェクトでは、参照元のシーンもアセットバンドル化することで対応しました。

UIシーンのアセットバンドル化で注意すべきことは、複数シーンで共通のアトラスを使用していた際には、アトラスとシーンをそれぞれ個別にアセットバンドル化する必要があります。なぜなら、それぞれのシーンのアセットバンドルに同じアトラスが含まれてしまい、ストレージとメモリを圧迫してしまうからです。リソースがメモリに重複して読み込まれてしまう問題については MemoryProfiler などのツールを使用して定期的にチェックするのが望ましいです。

おわりに

UI開発のマルチシーン編集の導入は、ゲーム開発のワークフローをどのように整備していくかという問題に対する、チームなりの1つの回答になります。チームのメンバ構成やスキルレベルが違えば、全く異なったワークフローの選択肢もあったかもしれません。

この投稿を読んだ開発者の皆さんが、ゲーム開発のワークフローを選択する際の一助となれば幸いです。