[Unity] 標準機能のみでボタンの連打、同時押し対応してみる


# 序
uGUI ボタンの連打、同時押し対応は皆さんどうしているだろうか。
行き当たりばったりプロジェクトだと、その場で作るわけだが、
意外とこれで決定版というものはない気がしている。

恐らく一番多いであろうと思うのは、Button クラスを継承して、
プロジェクト独自の Button クラスを作るパターンだろうか。

~~しかしながら、これには一つ面倒な点が存在する。
標準 Button の Inspector は、Editor 拡張によって使いやすいよう表示されており、
自前でクラスを継承した場合、Editor 拡張の方も作らないとダメじゃんとガッカリする事になる。
各状態の色指定を設定しなければいけないなど、結構手間がかかる。
(本気でやるなら公式の公開ソースから探して、そこから作るのがよさそう。)~~
特にイベント周りに手を入れようとしなければ、そのままの表示でしたorz
スワイプとかに対応しようとしてカスタムボタンにあるケースが多いですね…。

また、UIデザイナーさんがUIのひな型を作ってくれる場合、ボタンの配置はやってくれることが多いため、
エンジニアの方で再度置きなおしという手間を省くことができる。

しっかり作って、代々継承していけば問題ないわけだが、
今回は新規に作る場合に、手軽に導入できて、「あ、やべ。ボタン標準のやつのままじゃん。」という時に、
簡単に導入できるものを考える。

# どういうものが欲しいか

* 連打、同時押しに対応する
* 標準ボタンを変更しない。
* UniRXを使わない (地味に重要)
* 後から簡単に追加できる（既存構造に影響しない。）

こんなところだろうか。

このあたりの仕様が決まれば、自ずとやる事が見えてくる。

> 後から簡単に追加できる（既存構造に影響しない。）

このことから、コンポーネントで追加する形を採用する。

> 標準ボタンを変更しない。
> 連打、同時押しに対応する

標準ボタンを変更しないので、標準機能をそのまま活用する。
具体的には、 Intractable の変更で連打対応する。

同時押しに関しては、static なイベントを用意しておいて各々がそこにイベントを登録し、
イベントが発火した場合、Intractable を変更する。

# ソースコード

まんまコピーし、コンポーネントとして`UIButtonSyncBlocker`を追加すれば動作する。
特別なことはやっていない。ただし、**要.Net4.x**

```C#:UIButtonSyncBlocker 
using System;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;

public class UIButtonSyncBlocker : MonoBehaviour
{
    /// <summary>
    /// Intaractable が変更されるときに使用する Static な callback
    /// </summary>
    public static Action<bool> IntaractableSyncEvent;

    /// <summary>
    /// 個々に保持するUI
    /// </summary>
    [SerializeField] Button _SyncUIButton;

    [SerializeField] int _blockingTime = 1000;

    [SerializeField] bool _isSyncPointerUp = true;

    private bool _dontChengeFlag = false;

    private void Reset()
    {
        _SyncUIButton = GetComponent<Button>();
    }

    private void Awake()
    {
        IntaractableSyncEvent += SyncIntaractable;

        _SyncUIButton.onClick.AddListener(OnClickEvent);
    }

    private void OnDestroy()
    {
        IntaractableSyncEvent -= SyncIntaractable;
    }
    
    private void SyncIntaractable(bool isEnable)
    {
        if (isEnable)
        {
            // Enableのイベントが来た場合でも、フラグが立っている場合は元には戻さない
            if (_dontChengeFlag)
            {
                _dontChengeFlag = false;
                return;
            }
        } else
        {
            // 既に触れられない状態でコールバックが呼ばれた場合、変更対象から外す
            if (_SyncUIButton.interactable == false)
            {
                _dontChengeFlag = true;
                return;
            }
        }

        _SyncUIButton.interactable = isEnable;
    }
    
    public async void OnClickEvent()
    {
        // 同一フレームに2回イベントが発生した場合の対策
        if (_SyncUIButton.interactable == false) return;

        IntaractableSyncEvent?.Invoke(false);

        // TODO : Play SE

        await Task.Run( async () =>
        {
            await Task.Delay(_blockingTime);
        });

        IntaractableSyncEvent?.Invoke(true);
    }
}
```

## ざっくり解説

### Reset()
ランタイム時に`GetComponent`を避ける為、インスペクタ上見える形で、自動的にボタンを参照する。

### Awake(), OnDestory()
UniRxは使わない方針のため、MonoBehaviourのイベントを使用する。
初期化時に static な領域の`IntaractableSyncEvent`に同期処理を追加し、破壊されるときに削除する。

### OnClickEvent()
既存のボタンイベントに相乗りする形で実装される。

実行順的に、先に登録されているものがある可能性があるため、
そちらからこのボタンのIntaractable を参照するようなことはしてはいけない。(当たり前)

ただのイベントの相乗りな為、どこかで`onClick.RemoveAllListeners()`みたいなことをすると、
**当然まとめて吹き飛びます**。これは運用の注意として覚えておいた方がよさそう。

Task.Delayの部分は地味にマルチスレッド。
これをCoroutineで実装してしまった場合、
このGameObjectがDisableになってしまった場合に復帰処理が停止してしまう。
こういう挙動を理解していないと酷い目に合うので、基本的ではあるが重要。

# 発展形
この Intaractable というやつは、Buttonクラスのプロパティではない。
継承元の`UnityEngine.UI.Selectable`というやつが本体である。

つまるところ、今`Button`を取っているところを`Selectable`とする方が美しく、
また、これに対応することにより、`Toggle`や`InputField`など、多くのUIを同時に制御することができる。

ただし、これらはイベントが統一されているわけではないので、そこは個々に実装する必要がある。

また、コード上に`// TODO : Play SE` としているところがあるが、
この機能自体はサウンドを実装する機能ではない為、任意で調整して欲しい。

発展形のものは、気が向いたらgithubなどで公開するかもしれない。
複数ファイルに跨るので、Qiita だけでの記載は避けた次第だ。

以上