Help us understand the problem. What is going on with this article?

UIのシステムをUniRxで構築する

More than 1 year has passed since last update.

理想のUIシステムとして、値が変わったことに追従してアイコンが変わる、色が変わるなどの振る舞いがとても便利で、一度実装してしうことで運用コストも下げられるシステムが理想でしょう。
そんな機能を持ったUIシステムを、UniRxを用いて実装できたらと思います。

UniRxの解説を挟みたいところですが、長くなるので簡単な説明だけさせていただきます。

UniRxとは?

UniRxとは、neueccさんが作成されている、Unityに最適化された、Reactive Extensionsのライブラリです。
AssetStore、GitHubなどで配布されています。

入門編としては、こちらのとりスープさんの記事が分かりやすくてオススメです。
http://qiita.com/toRisouP/items/2f1643e344c741dd94f8

今回、このUniRxの利点である、変化があった時の通知を用いて、UIの管理を任せたいと思います。

ReactiveProperty

UniRxには、標準で IntReactivePropertyBoolReactivePropertyVector2ReactivePropertyなどが用意されており、それぞれ値に変化があるとOnNext()が発行されます。

これはとても便利な機能で、例えば値を渡した時に、同じ値であればスルー、違う値であればOnNext()が発生するというとても勝手の良いものになります。

今回は、OnNext()が発生したら、画像がenumのメンバーに応じて差し代わるような仕組みを作ってみたいと思います。

ReactibeProperty.gif

このemunがReactivePropertyとなっており、メンバーが変化すると合わせて画像も変化するようになっています。

合わせて、enumを用いた便利機能も採用していますので、そちらも合わせてお役立ていただけたらと思います。

本来であれば「値の変化」だけでも判断するためのコード数はそれなりのものになります。それが、変数の定義方法を変えるだけで軽量化できるのであれば、使わない手はないでしょう。

enumをReactivePropertyとして使用する方法は別途説明しますが、まずは基本的なコードをここで紹介したいと思います。

    BoolReactiveProperty bool = new BoolReactiveProperty(false);

    void start(){
        bool.Subscribe(Debug("Bool:" + bool.Value));
    }

    public void SetBool(bool setbool){
        bool.Value = setbool;
    }

と言った具合になります。

基本的に値を渡す時は.Valueに値を渡す必要があります。
これはよくハマる部分です。

渡した値に相違があった場合、OnNext()を発行してくれます。
この場合だと、trueの値が入っていて、falseが渡されるとOnNextが発行されます。

カスタムReactivePropertyを作成する

それではカスタムReactivePropertyを作成しましょう。
今回は、自身で定義したenumで使いたいと思います。

まず、使用するenumを定義します。
いきなりではありますが、このenumの値はのちにロードするリソースのファイル名と同じになるようにします。
Unityでは、リソースをロードするときにstring型で指定します。
その時のタイポを防ぐ、予防のためにenumを文字列に変更して使用するためです。

それではTabIconReactivePropertyクラスを作成しましょう。

ここでは特別設定することが設定するenumを指定して、ReactivePropertyを継承する事ぐらいになります。
あとはコンストラクタで引数と自身を継承するための設定を行いましょう。
baseを継承しているコンストラクタは、引数を使用してインスタンスする場合用です。

TabIconReactiveProperty.cs
using System.Collections;
using System.Collections.Generic;
using UniRx;
using UnityEngine;

namespace MVRP.UI
{

public enum TabIcon
{
    Blue,
    Green,
    Red,
    Yellow
}


    [System.Serializable]
    public class TabIconReactiveProperty : ReactiveProperty<TabIcon>
    {
        public TabIconReactiveProperty (){}
        public TabIconReactiveProperty (TabIcon initialValue) : base (initialValue) {}
    }
}


カスタムReactivePropertyをインスペクターで目視できるように

カスタムReactivePropertyを使用する際の注意点として、ジェネリック型のクラスはインスペクタで表示できないのがUnityの仕組みとなっています。
そこで、以下のクラスをEditorフォルダに設置し、カスタムReactivePropertyを使用した際に [SerializeField]の属性を付けてあげましょう。

このクラスは一つ用意して、カスタムReactivePropertyを増やした都度クラスに属性を増やしていく運用をしていきましょう。

