Edited at

UIElementsでもUniRxを使いたい


はじめに

これは『Unityゆるふわサマーアドベントカレンダー2019』の21日目の記事です。

前日は@kingyo222さんの『Unity:UI Elements でいくつかサンプル書いたよ!』でした。

翌日は @Sunmax0731 さんの『Unityとセンシングについて何か』です。


UnityにおけるUI実装パターンおさらい


IMGUI

おそらく、エディタ拡張でおなじみの文法ではないでしょうか。

これはエディタ拡張だけじゃなく、ゲーム中でも使用可能なのですが、

https://docs.unity3d.com/ja/current/Manual/GUIScriptingGuide.html


IMGUI システムは通常、プレイヤーが操作する普通のゲーム内ユーザーインターフェイスに使う事は意図されていません


ここにも書かれている通り、今はこれでゲームのUIを組んでいくことはほとんどないでしょう。

ゲーム中でボタンを表示するには以下のようにMonoBehaviourやらにOnGUIを定義します。

public class ImguiSampleScene : MonoBehaviour

{
void OnGUI()
{
if (GUILayout.Button("Button"))
{
Debug.Log("にゃんぱすー");
}
}
}

イベントを登録できるわけではなく、ループでの管理になるので、ボタンのステートの管理が別途必要になって非常にスパゲティがゆでやすい!

あと、当然のように ゲームを再生しないとボタンが表示されない ので、デバッグ用のUIを作るにしてもIMGUIは厳しいかなぁ、という感覚です。

今のUnityは多重シーンロードも容易く管理できますので、DebugMenuのようなシーンを用意し、LoadSceneでAdditiveに読み込んであげるのがいいと思います。


NGUIの覇権時代(Unity 4.5以前)

Unity 4.5までは標準のUIシステムが上記のIMGUIしかなく、NGUIなどのプラグインをアセットストアで買って使っていました。

私も昔は使っていたのですが、NGUIのなにもかもを忘れているので、きっと今後も使わないでしょう。

なので割愛(ひどい)。


uGUIの誕生と発展(Unity 4.6以降)

Unity 4.6でようやくUnity標準のUI機能が追加されました。

上記のNGUIの作者がUnityに入り、NGUIから枝分かれするような形で作ったものらしいです。

何はともあれ、標準のものができて、めでたしめでたし、ということで使ってみましょう。

public class UguiSampleScene : MonoBehaviour

{
public Button Button;

void Awake()
{
Button.onClick.AddListener(() => Debug.Log("にゃんぱすー"));
}

private void OnDestroy()
{
Button.onClick.RemoveAllListeners();
}
}

イベントを登録できるようになって非常に良くなりました!

しかもIMGUIと違って 再生しなくてもSceneビューにボタンが配置される! 素敵!

これができれば、オブザーバパターンでViewとModelを分離もできるので、してみます。

public interface IObserver

{
void Notify();
}

public class UguiSampleScene : MonoBehaviour, IObserver
{
public Text Text;
public Button Button;

private UguiSampleModel model;

void Awake()
{
// Observerに登録する
model = new UguiSampleModel(this);

Button.onClick.AddListener(() => model.IncrementCount());
}

private void OnDestroy()
{
Button.onClick.RemoveAllListeners();
}

// モデルから更新依頼が呼ばれたらUIに反映させる
void IObserver.Notify()
{
Text.text = model.Count.ToString();
}
}

public class UguiampleModel
{
public int Count { get; private set; }

private IObserver observer;

public UguiSampleModel(IObserver observer)
{
this.observer = observer;
}

public void IncrementCount()
{
Count++;

// Countが増えたので、Viewを更新して、とお願いする
observer.Notify();
}
}

最低限のObserverパターンでの実装を上でやってみました。

キモとなるのはIObserverで、Modelは自分の状態が更新されたらとにかくNotifyを呼ぶ、MonoBehaviourはNotifyを受けたらとにかくUIを更新する、ということをすることにより、ViewとModelを分離しています。

今はこれがIncrementCountのみですが、Modelの機能が増えてくるとスパゲティがゆでやすくなりそうだな、というのは容易に想像できるかと思います。


