Unityでも宣言的UI使いたくない？？

# Unity でも宣言的 UI 使いたくない？？

1 年前に Unity で [状態管理をするフレームワーク](https://qiita.com/uzimaru0000/items/0eea0305320540281a73) を作りました。
しかし、まだ辛いところがあったのです。

### **「宣言的に UI を書きたくない？？」**

## 宣言的 UI とは？

[Wikipedia](<[https://ja.wikipedia.org/wiki/%E3%82%B0%E3%83%A9%E3%83%95%E3%82%A3%E3%82%AB%E3%83%AB%E3%83%A6%E3%83%BC%E3%82%B6%E3%82%A4%E3%83%B3%E3%82%BF%E3%83%95%E3%82%A7%E3%83%BC%E3%82%B9#%E5%AE%A3%E8%A8%80%E7%9A%84UI](https://ja.wikipedia.org/wiki/グラフィカルユーザインタフェース#宣言的UI)>)より

> 宣言的 UI は[宣言型プログラミング](https://ja.wikipedia.org/wiki/宣言型プログラミング)を用いて構成された GUI、それを実現する手法である。GUI の生成・更新を変更前状態に基づいた更新命令によってコーディングするのではなく、あるべき状態を宣言してコーディングする。状態を分離することで UI の状態をより予測しやすいものにできる。[テンプレートエンジン](https://ja.wikipedia.org/wiki/テンプレートエンジン)は静的テンプレートと動的変数の関係を宣言しているとみなせるため、更新された状態とテンプレートからテンプレートエンジンによって UI 生成をおこなって UI を更新する形は宣言的 UI といえる。そういった意味でも宣言的 UI 自体は古くから存在する GUI 実装手法の 1 つである。

簡単に言うと「この状態のときはこの UI にする」というのを設定することです。

HTML でいうと、「HPが10のときには下のようなHTMLにする」

```html
<div>
  <span>HP:</span>
  <span>10</span>
</div>
```

見たいな感じですね

しかし、UI って動的な値を反映させてあげないといけないので愚直に宣言的 UI を実装しようとすると UI の構成要素を全部新しく作らないといけません。
これってかなり重たい処理で、Unity で言うと UI に反映させたい値（HP とか）を更新するたびに uGUI の構成要素を `Instantiate` するということです（逆に使わなくなった UI は、`Destory` するということ）

## 作ったもの

<a href="https://github.com/uzimaru0000/Veauty"><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159238/26438d1b-d0b1-0be8-8c86-5ab74fec357d.png" width="460px"></a>

そこで今回作ったライブラリは、効率よくUIの変更を更新できるようになっています！

### 仕組み

参考にしたのは、React や Vue などの Web フロントエンドのフレームワークで使われている **仮想 DOM** の概念です。

#### 仮想 DOM

仮想 DOM の概念はシンプルなもので表示に使っている Object（Web なら DOM、Unity なら GameObject）を仮想的なもの（木構造になっているデータ構造）で表して、更新の際はその木構造の差分を計算して必要な変更だけをしてあげる。

というものです。

### 機能

この [Veauty](https://github.com/uzimaru0000/Veauty) 自体の機能は主に２つで

- 仮想 Object の Tree を作成
- 差分の計算

があります。

ここで気になった人もいると思いますが、**必要な変更だけをしてあげる** が入ってないんですね。
この部分は来る UIElement に向けて ~~無駄に~~ 抽象化をしていてこのライブラリを使って実装するようにしています。（詳細は次の章で）

## 使い方

先程も言ったとおりこのライブラリだけだと GameObject に反映出来ないので以下のライブラリを利用します。

<a href="https://github.com/uzimaru0000/Veauty-GameObject"><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159238/640ea493-2a92-36f6-2873-5937687f7f5c.png" width="460px"></a>

今回はカウンターを作りながら使い方を見ていきます。

[![Image from Gyazo](https://i.gyazo.com/99daa1e4895c2094ae59bb976536035b.gif)](https://gyazo.com/99daa1e4895c2094ae59bb976536035b)

### インストール方法

Unity Package Manager を利用しているのでプロジェクトルート以下にある `Packages/manifest.json` に

```json
{
  "dependencies" : {
    ...
    "com.uzimaru.veauty": "https://github.com/uzimaru0000/Veauty.git",
    "com.uzimaru.veauty-gameobject": "https://github.com/uzimaru0000/Veauty-GameObject.git",
    ...
  }
}
```

と追記してエディタに行くとインストールされます．

### ボイラープレート

若干のボイラープレート的な物を書かないといけないのでそれにコードを追加していく形で解説していきます。

```csharp
// UIRoot.cs

using UnityEngine;
using UI = UnityEngine;
using Veauty;
using Veauty.VTree;
using Veauty.GameObject;

public class UIRoot : MonoBehaviour
{

  private VeautyObject veauty;

  void Start()
  {
    this.veauty = new VeautyObject(gameObject, Render, true);
  }

  void Render()
    => new Node("GameObject", IAttribute[] {}, IVTree[] {});
}
```

急にいろいろなクラスが出てきていますが順を追って説明していきます。
ここで作成された `UIRoot` クラスは Canvas に Attach してください。

### UI を作成

察しのいい人は分かると思うのですが、`Render` メソッドに UI の定義を書いていきます。

#### GameObject の作成

早速、ただの GameObject は以下のように宣言します。

```csharp
// UIRoot.cs

IVTree Render() =>
  new Node("GameObject", IAttribute[] {}, IVTree[] {});
```

`Node` クラスが何も component がついていない GameObject を生成する要素です。
第１引数の文字列は、GameObject の名前を示していてここの文字列が違うと前回とは違う要素だと判断して再描画されます。
第２引数の `IAttribute` の配列は、この GameObject の Component に何かしらの値を反映させるためのものです。（`transform.position` の変更とか）
最後の引数の `IVTree` の配列は、この GameObject の子要素の配列になります。

#### Component のついた GameObject の作成

このままでは何もすることが出来ないので GameObject に Component をつけていきます。
`HorizontalLayoutGroup` のついた GameObject を作成してみましょう。

```csharp
// UIRoot.cs

IVTree Render() =>
  new Node<UI.HorizontalLayoutGroup>(
  	"HorizontalLayoutGroup",
  	new IAttribute[] {},
  	new IVTree[] {}
  );
```

`Node` クラスの Generics に Attach したい Component の型をつけてあげるだけです。簡単ですね！

#### Button を作成する

カウンターの値を加算・減算するための Button を作成しましょう。
前のコードと同じように `Button` クラスのついたNodeを作成しましょう！

```csharp
// UIRoot.cs

IVTree Render() =>
    new Node<UI.HorizontalLayoutGroup>(
        "HorizontalLayoutGroup",
        new IAttribute[] {},
        new IVTree[]
        {
            new Node<UI.Button>("Button", IAttribute[] {}, new IVTree[] {})
        }
    );
```

これで一旦動かして見ましょう。

![スクリーンショット 2020-05-15 23.13.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159238/33ea2eaa-8d57-6d50-da91-de9695a36baa.png)

ヒエラルキー上で定義したような階層構造になっていることが分かると思います。
しかし、uGUI の`Button`クラスは押すために `Graphic` クラスを`targetGraphic` に設定しないといけないため現状では動きません。。。
そんな少し複雑になっている UI を作成するために使うのが `Widget` クラスです。

```csharp
// ButtonWidget.cs

public class ButtonWidget : Widget
{
    private IAttribute[] attrs;
    private IVTree[] kids;

    public ButtonWidget(IAttribute[] attrs, IVTree[] kids)
    {
        this.attrs = attrs;
        this.kids = kids;
    }

    public override GameObject Init(GameObject go)
    {
        var image = go.AddComponent<UI.Image>();
        var btn = go.GetComponent<UI.Button>();
        btn.targetGraphic = image;

        return go;
    }

    public override IVTree Render() =>
        new Node<UI.Button>(
            "Button",
            this.attrs,
            this.kids
        );

    public override void Destroy(GameObject go) { }

    public override IVTree[] GetKids() => this.kids;
}
```

少し長いですが、こんな感じのコードです。
順を追って説明していきます。

##### Widget

今回のメインの `Widget` クラスを継承します。このクラスは抽象クラスになっているので以下のメソッドをオーバーライドしなければいけません。

- `GameObject Init(GameObject go)`
- `IVTree Render()`
- `void Destory(GameObject go)`
- `IVTree[] GetKids()`

###### `Init`メソッド

このメソッドは、実体化した GameObject の初期設定をするためのメソッドです。
今回でいうと、`Image` クラスを Attach して`Button` クラスの `targetGraphic` に設定しています。（`Button` クラスは `Node` クラスの Generics で設定済み）

###### `Render` メソッド

widget 内での UI の宣言です。
今回は、`Node` クラスに`Button` クラスをつけてコンストラクタで受け取った Attributes と子要素を渡しています。

###### `Destory` メソッド

この Widget が削除されるときに実行されるメソッドです（実はまだ未実装）

###### `GetKids` メソッド

子要素を返します。

早速ここで作成した、`Button` を使ってボタンを作成してみましょう！

```csharp
IVTree Render() =>
    new Node<UI.HorizontalLayoutGroup>(
        "HorizontalLayoutGroup",
        new IAttribute[] {},
        new IVTree[]
        {
            new Button(IAttribute[] {}, new IVTree[] {})
        }
    );
```

これでボタンが生成されたと思います！

#### テキストに文字を指定する

ボタンは出来ましたが、中に入る `Text` が出来ていません。
とりあえず `Text` を出す Widget を作成します。

```csharp
using Veauty.VTree;
using UnityEngine;
using Veauty;
using UI = UnityEngine.UI;

public class Text : Widget
{
    private IAttribute[] attrs;

    public Text(IAttribute[] attrs)
    {
        this.attrs = attrs;
    }

    public override IVTree[] GetKids() => new IVTree[0];

    public override GameObject Init(GameObject go)
    {
        var textComponent = go.GetComponent<UI.Text>();
        textComponent.font = Resources.GetBuiltinResource<Font>("Arial.ttf");
        textComponent.alignment = TextAnchor.MiddleCenter;
        textComponent.color = Color.black;

        return go;
    }

    public override IVTree Render() => new Node<UI.Text>("Text", attrs, GetKids());

    public override void Destroy(GameObject go) { }
}
```

Widget の中身は Button と同じようなものなので省略します。
さて、ここで文字を指定するにはどうしたら良いでしょう？
普通の Unity だったら `UI.Text` の `text` に表示したい文字を入れます。では、Veauty だったら？
答えは Attribute を使います。
上でも少し説明したように `Attribute` とは **GameObject の Component に何かしらの値を反映させるためのもの** です。
なので今回は `UI.Text` の `text` に表示したい文字列を反映させる Attribute を作成しましょう。

#### ValueAttribute

Text だと Widget の方とかぶってしまうので `Value` という名前にします。

```csharp
// Value.cs

using Veauty;
using UI = UnityEngine.UI;

public class Value : IAttribute
{
    private string value;

    public Value(string value)
    {
        this.value = value;
    }

    public string GetKey() => "Value";

    public void Apply(GameObject obj)
    {
        var textComponent = obj.GetComponent<UI.Text>();
        if (textComponent)
        {
            textComponent.text = this.value;
        }
    }

    public bool Equals(IAttribute attr)
    {
        if (attr is Value other)
        {
            return this.value == other.value;
        }

        return false;
    }
}
```

`IAttribute` インターフェースで実装するメソッドは 3 つです。

##### `string GetKey()`

この Attribute を識別するためのものです。

##### `void Apply(GameObject obj)`

渡ってきた Object に対してこの Attribute がしたい操作を反映させます。

##### `bool Equals(IAttribute attr)`

渡ってきた `IAttribute` を見てこの Attribute と等しいかを判定します。

実際に使って見ましょう。

```csharp
// UIRoot.cs

IVTree Render() =>
    new Node<UI.HorizontalLayoutGroup>(
        "HorizontalLayoutGroup",
        new IAttribute[] {},
        new IVTree[]
        {
        new Button(IAttribute[] {}, new IVTree[]
            {
                new Text(new IAttribute[] { new Value("↑") })
            }),
        new Text(new IAttribute[] { new Value("0") }),
        new Button(IAttribute[] {}, new IVTree[]
            {
                new Text(new IAttribute[] { new Value("↓") })
            }),
        }
    );
```

だんだん形が見えて来ましたね。

#### OnClick を実装する

`Button` に対する OnClick も`Attribute` として実装します。

コードは以下のようになります。

```csharp
// OnClick.cs

using UnityEngine;
using UI = UnityEngine.UI;
using Events = UnityEngine.Events;
using Veauty;

public class OnClick : IAttribute
{
    private Events.UnityAction action;

    public OnClick(Events.UnityAction action)
    {
       this.action = action;
    }

  	public string GetKey() => "OnClick";

    public void Apply(GameObject obj)
    {
        var button = obj.GetComponent<UI.Button>();
        if (button)
        {
            button.onClick.RemoveAllListeners();
            button.onClick.AddListener(this.action);
        }
    }

    public bool Equals(IAttribute attr)
    {
        if (attr is OnClick other)
        {
            return this.action == other.action;
        }

        return false;
    }
}
```

これを使うとこんな感じですね

```csharp
// UIRoot.cs

IVTree Render() =>
    new Node<UI.HorizontalLayoutGroup>(
        "HorizontalLayoutGroup",
        new IAttribute[] {},
        new IVTree[]
        {
            new Button(new IAttribute[] { new OnClick(() => Debug.Log("↑")) }, new IVTree[]
                {
                    new Text(new IAttribute[] { new Value("↑") })
                }),
            new Text(new IAttribute[] { new Value("0") }),
            new Button(new IAttribute[] { new OnClick(() => Debug.Log("↓")) }, new IVTree[]
                {
                    new Text(new IAttribute[] { new Value("↓") })
                }),
        }
    );
```

これでボタンを押すと console に Log が出ると思います。

#### State を更新する

最後に State を更新してみましょう！
Veauty では State の更新をするために `VeautyObject` の `SetState` メソッドを使って State 更新用の関数を生成します。
コードで見るとこんな感じです。今回は、`counter` という `int` 型の値を State とします。

```csharp
// UIRoot.cs

using UnityEngine;
using UI = UnityEngine.UI;
using Veauty;
using Veauty.VTree;
using Veauty.GameObject;

public class Sample : MonoBehaviour
{
    private VeautyObject veauty;
    private int counter = 0;
    private System.Action<int> setCounter;

    void Start()
    {
        this.veauty = new VeautyObject(gameObject, Render, true);
        this.setCounter = this.veauty.SetState<int>(n => this.counter = n);
    }

    IVTree Render() =>
        new Node<UI.HorizontalLayoutGroup>(
            "HorizontalLayoutGroup",
            new IAttribute[] { },
            new IVTree[]
            {
                new ButtonWidget(new IAttribute[] {new OnClick(() => this.setCounter(this.counter + 1))}, new IVTree[]
                {
                    new Text(new IAttribute[] {new Value("↑")})
                }),
                new Node("Display", new IAttribute[0], new IVTree[]
                {
                    new Text(new IAttribute[] {new Value($"{this.counter}")}),
                }),
                new ButtonWidget(new IAttribute[] {new OnClick(() => this.setCounter(this.counter - 1))}, new IVTree[]
                {
                    new Text(new IAttribute[] {new Value("↓")})
                }),
            }
        );
}
```

ここで State を OnClick 内で直接更新しないで `this.setCounter` を経由して居ることを確認してください。
これは、`VeautyObject` に State が変わったこと（再描画をしてほしいこと）を通知するためにこのような更新の仕方をしています。
普通に`counter`を更新をしてしまうと State が変わったことを検知できないため再描画がされません。。。

肝心の `this.setCounter` ですが、Start メソッドで初期化しています。
`VeautyObject` の`SetState` メソッドに更新するための操作を渡してあげると更新のための関数が生成されます。
これを利用して `State` を更新すると再描画がされるといった仕組みです。

## 最後に

これが `Veauty` のおおよその使い方になります！

しかし、まだまだ不備があったりと完全なものではないので興味のある人は是非コントリビュートをしていただけるとありがたいです :bow:
（ちなみにここで Widget と Attribute の作り方を丁寧に説明したのは、uGUI 用の Widget や Attribute をまとめたライブラリの [Veauty-uGUI](https://github.com/uzimaru0000/Veauty-uGUI) に協力してほしいからです...！）

また、Veautyでこんなの作ったよ！というのもの常に受け付けているのでぜひ触って見てください！！

それでは！！