【Unity】Unity上でFlutter！？　UIWidgetsアセットの初歩的な使い方

# はじめに
![image.jpg](https://assetstorev1-prd-cdn.unity3d.com/key-image/d3fbdea9-eacb-49c5-ab68-23f8aa82dda7.jpg)
__UnityでFlutter！？__
なんとも最初目にしたときは驚きました。UIWidgetsというアセットを使えばUnityでFlutterと同じようなことができてしまうというのですから。
sampleを動かすとこれはホントにUnityかと疑うほどです。
しかし，わりと新しいアセットであることもあってか日本語の情報が少ない…いえ，全体的に情報が少ないです。
なので，備忘録も兼ねて，使い方を簡単にまとめておきたいと思います。

# UIWidgets アセットとは
Googleが開発した言語Dartで使えるフレームワークである__Flutter__で実装するように，コードを描くことでUnity上で同様のUIを実装できるアセットです。__開発元はUnityテクノロジー（おそらくUnity China）__ですので，長期的なアップデートも安心できそうです。
今後Unity本体に統合されるかもしれませんね。
UnityのUIのようにシーン上のCanvasに配置していくというよりはコードを書いていく形です。Dartで書くのをC#で記述できるようになり，なおかつ使い慣れたUnityで制作できるのが強みでしょうか。
また，__高効率のレンダリングが可能__なようです。

## 環境
Windows 10 Pro
Unity 2019.1.10f1 → __Unity 2018.3.13f1__（2018.3.xを推奨）
###追記(2019-09-20)
Unity 2019.2.x系統最新版のUnity 2019.2.6f1およびUIWidgets最新版1.5.4-preview.0との組み合わせの場合は後述のAndroidOSにおける表示の不具合は発生しないことを確認して頂きました。
## インポート
まずは[Unity Asset Store](https://assetstore.unity.com/packages/tools/gui/uiwidgets-146398)からマイアセットに追加し，UnityEditor上でダウンロードします。
そして
インポートを行います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271700/63f3eb49-763b-2412-4c3a-96c3738ffb5e.png)
## つづき的なもの
[こちら](https://qiita.com/NWaka_1415/items/acd7b9aae4e3ddfcbef5)に画像の表示の際の注意点やら，そのほか色々試した続き的な記事があります。お暇があればぜひ。

# Sampleを動かす
まずはサンプルを動かしてみることにします。
Windows>UIWidgets>Tests>Galleryからサンプルアプリが実行できます。
sampleの動きや，その他UIWidgets用のインスペクターの使用方法などは「[【Unity提供アセット】FlutterライクのワークフローをUnityで実現！強力なUIプラグインパッケージが新登場。60fps以上のレンダリング効率、クロスプラットフォーム対応。1ドローコールで描画する「UIWidgets」](http://www.asset-sale.net/entry/UIWidgets)」をぜひ参考にしてみてください！(投げやり)

# Hello Worldを出す！
それでは恒例(?)のHello World!を描画してみましょう。

## スクリプトの作成
今回は軽く確認したいだけだったので以下のようなスクリプトを用意しました。
C#では__名前付き引数__が利用できるので，ホントにDartのように書けます。まあ，newは省略できませんが，個人的にインスタンスにnewがないのはむず痒いのでむしろ好都合かも。

```Test.cs
using Unity.UIWidgets.engine;
using Unity.UIWidgets.material;
using Unity.UIWidgets.painting;
using Unity.UIWidgets.ui;
using Unity.UIWidgets.widgets;

public class Test : UIWidgetsPanel
{
    protected override Widget createWidget()
    {
        return new MaterialApp(
            home: new Center(
                child: new Text(
                    "Hello World!",
                    textAlign: TextAlign.center,
                    style: new TextStyle(
                        fontSize: 32.0f
                    )
                )
            )
        );
    }
}
```

これ，言語がC#になっただけでやること変わんないな！？
結局はDartだろうがC#だろうが，Flutterのアプリの作り方はある程度理解しておく必要がありそうですね。逆に言えば，Dartでもうすでにある程度慣れている場合はすんなりと入って行けそうです。

## Unity側の設定
標準のCanvasとPanelを用意します。
PanelからImageコンポーネントを削除しましょう（要はCanvasにレンダリングされる空のゲームオブジェクトが必要）。
このPanelに先ほどのスクリプトをアタッチすることで表示されます。
![コメント 2019-07-21 191650.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271700/3d122dc0-060e-14cc-10ed-a54b76593f05.png)

やったぁ！「Hello World!」が表示された！
ですが，テキストしか出してないので，Unityのダサい背景のままであまりアプリケーションという気がしませんね。次の項で実際にマテリアルデザインを適用していきましょう。

# StatelessWidgetsを使用する
先ほどのTest.csを以下のように書き換えてあげます。

```Test.cs
using System.Collections.Generic;
using Unity.UIWidgets.engine;
using Unity.UIWidgets.material;
using Unity.UIWidgets.painting;
using Unity.UIWidgets.rendering;
using Unity.UIWidgets.ui;
using Unity.UIWidgets.widgets;

public class Test : UIWidgetsPanel
{
    protected override Widget createWidget()
    {
        return new MyStatelessWidget();
    }
}

public class MyStatelessWidget : StatelessWidget
{
    public override Widget build(BuildContext context)
    {
        return new MaterialApp(
            home: new Scaffold(
                appBar: new AppBar(title: new Text("Title")),
                body: new Center(
                    child: new Text(
                        "Hello World!",
                        textAlign: TextAlign.center,
                        style: new TextStyle(
                            fontSize: 32.0f
                        )
                    )
                )
            )
        );
    }
}
```

UIWidgetsPanelを継承したTestクラスはDartで書いたとこで言う以下に相当するものですね，たぶん笑

```Test.dart
void main() => runApp(MyStatelessWidget());
```

話が少しそれましたが，Test.csを先ほどのものにして保存すると以下のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271700/896d6d08-ce5e-336a-ab9d-0202825ec5c5.png)

# StatefulWidgetを使用する
それではStatefulWidgetも扱ってみましょう。Test.csを以下のように書き換えます。

```Test.cs
using System.Collections.Generic;
using Unity.UIWidgets.engine;
using Unity.UIWidgets.material;
using Unity.UIWidgets.painting;
using Unity.UIWidgets.rendering;
using Unity.UIWidgets.ui;
using Unity.UIWidgets.widgets;
using UnityEngine;

public class Test : UIWidgetsPanel
{
    protected override Widget createWidget()
    {
        return new MaterialApp(
            home: new MyStatefulWidget()
        );
    }

    protected override void OnEnable()
    {
        FontManager.instance.addFont(Resources.Load<Font>(path: "MaterialIcons-Regular"), "Material Icons");
        Screen.fullScreen = false; // ナビゲーションバー表示用
        base.OnEnable();
    }
}

public class MyStatefulWidget : StatefulWidget
{
    public override State createState()
    {
        return new MyWidgetState();
    }
}

class MyWidgetState : State<MyStatefulWidget>
{
    public override Widget build(BuildContext context)
    {
        return new Theme(
            data: new ThemeData(),
            child: new Scaffold(
                appBar: new AppBar(title: new Text("Test App")),
                body: new Center(
                    child: new Text(
                        "Hello World!",
                        textAlign: TextAlign.center,
                        style: new TextStyle(
                            fontSize: 32.0f
                        )
                    )
                ),
                floatingActionButton: new FloatingActionButton(
                    child: new Icon(Icons.add),
                    onPressed: () =>
                    {
                        Navigator.push(
                            context,
                            new MaterialPageRoute(
                                fullscreenDialog: true,
                                builder: (buildContext => new SecondMyStatefulWidget())
                            )
                        );
                    }
                ),
                bottomNavigationBar: new BottomAppBar(
                    child: new Row(
                        mainAxisSize: MainAxisSize.max,
                        mainAxisAlignment: MainAxisAlignment.spaceBetween,
                        children: new List<Widget>
                        {
                            new IconButton(icon: new Icon(Icons.menu), onPressed: () => { }),
                            new IconButton(icon: new Icon(Icons.home), onPressed: () => { })
                        })
                )
            )
        );
    }
}

class SecondMyStatefulWidget : StatefulWidget
{
    public override State createState()
    {
        return new SecondMyState();
    }
}

class SecondMyState : State<SecondMyStatefulWidget>
{
    public override Widget build(BuildContext context)
    {
        return new Theme(
            data: new ThemeData(),
            child: new Scaffold(
                appBar: new AppBar(title: new Text("Test App")),
                body: new Center(
                    child: new Text(
                        "Hello World!",
                        textAlign: TextAlign.center,
                        style: new TextStyle(
                            fontSize: 32.0f
                        )
                    )
                ),
                floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
                floatingActionButton: FloatingActionButton.extended(
                    icon: new Icon(Icons.add),
                    label: new Text("Big FAB"),
                    onPressed: (() => { })
                ),
                bottomNavigationBar: new BottomAppBar(
                    child: new Row(
                        mainAxisSize: MainAxisSize.max,
                        mainAxisAlignment: MainAxisAlignment.spaceBetween,
                        children: new List<Widget>
                        {
                            new IconButton(icon: new Icon(Icons.menu), onPressed: () => { }),
                            new IconButton(icon: new Icon(Icons.home), onPressed: () => { })
                        })
                )
            )
        );
    }
}
```

`OnEnable()`に以下の記述がないとアイコン各種が読み込まれませんので注意してください。

```.cs
FontManager.instance.addFont(Resources.Load<Font>(path: "MaterialIcons-Regular"), "Material Icons");
```

こちらも反映されると以下のようになります。
![87y4u-j8x3i.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271700/561242bf-c9b7-8499-11bc-2d1e8de9d608.gif)

StatefulWidgetである意味合いがないコードですが，ご了承ください…

# 実機にビルドしてみる
ビルド方法は普通のUnityプロジェクトのと何ら変わりませんので詳細は省きます。

ビルドしたものをAndroidなどの実機で試すとわかりますが，そのままではステータスバーやナビゲーションバーが表示されません（記載したコードはナビゲーションバーのみ表示されるはずですが）。より通常のアプリらしく表示したいところですよね。これに関しては[UIWidgets側で推奨されているもの](https://github.com/Over17/UnityShowAndroidStatusBar/tree/0.5)や，[【Unity】Unity 5.3以降でAndroidのStatus Barを表示する方法](http://tsubakit1.hateblo.jp/entry/2016/01/23/041430)が大変参考になります。

## Androidへのビルドにおける描画不具合
出来上がったものがこちら。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271700/be6794b8-9c51-5c0f-f83f-b83363f2c3ba.png" width="25%" height="25%" >

…おや？　おやおや？
なぜかFABしか表示されていません。FABをタップすると遷移し，×ボタンがあるであろうポイントをタップすると閉じて最初のページに戻ります。つまり存在はしているようだが，描画が上手くいっていないようです（なお，Windows向けビルドは正常に表示された）。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271700/4405b1a8-d7c5-b8f1-1260-4a1ff68449ae.gif" width="25%" height="25%" >

何が原因か知りませんが，2019の環境（2019.1.10f1）では上記のようになります。
もしやと思い，2018の環境（2018.3.13f1）にして再度プロジェクトを作り直してみたら無事表示されました。なぜ…__大人しく2018の環境で開発した方がよさそうです__。

## ステータスバーの表示
一応ステータスバーの表示に関しても~~軽く~~触れておきます（軽く触れるつもりが熱中して色々試してしまいました）。
今回自分はまず始めにUIWidgets側で推奨されている方法にのっとりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271700/c76d1006-cf1f-4963-cbfa-497ab6877ea8.png)
この2つのファイルをプロジェクトAssetsのPlugins/Android下に置きます。もしPluginsなどのフォルダがUnity上で作成されてない場合はフォルダごと持ってきてしまいましょう。

あとはビルドするだけです。が，公式に推奨されているわりに前者はどの場合も微妙でした。
### 前者のUnityTransparentStatusBarThemeの場合
ナビゲーションバーにUIが食い込んで押せなくなります。。。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271700/4072a9cf-9646-d5d7-3adf-ac6762c3c9b8.png" width="25%" height="25%" >

### 前者のUnityStatusBarThemeの場合
ナビゲーションバーにUIが食い込むことはないものの，21:9の画面比率が悪いのか表示が乱れる。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271700/8f3a88b0-2d60-5d9e-5c39-9590ee3962b3.png" width="25%" height="25%" >

### 後者の表示方法の場合
やったにゃぁ～ん”ん”！！
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271700/d91db0c3-4889-1b09-83f4-ce0c7ac050e2.gif" width="25%" height="25%" >

ちなみに横にしても問題ナッシングです。
![Screenshot_20190719-194425.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271700/55a04448-055b-2126-3f3d-63a195f6225d.png)

無事いい感じになりました。この方法の場合，ナビゲーションバー（下）とステータスバー（上）で表示方式を変更できるので，こちらで細かに調整ができ使い勝手がいいです。またAndroidにのみ対応させるのであれば，スクリプトをコピペして使うだけなのでとても楽に実装できます。
※Unity製のためSonyのゲームエンハンサーにゲーム判定を受けています。

# Unityの機能と合わせて使う
さて無事表示されたことですし，C#でFlutterができるってだけでもとても大きいことですが，せっかくUnity上で作っているのだからUnityの機能を合わせてみたいですよね。
実際このアセットの強みはUnityの機能を活用しつつFlutterアプリケーションを作れる（またその逆も然り）部分ですので，実際に試していきたいと思います。
以下のクラスを`Test.cs`に追記します。

```.cs
class UnityFunctionStateless : StatelessWidget
{
    private Player _player;

    public UnityFunctionStateless()
    {
        _player = GameObject.Find("Player").GetComponent<Player>();
    }

    public override Widget build(BuildContext context)
    {
        return new Stack(
            fit: StackFit.expand,
            children: new List<Widget>()
            {
                new Align(
                    alignment: Alignment.topCenter,
                    child: new Row(
                        children: new List<Widget>()
                        {
                            new Container(
                                margin: EdgeInsets.only(left: 30.0f, top: 30.0f),
                                child: new FloatingActionButton(
                                    child: new Icon(Icons.arrow_back),
                                    onPressed: (() => Navigator.pop(context))
                                )
                            ),
                            new Container(
                                margin: EdgeInsets.only(left: 30.0f, top: 30.0f),
                                child: new Text("Play Area",
                                    style: new TextStyle(color: Colors.white, fontSize: 32f, inherit: false)
                                )
                            )
                        }
                    )
                ),
                new Align(
                    alignment: new Alignment(0.0f, 1.0f),
                    child: new Container(
                        margin: EdgeInsets.only(bottom: 30.0f),
                        child: FloatingActionButton.extended(
                            icon: new Icon(Icons.arrow_upward),
                            label: new Text("Jump"),
                            onPressed: (() => _player.Jump())
                        )
                    )
                )
            }
        );
    }
}
```

あんまり`GameObject.Find()`したくなかったんですが，Testクラスに`SerializeField`のメンバを追加してもヒエラルキー上からは見えず，事前にPlayerを登録できなかったので，仕方なくFindしました。
<img src="https://1.bp.blogspot.com/-Ac13uE_82fs/XTQ1zGsZz4I/AAAAAAAAN_Y/jjdIaa_8co01Sb1GD69JpnqW02jBtXrPgCLcBGAs/s1600/ibb9j-y56dk.gif" Width="25%" height="25%" >

# まとめ
最初は脳死状態でポコポコオブジェクトを配置していくだけで，Flutterで製作するようなアプリケーションが作れるのかと頭お花畑なことを考えましたが，さすがにそんな甘くはなかったですね。__あくまでもUnity上でFlutterと同じ要領でアプリケーションが開発できますよ__というアセットでした。
なので，結局UnityのGUIのプロになっていたとしてもあまり恩恵がなさそうです。書き方はDartで扱っていたFlutterと同等です。冒頭でも少し触れましたが，すでにFlutterでバンバンアプリケーション開発をしている方はこのアセットでもバンバンアプリケーションを開発できると思います。
__ドキュメントも[Flutter公式リファレンス](https://flutter.dev/docs)を読むのが手っ取り早いでしょう。__
(他にも，なんか優しく手取り足取り教えてくれそうな[リファレンス](https://nzigen.com/flutter-reference/)もあったので一応)
Dart特有の記述方法はありますが，ほぼそのままC#で書けば同じようなことができます。

「言語がC#という(人によっては)Dartより慣れ親しんだ言語で書ける」，「Unityという環境を持っている場合はややこしいFlutterやDartの実行環境を整える必要がない」というメリットがあると思います。また，他にも__Unityの機能やその他のアセットが利用できる__のが大きな強みと言えそうです。