この記事は、これは『Unityゆるふわサマーアドベントカレンダー2019』の21日目の記事、[『UIElementsでもUniRxを使いたい』]
(https://qiita.com/yKimisaki/items/c45e9cc7d2c01199a0c3)の補足記事です。
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だとやりづらくて、イベントとは別に明確に棲み分けができるものだと思います。