コンテンツへスキップ

【Unity】シーン管理に関する機能を追加する「Scene System」を公開しました

  • Unity

Unityのシーン管理に関する機能を追加するライブラリ「Scene System」を公開しました。いつも通りGithubにOSSとして置いてあります。

基本的な使い方についてはREADMEに全部書いてあるのでそちらを見ていただくとして、この記事ではREADMEに書いていないような部分や内部の実装のあれこれを綴っていきます。

このライブラリについて

まずそもそも今回のScene Systemについてですが、記事の冒頭でも書いた通り「Unityのシーン管理に関する機能を追加するライブラリ」です。

Unityにおけるシーンのロードに関してはSceneManagerを用いて行います。が、このクラスには最低限の機能しか搭載されていないので、実際に使う際はほぼ確実に独自のシーン遷移機構を構築していくことになるでしょう。

それを毎回やるのは面倒なので、シーン管理に関する機能をライブラリとしてまとめておくことにしました。このScene Systemには複数シーンの管理やらロード画面の実装やら、シーンを扱う上でよく使いそうな機能が搭載されています。また、開発中の新作でもバリバリ使っている(というかそちらから分離してきた)ライブラリなので、随時機能追加も行っていく予定です。

複数シーンの遷移

Unityのマルチシーンエディティング、便利ですよね。複数のシーンに分割して編集が可能なので、ゲーム部分とUI部分を分割したり、ゲーム中ずっと動かしておきたいものを別シーンで置いておいたり…など、単一のシーンだけでは難しいことが実現可能になるほか、複数人で開発する際に同じシーンを編集してコンフリクトするような事態を避けることができるため、分担してシーンの作業をすることが可能になります。

そんなマルチシーンエディティングですが、面倒なのがシーン遷移。ただでさえ単一シーンの遷移も結構面倒なのに、複数シーンのロードとか…だるい…

という問題を解決するために、Scene SystemではSceneContainerというクラスを用意しました。コンテナにあらかじめ使うシーンを放り込んでおいて、あとはPush/Popを呼べばOK!というお手軽シーン遷移を実現します。

// コンテナを作成
var container = SceneContainer.Create();

// 使うシーンを登録
container.RegisterPermanent("Permanent1");
container.RegisterPermanent("Permanent2");
container.Register("Page1", "Sample1A");
container.Register("Page1", "Sample1B");
container.Register("Page2", "Sample2");

// コンテナを構築 & 非同期で待機
// この時点でRegisterPermanentで登録したシーンがロードされる
var handle = container.Build();
await handle.ToTask();

// Registerで登録したシーンをまとめてロード
handle = container.Push("Page1");
await handle.ToTask();

handle = container.Push("Page2");
await handle.ToTask();

// 履歴がスタックされるため、Popで前のシーンに戻れる
handle = container.Pop();
await handle.ToTask();

// 履歴をリセット & シーンをアンロード
handle = container.ClearStack();
await handle.ToTask();

これだけで複数シーンをまとめてロード/アンロード出来ます。もちろん、普通の単一シーンの遷移に使うことも可能です。

ロード画面の実装

シーン遷移を実装する際、大体のゲームではロード画面も一緒に実装することになります。が、ロード画面の実装は結構面倒な上、表示するものも大体決まっています。そのため、ロード画面用の機能をあらかじめライブラリ側で用意しておくことにしました。

Scene Systemにおけるロード画面の実装にはLoading Screenコンポーネントを利用します。使い方に関してはREADMEで説明している上にサンプルも付いているので、それらを見ればおおむね理解できると思います。

あと、サンプルでもそうなっている通り、Loading ScreenはPrefabとして管理されることを前提に設計されています。ロード画面に関しては専用のシーンを用意しているプロジェクトも多いかもしれない(というかそっちの方が多数派な気がする)んですが、ロード画面をシーンとして分けてしまうと絶妙に扱いづらくなるんですよね。