ExtendInspectorDisplayDrawer.cs
using System.Collections;
using System.Collections.Generic;
using UniRx;
using UnityEngine;


namespace MVRP.UI
{
    [UnityEditor.CustomPropertyDrawer(typeof(TabIconReactiveProperty))]
    /// ここに随時追加
    public class ExtendInspectorDisplayDrawer : InspectorDisplayDrawer
    {
    }
}


管理をenumで一元化する

ここで、今回のカスタムReactivePropertyを運用するにあたって、enumの便利な使い方、効率の良い実装に向けた機能の紹介をします。

Unityで stringを引数として使用するAPIの運用に関して、タイポミスなどを防ぐための予防策として、enumを使うことを推奨します。
項目としてある程度決まった仕様(ゲーム内での属性、攻撃の種類など)をenumの構造体として運用することで、正しく設定されていないものに関してエラーを返す仕組みが働くよう実装が可能になります。

Enum.GetNames
https://msdn.microsoft.com/ja-jp/library/system.enum.getnames(v=vs.110).aspx
→構造体の名前を取得します

Enum.ToObject
https://msdn.microsoft.com/ja-jp/library/system.enum.toobject(v=vs.110).aspx
→整数からメンバー値を取得します

文字列としてメンバーを取り出すのであれば、enum.nanika.ToString()、構造体のメンバー数を取得するなら Enum.GetNames (typeof(enum)).Length、整数でメンバーを呼び出したいならEnum.ToObject (typeof(enum), i)が有用です。

文字列や、マジックナンバーで実装が進んでしまうと、変更があった際の対応、タイポのチェックなどが運用フェーズになってしまうと追いつかないため、ある程度ルールで縛るといった実装が効果的になる場合があります。

Presenterの実装

さて、いよいよPresenterの実装になります。
今回は、巷で人気が出てきたMV(R)Pという設計をイメージした仕様で実装を進めたいと思います。
本来はこのPresenterと呼ばれるクラス(MV(R)PのP)は、軽い実装で作られるもので、ViewとModelをしっかりと作りこむのが基本となっています。
細かい説明は省きますが、今後発展的な実装を見越してPresenterとしてクラスを定義したいと思います。

まずは必要なUIパーツ(View)の紐付けと、リソースのロード。
次にReactiveProperty(Model)の設定と、外部から今の値を確認できる変数を設定してあげましょう。

今回実装するクラスは、タブのアイコンをenumのメンバーから変更しようと言った試みです。
参照先のImageクラス(View)とリソースをキャッシュするためのTexture2D変数を準備します。

    [SerializeField] Image icomImage;
    Dictionary<TabIcon, Texture2D> iconSprite = new Dictionary<TabIcon, Texture2D>();

Dictionatyクラスを使用して、enumのメンバーをKeyに割り当てたいと思います。
enumのKeyに対してTexturをキャッシュします。

次に、ReactivePropertyの宣言と、外部から読み取れる読み取り専用のReactivePropertyを準備します。

    [SerializeField] TabIconReactiveProperty iconSpriteStatus = new TabIconReactiveProperty (TabIcon.Red);
    public IReadOnlyReactiveProperty<TabIcon> IconSpriteStatus { get { return iconSpriteStatus; } }

IReadOnlyReactivePropertyはとても便利なもので、ここの参照からSubscribeさせることが可能となります。
中のenumのメンバーを確認したい場合は、IconSpriteStatus.Valueにて確認が可能です。

さて、ここからStartのイベント(メソッド)から、必要な準備と、ストリームの設定を行いたいと思います。
まずは、差し替えるための画像をキャッシュしましょう。

enumに設定されているメンバーの数分画像をロードし、Texture2Dの変数にキャッシュします。
一点メンバー名の設定のコツとして、ファイル名をenumのメンバー名にするとロード時のテキストデータに変換して使えます。
ここではフォルダのパスを直接書いてしまっていますが、本来の実装方法として望ましくないことを付け加えておきます。
この話の文脈からすると、フォルダのパスもenumとして定義するべきだからです。

