Unity/UniRx - 複数ボタンの同時押し/連打制御

## はじめに

UnityのButtonは標準で複数ボタンの同時押しや連打の制御がないため、自前で実装する必要があります。

UniRxを使うと（同じボタンの）連打に関しては記事が見つかりましたが、複数ボタンの同時押しを含めた制御について書かれている記事がなかったため、自分で実装してみました。


## 連打制御について

`OnClickAsObservable()`で`Button`からクリックイベントを取得し、`ThrottleFirst()`を使って指定時間の間、最初の1回のイベントのみ取り出すことで連打を防ぐ制御ができます。

以下は指定時間を1秒とした場合です。

```ButtonSample.cs
using UnityEngine;
using UnityEngine.UI;
using System.Collections;
using UniRx;
using UniRx.Triggers;
using System;

public class ButtonSample : MonoBehaviour
{
    public Button Button;

    void Start()
    {
        this.Button
            .OnClickAsObservable()
            .TakeUntilDestroy(this)
            .ThrottleFirst(TimeSpan.FromMilliseconds(1000))
            .Subscribe(_ => { OnClick(); });
    }

    private void OnClick()
    {
        // ボタンが押された時の処理
    }
}
```

参考: [UnityのButton連打を防ぎたい](https://qiita.com/mattak/items/a78b3ffa1144c9ff1750)


## 同時押し/連打制御について

連打制御の方法を応用して、複数のボタンクリックイベントを合わせて最初の1回だけ取り出すようにします。

イベントを合わせるのは、`Observable.Merge()`を使います。

イベントを合わせた後にどのボタンから発生したイベントなのかを判別する必要があるため、`Button`から発行されたイベントを`Select()`で任意のものに置き換えます。

置き換えるのは、intで0,1,2,3,...としてもいいですが、後でSwitch文で処理を条件分岐させるため、可読性の観点からenumを定義して使うのを推奨します。

```ButtonSample2.cs

    // 決定ボタン
    [SerializeField] private Button ConfirmButton;
    // キャンセルボタン
    [SerializeField] private Button CancelButton;

    enum ButtonType
    {
        Confirm,
        Cancel,
    }

    private void Start()
    {
        IObservable<ButtonType> observable1 = ConfirmButton
                .OnClickAsObservable()
                .TakeUntilDestroy(this)
                .Select(_ => ButtonType.Confirm)
            ;
        
        IObservable<ButtonType> observable2 = CancelButton
                .OnClickAsObservable()
                .TakeUntilDestroy(this)
                .Select(_ => ButtonType.Cancel)
            ;
        
        Observable.Merge(observable1, observable2)
            .ThrottleFirst(TimeSpan.FromMilliseconds(1000))
            .Subscribe(x =>
            {
                switch (x)
                {
                    case ButtonType.Confirm:
                        OnClickedConfirmButton();
                        break;
                    case ButtonType.Cancel:
                        OnClickedCancelButton();
                        break;
                }
            });
    }

    private void OnClickedConfirmButton()
    {
        // 決定ボタンが押された時の処理
    }

    private void OnClickedCancelButton()
    {
        // キャンセルボタンが押された時の処理
    }
```


あるいは、`Slect()`でボタン自身を取り出してenumの定義を省く書き方もあります。

```ButtonSample3.cs

    // 決定ボタン
    [SerializeField] private Button ConfirmButton;
    // キャンセルボタン
    [SerializeField] private Button CancelButton;

    private void Start()
    {
        IObservable<ButtonType> observable1 = ConfirmButton
                .OnClickAsObservable()
                .TakeUntilDestroy(this)
                .Select(_ => ConfirmButton)
            ;
        
        IObservable<ButtonType> observable2 = CancelButton
                .OnClickAsObservable()
                .TakeUntilDestroy(this)
                .Select(_ => CancelButton)
            ;
        
        Observable.Merge(observable1, observable2)
            .ThrottleFirst(TimeSpan.FromMilliseconds(1000))
            .Subscribe(x =>
            {
                if (x == ConfirmButton)
                {
                    OnClickedConfirmButton();
                }
                else if (x == CancelButton)
                {
                    OnClickedCancelButton();
                }
            });
    }

    private void OnClickedConfirmButton()
    {
        // 決定ボタンが押された時の処理
    }

    private void OnClickedCancelButton()
    {
        // キャンセルボタンが押された時の処理
    }
```


## あとがき
UniRxを使わない場合、boolを定義してフラグ管理を駆使する方法が考えられます。

しかし、この方法ではフラグの初期化を忘れてボタンを押せなくなる...みたいなバグが発生しがちで煩雑です。

UniRxの`ThrottleFirst()`を使うことでこの煩わしさから解放されるメリットは大きいと思います。

ボタンの数が片手で数えられるくらいならこの書き方でまかなえると思いますが、ScrollViewにセルがたくさんあって画面全体で制御をしないといけない場合はもっと捻った解決策が必要な想定です。