Quantcast
Channel: かずきのBlog@hatena
Viewing all articles
Browse latest Browse all 1387

Unity でも DI 使ったりしたいし画面もいい感じに作りたい「Zenject & UIWidgets」 その 2

$
0
0

前回は Zenject 使って複数シーンを跨いで有効なシングルトンなオブジェクトを作ってみました。 アプリ全体で管理したい情報などは、こういうのを使って管理したりすると捗りそうです。

blog.okazuki.jp

UIWidgets を入れてみよう

ということで今は The Unity って感じの見た目をしているのでカウンターの値を表示する部分を UIWidgets でそれっぽくしてみたいと思います。UIWidgets を使うと画面遷移とかもその場でサクッと出来そうなので、今回のようなものではシーンをわける必要すらないのですが、3D 世界と 2D 世界を行き来するときはシーンでわけたほうがやりやすいケースとかもあるかもしれません。

導入

UIWidgets の GitHub のリポジトリーのリリースページからソースコードを zip で落とします。zip を解凍したフォルダーを Unity のプロジェクトフォルダーの Packages フォルダーにコピーします。 Material Icons を使うためにフォントも入れておきましょう。以下のサイトから ttf 形式をダウンロードします。

github.com

そして、コルーチンを await したかったので UniRx.Async も以下のページからダウンロードして入れました。

github.com

追加したら、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 のコンポーネントに追加します。この状態でエディター上で実行してみると以下のようになりました。ちゃんと動いてる。

f:id:okazuki:20191219161604g:plain

実機で動かそう

実機向けのビルドにあたって少しだけコードをいじって base シーンが読み込まれたときに main シーンも追加で読み込むスクリプトだけ足します。まぁ、これは難しくないのでよしなに。

ビルド時には base, main, next の 3 つのシーンが入っていることを確認します。特に base が初期シーンとして読み込まれるように一番上に持っていきます。そして、今回は Android 向けにビルドしました。実機で動かしてみると…

f:id:okazuki:20191219170432g:plain

ちゃんと動いてる。やったね!

ソースコードは以下の github のリポジトリに上げておきました。

github.com


Viewing all articles
Browse latest Browse all 1387

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>