UniRxの登場(Unity 5以降)

UniRxの登場はUnityにおけるUIの歴史において一つの大きなものだったと言って過言ではないと思います。

そもそもTaskの時代になっても一向に非同期処理ができなかったUnity上で非同期処理を行うものとして使っていた方もおられると思うのですが、RxはもともとLINQ to Eventsとか呼ばれたりとイベントの処理に長けています。

実際に、上のObserverパターンをUniRxで書き直すとこうなります。

public class UguiSampleScene : MonoBehaviour

{
public Text Text;
public Button Button;

private UguiSampleModel model;

void Awake()
{
model = new UguiSampleModel();

Button.onClick.AddListener(() => model.IncrementCount());

// モデルから更新依頼が呼ばれたらUIに反映させる
model.OnUpdate.Subscribe(_ => Notify());
}

private void OnDestroy()
{
Button.onClick.RemoveAllListeners();
}

void Notify()
{
Text.text = model.Count.ToString();
}
}

public class UguiSampleModel
{
public int Count { get; private set; }
public Subject<Unit> OnUpdate { get; } = new Subject<Unit>();

public void IncrementCount()
{
Count++;

// Countが増えたので、Viewを更新して、とお願いする
OnUpdate.OnNext(Unit.Default);
}
}

Subjectを使って書きなおすとこうなります。

本来のオブザーバパターンのObserverとは観察者という意味なのですが、UniRxでは IObvervable(観察できるもの)をSubscribe(購読)する ことによって、オブザーバパターンを実装します。

このIObservableが実は素晴らしく、以下のようにボタンのイベントも購読できます。

// Button.onClick.AddListener(() => model.IncrementCount());

Button.OnClickAsObservable()
.Subscribe(_ => model.IncrementCount());

また、SubscribeはIDisposableを使った寿命の管理もできます。

// Disposableを貰う

var disposable = Button.OnClickAsObservable()
.Subscribe(_ => model.IncrementCount());

// DisposeすることがRemoveListenerと同じ役目
disposable.Dispose();

Rxでは寿命の管理も大事なので、複数のIDisposableを管理する機能などが複数存在します。

https://blog.xin9le.net/entry/2014/02/10/120619

xin9leさんが各機能を細かくまとめてくれててめっちゃ助かるので、是非一度見てみてください。

さらにUniRxにはReactivePropertyやSubscribeTo、AddToなどの便利機能もあります。

これらを使って書きなおすとこうなります。

public class UguiSampleScene : MonoBehaviour

{
public Text Text;
public Button Button;

private UguiSampleModel model;

void Awake()
{
model = new UguiSampleModel();

Button.OnClickAsObservable()
.Subscribe(_ => model.IncrementCount())
.AddTo(this); // 寿命はこのGameObjectが消えるまで

model.Count
.SubscribeToText(Text) // テキストにSubscribe
.AddTo(this); // 寿命はこのGameObjectが消えるまで
}
}

public class UguiSampleModel
{
public ReactiveProperty<int> Count { get; } = new ReactiveProperty<int>();

public void IncrementCount()
{
Count.Value++; // .Valueを書き換えると、OnNextが呼ばれる
}
}

ここまでくると、UniRxを使うと、自前でNotifyしてたオブザーバパターンに比べてシンプルに書ける、というのが実感できるかと思います。


UIElementsへの移行(Unity 2018.3以降、Unity 2020.x以降予定)

さて、なぜここだけ以降が2種類あるかといいますと、UIElementsは既にEditor用としてはリリースされているのですが、ハイ、アプリ側でもUnity 2020で使えるようになるらしいです。

ただIMGUIとuGUIもメンテはされるようで廃止という話にはすぐならないそうで、当然2020年リリースですから、2021年ぐらいにリリースされるアプリまではuGUIバリバリの現役ではないでしょうか…?

しかし、だからと言ってUIElementsから逃げられるかというと、そんなことはないはずで、Prefab in Prefabが登場しようが再利用できるテンプレートには絶対勝てないのは目に見えているので、さっさと勉強を始めましょうと思い、自分も触ってみました。

