Unityの各種イベントをIObservable<T>に変換する

この記事は、これは[『Unityゆるふわサマーアドベントカレンダー2019』](https://qiita.com/nkjzm/items/7c933bb5c772a0b75512)の21日目の記事、[『UIElementsでもUniRxを使いたい』]
(https://qiita.com/yKimisaki/items/c45e9cc7d2c01199a0c3)の補足記事です。

# Subjectを使ってIMGUIをIObservable<T>化

単純にButtonのクリックイベントをIObservable<T>化します。
する前はこんな感じ。

```.cs
public class ImguiSampleScene : MonoBehaviour
{
    bool previousPressed;

    void OnGUI()
    {
        var currentPressed = GUILayout.Button(Text);
        if (!currentPressed && previousPressed)
        {
            Debug.Log("Clicked!!")
        }
        previousPressed = currentPressed;
    }
}
```

これだとボタンの数だけpreviousPressedとcurrentPressedが必要になってしまいます。
そこでラッピングします。

```.cs
public class IMGUIButton : IDisposable
{
    private Subject<Unit> onClick = new Subject<Unit>();
    public IObservable<Unit> OnClickAsObservable() => onClick;

    private string Text { get; set; }
    private bool previousPressed;

    public IMGUIButton(string text)
    {
        Text = text;
    }

    public void Dispose()
    {
        onClick.Dispose();
    }

    public void Draw()
    {
        var currentPressed = GUILayout.Button(Text);
        if (!currentPressed && previousPressed)
        {
            onClick.OnNext(Unit.Default);
        }
        previousPressed = currentPressed;
    }
}
```

これを使うと

```.cs
public class IMGUISampleScene : MonoBehaviour
{
    private IMGUIButton button;

    private void Start()
    {
        button = new IMGUIButton("Button");
        button.AddTo(this);

        button.OnClickAsObservable().Subscribe(_ => Debug.Log("Clicked!!")).AddTo(this);
    }

    void OnGUI()
    {
        button.Draw();
    }
}
```

こうできます。

# uGUIのI***HandlerをIObservable<T>化

通常だとこんな感じでインターフェイスを作ったものをuGUIに配置すると思います。
これもいいのですが、本来このInputというのはゲームのコントローラと一緒で1つでいいものです。
しかし、シチュエーションによって、例えばフィールドであればキャラを動かす、インベントリならアイテムを入れ替えたりする、など、同じイベントで異なる挙動をする場合、このクラス1つにそれらの処理が依存し、密になってしまう可能性があります。

```.cs
public class InputPresenter : MonoBehaviour, IDragHandler, IBeginDragHandler, IEndDragHandler, IPointerDownHandler, IPointerUpHandler, IPointerClickHandler
{
    public void OnBeginDrag(PointerEventData eventData) { }
    public void OnDrag(PointerEventData eventData) { /* フィールドならキャラ移動、インベントリならアイテム移動の処理 */ }
    public void OnEndDrag(PointerEventData eventData) { }
    public void OnPointerDown(PointerEventData eventData) { }
    public void OnPointerUp(PointerEventData eventData) { }
    public void OnPointerClick(PointerEventData eventData) { }
}
```

そこをIObservable<T>化することで疎にしましょう。

```.cs
public interface IInputEvents
{
    IObservable<PointerEventData> OnDragBegin();
    IObservable<PointerEventData> OnDrag();
    IObservable<PointerEventData> OnDragEnd();
    IObservable<PointerEventData> OnPressPointer();
    IObservable<PointerEventData> OnReleasePointer();
    IObservable<PointerEventData> OnClick();
}
```

まず欲しいイベント類をinterfaceで定義し、それをuGUIの方で実装します。

```.cs
[RequireComponent(typeof(RaycastTarget))]
public class InputPresenter : MonoBehaviour, IInputEvents, IDragHandler, IBeginDragHandler, IEndDragHandler, IPointerDownHandler, IPointerUpHandler, IPointerClickHandler
{
    public float ClickThrottleTime = 0.25f;
    private float lastPointerDownTime;
    private bool isDrag;

    private Subject<PointerEventData> beginDrag = new Subject<PointerEventData>();
    public IObservable<PointerEventData> OnDragBegin() => beginDrag;

    private Subject<PointerEventData> drag = new Subject<PointerEventData>();
    public IObservable<PointerEventData> OnDrag() => drag;

    private Subject<PointerEventData> endDrag = new Subject<PointerEventData>();
    public IObservable<PointerEventData> OnDragEnd() => endDrag;

    private Subject<PointerEventData> pointerDown = new Subject<PointerEventData>();
    public IObservable<PointerEventData> OnPressPointer() => pointerDown;

    private Subject<PointerEventData> pointerUp = new Subject<PointerEventData>();
    public IObservable<PointerEventData> OnReleasePointer() => pointerUp;

    private Subject<PointerEventData> pointerClick = new Subject<PointerEventData>();
    public IObservable<PointerEventData> OnClick() => pointerClick;

    public void OnDestroy()
    {
        this.beginDrag.Dispose();
        this.drag.Dispose();
        this.endDrag.Dispose();
        this.pointerDown.Dispose();
        this.pointerUp.Dispose();
        this.pointerClick.Dispose();
    }

    public void OnBeginDrag(PointerEventData eventData)
    {
        beginDrag.OnNext(eventData);
    }

    public void OnDrag(PointerEventData eventData)
    {
        isDrag = true;
        drag.OnNext(eventData);
    }

    public void OnEndDrag(PointerEventData eventData)
    {
        endDrag.OnNext(eventData);
    }

    public void OnPointerDown(PointerEventData eventData)
    {
        isDrag = false;
        lastPointerDownTime = Time.realtimeSinceStartup;

        pointerDown.OnNext(eventData);
    }

    public void OnPointerUp(PointerEventData eventData)
    {
        pointerUp.OnNext(eventData);
    }

    public void OnPointerClick(PointerEventData eventData)
    {
        // 長押しやドラッグがクリックと誤検知されるのを防ぐ
        if (isDrag || Time.realtimeSinceStartup - lastPointerDownTime > ClickThrottleTime)
        {
            return;
        }

        pointerClick.OnNext(eventData);
    }
}
```

これを使う場合は、

```.cs
public class CharacterMovementModel
{
    private Vector2 startPosition;

    public CharacterMovementModel(IInputEvents input, CompositeDisposable gameObjectLifeTime)
    {
        var dragDirection = Vector2.zero;

        // ドラッグで移動
        input.OnDragBegin()
            .Do(x => startPosition = x.position)  // ドラッグ開始時に指の位置を始点として確保
            .Select(_ => input.OnDrag()) // ドラッグ中に繋げる
            .Switch()
            .Subscribe(x => dragDirection = (x.position - startPosition).normalized) //  ↑の始点との差で方向を取って、スティック入力風にする
            .AddTo(gameObjectLifeTime);
        input.OnDragEnd()
            .Subscribe(x => dragDirection = Vector2.zero) // ドラッグ終了時にお掃除
            .AddTo(gameObjectLifeTime);
            
        // クリックでジャンプ
        input.OnClick()
            .Subscribe(x => Jump()) // 割愛
            .AddTo(gameObjectLifeTime);
    }
}
```

という感じで実装することができます。

# UIElementsのManipulatorについて

調べたところ、Unity 2019.1.14f1では以下のEventBaseが継承された各種イベントがあるようです。

```.cs
UnityEngine.UIElements.AttachToPanelEvent
UnityEngine.UIElements.BlurEvent
UnityEngine.UIElements.ChangeEvent<T>
UnityEngine.UIElements.CommandEventBase<T>
UnityEngine.UIElements.ContextClickEvent
UnityEngine.UIElements.ContextualMenuPopulateEvent
UnityEngine.UIElements.CustomStyleResolvedEvent
UnityEngine.UIElements.DetachFromPanelEvent
UnityEngine.UIElements.DragAndDropEventBase<T>
UnityEngine.UIElements.DragEnterEvent
UnityEngine.UIElements.DragExitedEvent
UnityEngine.UIElements.DragLeaveEvent
UnityEngine.UIElements.DragPerformEvent
UnityEngine.UIElements.DragUpdatedEvent
UnityEngine.UIElements.EventBase<T>
UnityEngine.UIElements.ExecuteCommandEvent
UnityEngine.UIElements.FocusEvent
UnityEngine.UIElements.FocusEventBase<T>
UnityEngine.UIElements.FocusInEvent
UnityEngine.UIElements.FocusOutEvent
UnityEngine.UIElements.GeometryChangedEvent
UnityEngine.UIElements.IMGUIEvent
UnityEngine.UIElements.InputEvent
UnityEngine.UIElements.KeyboardEventBase<T>
UnityEngine.UIElements.KeyDownEvent
UnityEngine.UIElements.KeyUpEvent
UnityEngine.UIElements.MouseCaptureEvent
UnityEngine.UIElements.MouseCaptureEventBase<T>
UnityEngine.UIElements.MouseCaptureOutEvent
UnityEngine.UIElements.MouseDownEvent
UnityEngine.UIElements.MouseEnterEvent
UnityEngine.UIElements.MouseEnterWindowEvent
UnityEngine.UIElements.MouseEventBase<T>
UnityEngine.UIElements.MouseLeaveEvent
UnityEngine.UIElements.MouseLeaveWindowEvent
UnityEngine.UIElements.MouseMoveEvent
UnityEngine.UIElements.MouseOutEvent
UnityEngine.UIElements.MouseOverEvent
UnityEngine.UIElements.MouseUpEvent
UnityEngine.UIElements.PanelChangedEventBase<T>
UnityEngine.UIElements.TooltipEvent
UnityEngine.UIElements.ValidateCommandEvent
UnityEngine.UIElements.WheelEvent
```

ふむむ、IMGUIEventなるものまで、あるんですねぇ…

前日の[@kingyo222](https://twitter.com/kingyo222)さんの[『Unity：UI Elements でいくつかサンプル書いたよ！』](https://www.fast-system.jp/unity-ui-elements-samples/)によりますと、Manipulatorなるものがあり、これを継承してVisualElementにAddManipulatorするといい感じにイベントを制御できるそうです。

```.cs
class MyManipulator : Manipulator
{
    protected override void RegisterCallbacksOnTarget()
    {
        // EventBaseなものを登録するとそれが取れるらしい
        target.RegisterCallback<ContextClickEvent>(OnMouseUpEvent); 
    }

    protected override void UnregisterCallbacksFromTarget()
    {
        target.UnregisterCallback<ContextClickEvent>(OnMouseUpEvent);
    }

    void OnMouseUpEvent(EventBase<ContextClickEvent> evt)
    {
        UnityEngine.Debug.Log("Nya");
    }
}

// 上のManipulatorをこんな感じで追加するとイベントが取れる

var button = labelFromUXML.Q<Button>("button");
button.AddManipulator(new MyManipulator());
```

個人的には素直にObservable.FromEventで以下のようにする手段の方が好きなんですが、個別にこのOnHogeAsObservableみたいなものを用意しないといけないので、それはそれで面倒です。
そういう意味では標準で用意されているManipulatorは使い勝手がいいと思います。

```.cs
public static IObservable<Unit> OnClickAsObservable(this Button source)
{
    return Observable.FromEvent(
        h => h,
        h => source.clickable.clicked += h,
        h => source.clickable.clicked -= h);
}

//==

var button = labelFromUXML.Q<Button>("button");
button.OnClickAsObservable()
    .Subscribe(_ => UnityEngine.Debug.Log("Nya"))
    .AddTo(LifeTimeDisposable);
```

## IManipulatorについて

基本イベントを取るだけであればManipulatorを継承しておけばいいと思いますが、AddManipulatorはIManipulatorを引数に取るので、本来はManipulatorより自由度が高いです。

```.cs
public interface IManipulator
{
    VisualElement target { get; set; }
}
```

というかんじで、Manipulator（操縦者）の通り、VisualElementをとりあえず操作するだけっぽいです。
UnityEditor上だとWindowが開かれたときにtargetのsetterが呼ばれるようになってます。
なので、UIアニメーションなど（がUIElements上でどう実装するのか、Animatorなのか、他なのかは置いといて）はこのManipulatorで操作してやるのかしら、という印象です。

```.cs
class ScaleAnimation : IManipulator
{
    private VisualElement _target;
    public VisualElement target
    {
        get { return _target; } // getは素直に返しましょう
        set
        {
            // Manipulatorの方はここでRegister/Unregisterを実装
            this._target = value;
        }
    }

    private float start;
    private float end;
    public ScaleAnimation(float start = 1f, float end = 1f) { // 割愛 }

    public async UniTask PlayAsync() { // 割愛 }
}

// ==

var button = labelFromUXML.Q<Button>("button");
var scaleAnimation = new ScaleAnimation(1f, 2f);
button.AddManipulator(scaleAnimation);

await scaleAnimation.PlayAsync(); // 任意のタイミングでPlayできるし、終了も待てる
```

おそらく上記のようなやり方だとRxだとやりづらくて、イベントとは別に明確に棲み分けができるものだと思います。