Quantcast
Channel: C#タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9364

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

$
0
0

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

1 年前に Unity で 状態管理をするフレームワークを作りました。
しかし、まだ辛いところがあったのです。

「宣言的に UI を書きたくない??」

宣言的 UI とは?

Wikipediaより

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

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

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

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

見たいな感じですね

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

作ったもの

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

仕組み

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

仮想 DOM

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

というものです。

機能

この Veauty自体の機能は主に2つで

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

があります。

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

使い方

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

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

Image from Gyazo

インストール方法

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

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

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

ボイラープレート

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

// UIRoot.csusingUnityEngine;usingUI=UnityEngine;usingVeauty;usingVeauty.VTree;usingVeauty.GameObject;publicclassUIRoot:MonoBehaviour{privateVeautyObjectveauty;voidStart(){this.veauty=newVeautyObject(gameObject,Render,true);}voidRender()=>newNode("GameObject",IAttribute[]{},IVTree[]{});}

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

UI を作成

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

GameObject の作成

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

// UIRoot.csIVTreeRender()=>newNode("GameObject",IAttribute[]{},IVTree[]{});

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

Component のついた GameObject の作成

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

// UIRoot.csIVTreeRender()=>newNode<UI.HorizontalLayoutGroup>("HorizontalLayoutGroup",newIAttribute[]{},newIVTree[]{});

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

Button を作成する

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

// UIRoot.csIVTreeRender()=>newNode<UI.HorizontalLayoutGroup>("HorizontalLayoutGroup",newIAttribute[]{},newIVTree[]{newNode<UI.Button>("Button",IAttribute[]{},newIVTree[]{})});

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

スクリーンショット 2020-05-15 23.13.02.png

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

// ButtonWidget.cspublicclassButtonWidget:Widget{privateIAttribute[]attrs;privateIVTree[]kids;publicButtonWidget(IAttribute[]attrs,IVTree[]kids){this.attrs=attrs;this.kids=kids;}publicoverrideGameObjectInit(GameObjectgo){varimage=go.AddComponent<UI.Image>();varbtn=go.GetComponent<UI.Button>();btn.targetGraphic=image;returngo;}publicoverrideIVTreeRender()=>newNode<UI.Button>("Button",this.attrs,this.kids);publicoverridevoidDestroy(GameObjectgo){}publicoverrideIVTree[]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を使ってボタンを作成してみましょう!

IVTreeRender()=>newNode<UI.HorizontalLayoutGroup>("HorizontalLayoutGroup",newIAttribute[]{},newIVTree[]{newButton(IAttribute[]{},newIVTree[]{})});

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

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

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

usingVeauty.VTree;usingUnityEngine;usingVeauty;usingUI=UnityEngine.UI;publicclassText:Widget{privateIAttribute[]attrs;publicText(IAttribute[]attrs){this.attrs=attrs;}publicoverrideIVTree[]GetKids()=>newIVTree[0];publicoverrideGameObjectInit(GameObjectgo){vartextComponent=go.GetComponent<UI.Text>();textComponent.font=Resources.GetBuiltinResource<Font>("Arial.ttf");textComponent.alignment=TextAnchor.MiddleCenter;textComponent.color=Color.black;returngo;}publicoverrideIVTreeRender()=>newNode<UI.Text>("Text",attrs,GetKids());publicoverridevoidDestroy(GameObjectgo){}}

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

ValueAttribute

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

// Value.csusingVeauty;usingUI=UnityEngine.UI;publicclassValue:IAttribute{privatestringvalue;publicValue(stringvalue){this.value=value;}publicstringGetKey()=>"Value";publicvoidApply(GameObjectobj){vartextComponent=obj.GetComponent<UI.Text>();if(textComponent){textComponent.text=this.value;}}publicboolEquals(IAttributeattr){if(attrisValueother){returnthis.value==other.value;}returnfalse;}}

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

string GetKey()

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

void Apply(GameObject obj)

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

bool Equals(IAttribute attr)

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

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

// UIRoot.csIVTreeRender()=>newNode<UI.HorizontalLayoutGroup>("HorizontalLayoutGroup",newIAttribute[]{},newIVTree[]{newButton(IAttribute[]{},newIVTree[]{newText(newIAttribute[]{newValue("↑")})}),newText(newIAttribute[]{newValue("0")}),newButton(IAttribute[]{},newIVTree[]{newText(newIAttribute[]{newValue("↓")})}),});

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

OnClick を実装する

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

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

// OnClick.csusingUnityEngine;usingUI=UnityEngine.UI;usingEvents=UnityEngine.Events;usingVeauty;publicclassOnClick:IAttribute{privateEvents.UnityActionaction;publicOnClick(Events.UnityActionaction){this.action=action;}publicstringGetKey()=>"OnClick";publicvoidApply(GameObjectobj){varbutton=obj.GetComponent<UI.Button>();if(button){button.onClick.RemoveAllListeners();button.onClick.AddListener(this.action);}}publicboolEquals(IAttributeattr){if(attrisOnClickother){returnthis.action==other.action;}returnfalse;}}

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

// UIRoot.csIVTreeRender()=>newNode<UI.HorizontalLayoutGroup>("HorizontalLayoutGroup",newIAttribute[]{},newIVTree[]{newButton(newIAttribute[]{newOnClick(()=>Debug.Log("↑"))},newIVTree[]{newText(newIAttribute[]{newValue("↑")})}),newText(newIAttribute[]{newValue("0")}),newButton(newIAttribute[]{newOnClick(()=>Debug.Log("↓"))},newIVTree[]{newText(newIAttribute[]{newValue("↓")})}),});

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

State を更新する

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

// UIRoot.csusingUnityEngine;usingUI=UnityEngine.UI;usingVeauty;usingVeauty.VTree;usingVeauty.GameObject;publicclassSample:MonoBehaviour{privateVeautyObjectveauty;privateintcounter=0;privateSystem.Action<int>setCounter;voidStart(){this.veauty=newVeautyObject(gameObject,Render,true);this.setCounter=this.veauty.SetState<int>(n=>this.counter=n);}IVTreeRender()=>newNode<UI.HorizontalLayoutGroup>("HorizontalLayoutGroup",newIAttribute[]{},newIVTree[]{newButtonWidget(newIAttribute[]{newOnClick(()=>this.setCounter(this.counter+1))},newIVTree[]{newText(newIAttribute[]{newValue("↑")})}),newNode("Display",newIAttribute[0],newIVTree[]{newText(newIAttribute[]{newValue($"{this.counter}")}),}),newButtonWidget(newIAttribute[]{newOnClick(()=>this.setCounter(this.counter-1))},newIVTree[]{newText(newIAttribute[]{newValue("↓")})}),});}

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

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

最後に

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

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

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

それでは!!


Viewing all articles
Browse latest Browse all 9364

Latest Images

Trending Articles