どうも、UXMLという見た目の定義ファイルをまず用意しなきゃダメのようです。

<?xml version="1.0" encoding="utf-8"?>

<engine:UXML
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:engine="UnityEngine.UIElements"
xmlns:editor="UnityEditor.UIElements"

xsi:noNamespaceSchemaLocation="../../../UIElementsSchema/UIElements.xsd"
xsi:schemaLocation="
UnityEngine.UIElements ../../../UIElementsSchema/UnityEngine.UIElements.xsd
UnityEditor.UIElements ../../../UIElementsSchema/UnityEditor.UIElements.xsd
UnityEditor.PackageManager.UI ../../../UIElementsSchema/UnityEditor.PackageManager.UI.xsd
"

>

<!-- ここから↑はおまじないみたいなもの、ここから下が配置されるパーツ -->

<engine:Label name="label" text="0"/>
<engine:Slider name="slider"/>
<engine:Button name="button" text="Submit"/>

<!-- ここまでが配置されるパーツ -->

</engine:UXML>

なんかボタンとスライダーの位置が逆な気がしますが、今回は大目に見ましょう(雑)。

これをC#上で扱うにはUQueryなるものを使うといいそうです。

// 他のアセットと同じようにUXMLを読み込む

var visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Editor/Windows/SampleObjectWindow.uxml");
// CloneTreeでInstantiateみたいな感じらしい
var labelFromUXML = visualTree.CloneTree();

// ↑で作ったインスタンスから名前でパーツを引ける
var label = labelFromUXML.Q<Label>("label");
var button = labelFromUXML.Q<Button>("button");

ややこしいことに、このButtonクラスとかはuGUIのButtonクラスとかとは別物です。

なのでUniRxの便利な拡張が使えません…

ので、簡単に作ってみましょう。

まずはButtonから。

public static IObservable<Unit> OnClickAsObservable(this UnityEngine.UIElements.Button source)

{
return Observable.FromEvent(
h => h,
h => source.clickable.clicked += h,
h => source.clickable.clicked -= h);
}

Observable.FromEventを使うと、このような感じで任意のイベントをIObservableにできます。

上で話した通り、IObservableにしてしまえばあとはこちらのもので、Subscribeしましょう!

var button = labelFromUXML.Q<Button>("button");

button.OnClickAsObservable()
.Subscribe(_ => Debug.Log("Nya"))
.AddTo(this);

これでUIElementsをuGUIと似たような感じで取り扱うことができました。

IObservable最高!

次にSliderの方のイベントもIObservableにしてしまいましょう。

ただし、Sliderをそのままするのではなく、これらのものはINotifyValueChangedというインターフェイスを実装しているらしいので、これに対して拡張メソッドを定義します。

public static IObservable<ChangeEvent<T>> OnValueChange<T>(this INotifyValueChanged<T> source)

{
return Observable.FromEvent<EventCallback<ChangeEvent<T>>, ChangeEvent<T>>(
h => new EventCallback<ChangeEvent<T>>(h),
h => source.RegisterValueChangedCallback(h),
h => source.UnregisterValueChangedCallback(h));
}

public static IObservable<T> OnValueChanged<T>(this INotifyValueChanged<T> source)
{
// ↑のChangeEvent<T>がpreviousValueとnewValueを持ってるけど、
// 欲しいのは大体newValueだと思うので、ここで変換する
return source.OnValueChange().Select(x => x.newValue);
}

// ついでにSubscribeToも定義しておく
public static IDisposable SubscribeToText<T>(this IObservable<T> source, TextElement text)
{
return source.SubscribeWithState(text, (x, t) => t.text = x.ToString());
}

できました!

これを使ってSubscribeしてみましょう。

var label = labelFromUXML.Q<Label>("label");

var slider = labelFromUXML.Q<Slider>("slider");
slider.OnValueChanged()
.SubscribeToText(label)
.AddTo(this);

IObservable最高!(大事なことなので…)


まとめ

IObservable最高!

あとこれ単体だと記事が長くなるかと思って、補足記事として『コチラ』もご覧ください。