呼び出し方法としては、まずメンバー数を取得し、ループの回数として設定します。
そこから初期化したint値を用いてEnum.ToObjectからenumのメンバーを設定します。
その設定したメンバー値を使って、Dectionary変数に、KeyとTexture2DをAddします。

for (int i = 0; i < Enum.GetNames (typeof(TabIcon)).Length; i++) {
                TabIcon iconFileName = (TabIcon)Enum.ToObject (typeof(TabIcon), i);
                iconSprite.Add (iconFileName, (Texture2D)Resources.Load ("Textures/UI/" + iconFileName.ToString ()));
            }

画像のキャッシュが完了したら、いよいよReactivePropertyを用いたストリームを設定します。
ここでは特に難しいオペーレーターを使用せず、値が変わったらSubscribeするだけの単純な設定にしたいと思います。

this.iconSpriteStatus.Subscribe (
                x => icomImage.sprite 
                    = Sprite.Create(
                    iconSprite[x], 
                    new Rect(0, 0, iconSprite[x].width,
                    iconSprite[x].height), 
                    Vector2.zero
                )
            );

カスタムReactivePropertyで、enumのメンバーが変更されたら、その値を用いてキャッシュされているTexture2Dをロードします。
今回は、ImageクラスにSpriteをロードしなくてはなりません。
しかしキャッシュされているのはTexture2Dになりますので、コンバートの作業が必要になります。
そこで、SpriteクラスのCreateメソッドを用いて、Texture2Dの素材からSpriteを生成し、渡してあげましょう。

以上で基本的な実装は終了です。
最後に、enumのメンバーを切り替えるためのメソッドを準備したいと思います。

    public void SetTabButtonIcon(TabIcon iconName){
        iconSpriteStatus.Value = iconName;
    }

重ねて注意すべき点ですが、ReactivePropertyに値を渡す際には、必ずValueに渡してあげましょう。

以上で全てのソースコードになります。
それでは、namespaceの設定も含めた、全てのソースコードになります。

TabButtonPresemter.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UniRx;
using UnityEngine;
using UnityEngine.UI;

namespace MVRP.UI
{
    public class TabButtonPresenter : MonoBehaviour
    {
        [SerializeField] Image icomImage;
        Dictionary<TabIcon, Texture2D> iconSprite = new Dictionary<TabIcon, Texture2D>();

        [SerializeField] TabIconReactiveProperty iconSpriteStatus = new TabIconReactiveProperty (TabIcon.Red);
        public IReadOnlyReactiveProperty<TabIcon> IconSpriteStatus { get { return iconSpriteStatus; } }
        void Start ()
        {
            icomImage = gameObject.GetComponent<Image>();
            for (int i = 0; i < Enum.GetNames (typeof(TabIcon)).Length; i++) {
                TabIcon iconFileName = (TabIcon)Enum.ToObject (typeof(TabIcon), i);
                iconSprite.Add (iconFileName, (Texture2D)Resources.Load ("Textures/UI/" + iconFileName.ToString ()));
            }

            this.iconSpriteStatus.Subscribe (
                x => icomImage.sprite = Sprite.Create(iconSprite[x], new Rect(0, 0, iconSprite[x].width, iconSprite[x].height), Vector2.zero)
            );
        }

        public void SetTabButtonIcon(TabIcon iconName){
            iconSpriteStatus.Value = iconName;
        }
    }
}


出来上がったクラスを、差し替え対象のImageObjectにAddして、再生ボタンを押してみましょう。
インスペクターからenumのメンバーを変更することができるので動作確認をし、問題がないようであればSetTabButtonIcon(TabIcon iconName)から変更してみましょう。
正常に動作しなかった場合は、まずIconImageNoneになっていない事を確認しましょう。

Unity_5_5_0f3__64bit__-_Untitled_-_UniRx_-_iPhone__iPod_Touch_and_iPad__OpenGL_4_1_.png

以上です。
ありがとうございました。

dsgarage
UnityとArduinoをこよなく愛するドラマーです。
unity-game-dev-guild
趣味・仕事問わずUnityでゲームを作っている開発者のみで構成されるオンラインコミュニティです。Unityでゲームを開発・運用するにあたって必要なあらゆる知見を共有することを目的とします。
https://unity-game-dev-guild.github.io/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした