LoginSignup
16
13

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-08-20

この記事は、これは『Unityゆるふわサマーアドベントカレンダー2019』の21日目の記事、『UIElementsでもUniRxを使いたい』の補足記事です。

Subjectを使ってIMGUIをIObservable化

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

public class ImguiSampleScene : MonoBehaviour
{
    bool previousPressed;

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

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

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;
    }
}

これを使うと

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化

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

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化することで疎にしましょう。

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

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

[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);
    }
}

これを使う場合は、

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が継承された各種イベントがあるようです。

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さんの『Unity:UI Elements でいくつかサンプル書いたよ!』によりますと、Manipulatorなるものがあり、これを継承してVisualElementにAddManipulatorするといい感じにイベントを制御できるそうです。

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は使い勝手がいいと思います。

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より自由度が高いです。

public interface IManipulator
{
    VisualElement target { get; set; }
}

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

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だとやりづらくて、イベントとは別に明確に棲み分けができるものだと思います。

16
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
13