前回は Zenject 使って複数シーンを跨いで有効なシングルトンなオブジェクトを作ってみました。 アプリ全体で管理したい情報などは、こういうのを使って管理したりすると捗りそうです。
UIWidgets を入れてみよう
ということで今は The Unity って感じの見た目をしているのでカウンターの値を表示する部分を UIWidgets でそれっぽくしてみたいと思います。UIWidgets を使うと画面遷移とかもその場でサクッと出来そうなので、今回のようなものではシーンをわける必要すらないのですが、3D 世界と 2D 世界を行き来するときはシーンでわけたほうがやりやすいケースとかもあるかもしれません。
導入
UIWidgets の GitHub のリポジトリーのリリースページからソースコードを zip で落とします。zip を解凍したフォルダーを Unity のプロジェクトフォルダーの Packages フォルダーにコピーします。 Material Icons を使うためにフォントも入れておきましょう。以下のサイトから ttf 形式をダウンロードします。
そして、コルーチンを await したかったので UniRx.Async も以下のページからダウンロードして入れました。
追加したら、Scripts フォルダーの下に作った Assembly Definition に Unity.UIWidgets と UniRx.Async を Assembly Definition References に追加します。
Zenject と UIWidgets を一緒に使うためのちょっとした下準備をしておきます。ZenjectStatefulWidget クラスを定義して以下のように変更します。
using Unity.UIWidgets.widgets; using Zenject; namespace UnityAppTest { publicclass ZenjectStatefulWidget<TSelf, TState> : StatefulWidget where TSelf : StatefulWidget where TState : State { [Inject] public DiContainer Container { get; set; } publicoverride State createState() => Container.Resolve<TState>(); publicstaticvoid InstallToContainer(DiContainer container) { container.Bind<TSelf>().AsTransient(); container.Bind<TState>().AsTransient(); } } }
DI コンテナにステートとウィジェットの両方を追加したいので、登録漏れが少なくなるように登録用のメソッドも用意しておきました。
まず、最初の画面を作りましょう。先ほど作った ZenjectStatefulWidget を継承する形の Widget と State を定義します。
class HomeWidget : ZenjectStatefulWidget<HomeWidget, HomeState> { } class HomeState : State<HomeWidget> { [Inject] public Counter Counter { get; set; } publicoverride Widget build(BuildContext context) { returnnew Theme( data:new ThemeData(), child:new Scaffold( appBar:new AppBar( title:new Text(data: "Counter app"), actions:new List<Widget> { new IconButton( icon:new Icon(Icons.navigate_next), onPressed: () => Navigator.of(context).pushNamed("/increment") ), new IconButton( icon:new Icon(Icons.web), onPressed: async () => { await SceneManager.LoadSceneAsync("next", LoadSceneMode.Additive); await SceneManager.UnloadSceneAsync("main"); } ) } ), body:new Container( padding: EdgeInsets.all(20), child:new Text(data: $"This is a sample: {Counter.Value}") ), floatingActionButton:new FloatingActionButton( tooltip:"Increment", child:new Icon(Icons.add), onPressed: () => { setState(() => Counter.Increment()); } ) ) ); } }
State も Zenject の DiContainer からインスタンスを作る感じでやるので、Inject 出来ます。あとは適当に画面を作ってインクリメント用のボタンなんかも用意してます。
画面上部に /increment
と別のシーンへ行くためのボタンも用意しました。
次は /increment
で遷移する先の画面も作ります。
class IncrementWidget : ZenjectStatefulWidget<IncrementWidget, IncrementState> { } class IncrementState : State<IncrementWidget> { [Inject] public Counter Counter { get; set; } publicoverride Widget build(BuildContext context) { returnnew Theme( data:new ThemeData(), child:new Scaffold( appBar:new AppBar( title:new Text(data: "Increment"), leading:new IconButton(icon: new Icon(Icons.arrow_back), onPressed: () => Navigator.pop(context)) ), body:new Container( padding: EdgeInsets.all(20), child:new Column( children:new List<Widget> { new IconButton(icon: new Icon(Icons.add), tooltip: "Increment", onPressed: () => setState(() => Counter.Increment())) } ) ) ) ); } }
画面上部のアプリバーに戻るボタンと、画面内にインクリメントするボタンがあるだけです。この画面からはカウンターの値が今いくつなのかは見ることは出来ません。まぁ見れてもいいんですが、前のページと同じになってしまうので、なんとなく出さないようにしました。本当に不便です。
ここまで出来たら UIWidgetsPanel を継承したクラスを作ります。このクラスを Panel のコンポーネントとして追加することで画面が描画されます。
publicclass CounterApp : UIWidgetsPanel { [Inject] public DiContainer Container { get; set; } protectedoverridevoid OnEnable() { Screen.fullScreen = false; FontManager.instance.addFont(Resources.Load<Font>(path: "fonts/MaterialIcons-Regular"), "Material Icons"); base.OnEnable(); } protectedoverride Widget createWidget() { if (Container == null) { returnnew MaterialApp(home: new Scaffold()); } returnnew MaterialApp( home: Container.Resolve<HomeWidget>(), routes:new Dictionary<string, WidgetBuilder> { ["/increment"] = ctx => Container.Resolve<IncrementWidget>(), } ); } }
フォントの読み込みと、/increment
という名前が来たときに IncrementWidget が生成されるような定義が入っています。また、何度やっても Container プロパティがインジェクトされる前に createWidget
が呼ばれて、Container がインジェクトされた後に、また createWidget が呼ばれるとか、あとは再生をしてないときは常時 Container が null みたいなので Container が null の時は真っ白な画面を出すだけにしました。
そして、main シーンにある Canvas を削除します。そして Panel を 1 つ作成して Image コンポーネントを削除します。Image コンポーネントを削除したら、先ほどの CounterApp を Panel のコンポーネントに追加します。この状態でエディター上で実行してみると以下のようになりました。ちゃんと動いてる。
実機で動かそう
実機向けのビルドにあたって少しだけコードをいじって base シーンが読み込まれたときに main シーンも追加で読み込むスクリプトだけ足します。まぁ、これは難しくないのでよしなに。
ビルド時には base, main, next の 3 つのシーンが入っていることを確認します。特に base が初期シーンとして読み込まれるように一番上に持っていきます。そして、今回は Android 向けにビルドしました。実機で動かしてみると…
ちゃんと動いてる。やったね!
ソースコードは以下の github のリポジトリに上げておきました。