Unityで作る最強(当社比)のシーン管理 その1

# 動機

Unityといえば今をときめくゲームエンジンなので、シーン遷移管理についてもさぞかし先進的な機能が搭載されているのだろうと思いきや、案外そうでもなくて前業務では結構大変な目に会いました。無策で次の業務に突っ込むとまた同じ苦労を背負い込むことになろうと考えて、最高のシーン遷移管理エンジンはどんなものだろうかと試作してみた結果のレポートが本稿になります。

本稿で説明するシーン遷移管理エンジンの特徴は、ファイルシステムやURLのような階層型パスを使って任意の場所にジャンプすることができ、移動元と移動先の間のトランジションを階層を意識した形で自動で処理してくれることです(hierarchical state machine的な考え)。またそのジャンプの間に動的にUnityシーンのロード(Additive)が含まれることも設計に取り込まれており、ゲームシーン全体を階層的データ構造の中で扱うことができます。チュートリアルやショートカット、特殊イベントなど特殊な処理をするときにこの仕組が生きてきます。

このフレームワークでは、UniRxを階層構造上の情報の伝播に利用しています。正直大したことしてなくて自力で実装しても構わなかったのですが、次の業務でUniRxを導入すべきか否かの判断のために触っておこうとという考えで採用しました。まあ一応多少コード減ったっぽいのでよかったんじゃないかな、というのが現在の評価です。同様にZenjectも試してみたのですが、どうも問題を余計面倒にしているようにしか思えなかったので、ここでは不採用としました。動的なシーンロードのときには使いたい人は使ってもいいのではないかという気持ちです。

ということで、以下で設計と使い方の説明をしていきます。