なので、基本的にはサンプルコードと同じように、ロード画面のPrefabを生成してDon’tDestroyOnLoadに設定した上でWithLoadingScreenに生成したオブジェクトを渡すようにしてください。シーンを分けるやり方でも出来ないことはないと思いますが、色々と面倒そうなのでお勧めはしません…

また、WithLoadingScreenはLoadSceneOperationHandleの拡張メソッドなので、SceneContainerなどの操作にも利用可能です。例えば…

// Build済みの適当なコンテナがあったとして...
SceneContainer container;

// ロード画面を生成
var loadingScreen = Instantiate(loadingScreenPrefab);
DontDestroyOnLoad(loadingScreen);

// Pushの際にロード画面を表示する
var handle = container.Push("Page1")
    WithLoadingScreen(loadingScreen);

// 完了待ち...
await handle.ToTask();

こんな感じでSceneContainerにシーンをPushするタイミングでロード画面を表示する、なんてことも簡単に出来ます。

ロード画面に関する基本的な機能は網羅してると思うので、あとは適宜必要に応じて拡張していただければいい感じになるんじゃないでしょうか…!

非同期メソッドの扱い

シーンのロードに関しては非同期で行うのが一般的です。Scene Systemの非同期メソッドはLoadSceneOperationHandleという独自の構造体を戻り値に持ち、あらゆる方法で待機が可能なように対応しています。

var handle = Scenes.LoadSceneAsync("SceneName");

// コールバックで待機
handle.onCompleted += () =>
{
     Debug.Log("completed");
};

// コルーチンで待機
yield return handle.ToYieldInteraction();

// async/awaitで待機
await handle.ToTask();
await handle.ToUniTask();

が、ぶっちゃけこの方法、めちゃくちゃ面倒くさい…

じゃあ何でこんな方法取ってんだ、という理由については、複数のAsyncOperationを統合して扱う必要があったからです。Scene Systemは複数シーンのロードを行う機能がありますが、それらをまとめて待機する手段としてLoadSceneOperationHandleを用意しています。ロードするのが複数のシーンか単一のシーンかで待機の方法が変わってしまっては困るので。

…というかUnityの非同期メソッドがカオスすぎるんですよね。
非同期メソッドが増えるたびにAsyncOperationみたいな型が乱立されていってるし、これを継承してるせいであちこちにallowSceneActivationが生えてるし、allowSceneActivationをfalseにするとProgressが0.9で止まるみたいな謎の挙動するし…

この辺はAwaitableクラスの登場で変わっていくと思いますが、機能的にはまだまだ不足気味なので当分はこのままでしょう。うーん…

現状Unityで非同期処理を扱う上で最善なのはUniTaskを使うことですが、ライブラリとして公開するものであるためUniTaskがないと動かないというのも…
ということもあって、結局AsyncOperationに対して独自のラッパーを作ることで対応しました。あんまり良くはないですが、使う分にはそんなに不便でもないので許容範囲…かなあ。

ただ、毎回ToTaskとかToYieldInteractionとか呼ぶのは流石に面倒なので、そのうちLoadSceneOperationHandleを直接await / yield returnできるように対応しておきます。それまでは適宜メソッドで変換して使ってください。

まとめ

元々は開発中のプロジェクトのシーン遷移に利用しているものを切り離して切り離してライブラリ化したものですが、なかなか汎用的で便利なものになったのではないでしょうか。特に、マルチシーン構成の際のシーン遷移には結構使えるんじゃないかと思ってます。

あとは、Addressablesの対応やらシーン間での値の受け渡しやらも実装したかったんですが、その辺をやってると新作がいつまでも完成しないので、後回しにして公開しちゃいました。まあ、すでに十分機能もあるしいいでしょう。Done is better than perfect!

というわけで今回のライブラリもOSSとして公開しているので、是非是非使ってみてください!バグや欲しい機能などがあったら教えていただけると嬉しいです〜!

タグ:

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です