リポジトリは[こちら](https://github.com/jonigata/routene)。route+sceneでrouteneと名付けました。今回はAssets/Samples/SingleSceneを利用して説明します。このサンプルシーン専用のC#コードは一応ゼロです(このフレームワークとあまり関係ない自作汎用コンポーネントは含まれているので、微妙な話ですが)。

# 基本形

まず単純な構成ということで、Unityのシーンファイル1つで完結する構造を構築します。

## 準備

Canvasを複数枚用意しておき、フッターボタンの押下によってページを切り替えるという構造を考えてみます。テンプレソシャゲのホーム画面っぽい構造です。

![game_screen.png](https://qiita-image-store.s3.amazonaws.com/0/18886/a3c4bfab-38c7-9f47-354e-7239bbb7122b.png)

フッターボタンを3つ用意し、それに対応するページも3枚用意しておきます。ページごとにそれぞれ別のCanvasとしています。

![screen_hierarchy.png](https://qiita-image-store.s3.amazonaws.com/0/18886/5e81cfc8-d12c-8756-9f78-be203f61bef2.png)

ここで、`PageA` `PageB` `PageC` `Footer` がそれぞれCanvasで、`Footer`だけ`SortOrder`が手前になっています。

わかりやすいようにページの色を変えておき、ボタンを押すとそれぞれのページをアクティブにするようにしておきます。余談ですが、ここで`Page`と`PageGroup`という自作コンポーネントを使用しています。これはToggleとToggleGroupのように一つのページだけアクティブにする(同じ`PageGroup`に所属する別の`Page`がアクティブになると非アクティブになる)コンポーネントです。


## ルータを置く

ここで、「ルータ」という概念を導入します。path的な文字列を解釈して、適切な状態に遷移するオブジェクト群です。

routeneでは、このルータの設定時にUnityのヒエラルキーを利用します。このような構造です。

![router_hierarchy.png](https://qiita-image-store.s3.amazonaws.com/0/18886/c651403d-3c4c-407f-0a40-2be30c4d8ecf.png)

### root

ルータのrootとなるオブジェクトには、routeneのコンポーネント「`Routing`」と「`Router`」をアタッチします。

![router_inspector.png](https://qiita-image-store.s3.amazonaws.com/0/18886/69c0df8c-21e8-f472-696a-18499c834763.png)

`Routing`は外部から操作するときのインターフェイスで、`JumpTo(path)`などのインターフェイスを備えています。基本的にシングルトン的な運用が前提にされており、この`JumpTo`はstaticメソッドになっています。とはいえ本当に1つしか存在できないとシーン分割して開発するときに不便なので、priorityを設定することができるようにしてあり、複数のオブジェクトが存在する場合はもっともpriorityの小さいものが使われます。

`Router`はパスツリーのノードを表現するオブジェクトで、ツリーを構成するすべてのノードにこの`Router`か`Router`から派生するコンポーネントを貼り付けます(このツリーを作るのが微妙に面倒なので、あとでエディタ拡張で対応する予定です)。このオブジェクトをツリー構造のrootにします。

`Routing`の「Concrete Router」フィールドにはこのrootの`Router`を設定します。必ずしも`Routing`とルートの`Router`が同じオブジェクトに貼り付けてある必要はない(`Routing`はパスツリーと別の場所にあってもよい)ので、適宜判断します。

要するに、下図のようなヒエラルキーを作ろうという話で、この図ではRoutingとrootのRouterのGameObjectが別になっていますが、一緒でも構わないということです。

![routene.png](https://qiita-image-store.s3.amazonaws.com/0/18886/b2c54041-ffda-f3d9-d3eb-258195e23cba.png)

### root以外

ルータのツリー構造の構築にはUnityのヒエラルキー構造をそのまま利用しますので、子ノードを作りたいときはUnityのヒエラルキーツリー上で子オブジェクトを作り、そこに`Router`の派生コンポーネントを設定します。

このとき、そのノードに来たときに生じる遷移のタイプによって、どの`Router`派生コンポーネントを選ぶかが決まります。一瞬で遷移して良いときは`FastRouter`、フェードイン・フェードアウトなどのエフェクトが入るときは`SlowRouter`を選びます。`SlowRouter`は若干ややこしいので後日説明することにして、ここは`FastRouter`を選びましょう。

### トリガ

`FastRouter`/`SlowRouter`には「ルーティングトリガ」というものを設定できます。これは、該当するノードが`JumpTo`でアクティブになったときに実行されるものです。何種類か用意しましたが、今回は`ActivationRoutingTrigger`を使ってみましょう。これは、トリガされるときに指定したオブジェクトを`SetActive(true)`するものです。これに、`PageA`(Canvasの方)を設定してみましょう。すると、このパスに来たときに`PageA`がアクティブになります。

Routingは前回のパスも覚えていて、`JumpTo`するときに前回のパス(の今回のパスと共通でない部分)に対してLeaveイベントを発火してくれるので、`ActivationRoutingTrigger`はこれを利用して指定オブジェクトを`SetActive(false)`します。

この機能があるので、実は`Page`/`PageGroup`コンポーネントはつけなくてもページ切り替えできるのですが、Page機能は起動時に「1つだけActiveな状態」を正しく設定してくれるので、シーンのセーブ時に毎回各Pageのアクティブ状態を手動で設定するのが面倒なので使っています。

特にトリガを必要としない、ファイルパスで言うところのディレクトリのようなノードでは、生の`FastRouter`/`SlowRouter`の代わりに生の`Router`をアタッチします。

子ノードのインスペクタは次のようになります。

![child_inspector.png](https://qiita-image-store.s3.amazonaws.com/0/18886/f60f29fc-6e30-046c-819e-1ca224af0c5c.png)

### ヒエラルキーツリーとの関係

routeneでは、前述の通りTransformの親子関係を流用してパスツリーを構築します。特にそうしなければならない理由はないのですが、パスツリーをUnityのヒエラルキーツリー上で確認できたほうが便利だろうということでそうしてあります。

上のほうでもちらちら出てきたとおり階層化も可能で(後日説明します)、パス上ではファイルシステムと同様に'`/`'区切りで指定します。また先頭に'`/`'が必要です

### ルーティング

この時点で、ルーターの完成です。`Routing.JumpTo("/PageB")`などとすることで、PageBに設定されたトリガが発動します。

ここで、フッターのボタンを、直接Pageをアクティブにするのではなく`Routing`経由でジャンプするようにしてみましょう。staticメソッドはUnityEventに設定できないし、C#でstaticメソッドと同名同シグネチャの非staticメソッドを作ることはできないようなので、仕方なく`Jump`というコンポーネントを作りました。それも`Router`オブジェクトに貼ったら、ボタンのOnClickでそのJumpコンポーネントのJumpToメソッドを呼び出すようにしましょう。引数はパスですので、`"/PageA"` `"/PageB"`などとします。

// インスペクタ button_inspector
![button_inspector.png](https://qiita-image-store.s3.amazonaws.com/0/18886/cb4cd9a8-ad7b-a89f-ae21-1c7396c1507b.png)

# まとめ

- Unityのヒエラルキーでパスツリーを表現し、パスツリー上のすべてのGameObjectにRouter(もしくはその派生)コンポーネントをアタッチする
- なにかを実行させたいパスツリー上の場所にはFastRouterを設定し、RoutingTriggerを設定する
- `Routing.JumpTo("/PageA")`

# 次回以降の予定

- [その2 階層構造の説明](https://qiita.com/jonigata/items/c9b1a94b2a49cd67bb4a) 
- SlowRouterの説明
- 動的なシーンのロード
- なぜこのような（若干面倒にも見える）構造になっているのか、の説明

全体をnamespaceでくるむ、一部フィールド名の変更なども行う予定です。