C#
Unity
async
uGUI
Unity2018

Unityでボタンの連打や同時押し対策とかを考える

普段UI周り触らない(触れない)人がUI関連触ってみましたシリーズ。
まさかの2回め。

1回めはこちら
UI上の長い文字列の扱いを考えてみる

事の発端

・独自のボタンコンポーネント単体で連打、同時押し対策をしたい
・そしたら押したときにフラグを立て そのフラグが立っている時は押しても処理しないにしてみよう
・そのフラグはstaticにしておけば他の独自ボタンコンポーネントも動作しない
・ボタンを押したコールバックは非同期処理も出来てそれが終わってからフラグを折るようにすれば処理待ちもできるかな

みたいなのを作ろうと思って何も考えずに引数にコルーチンわたして
動いた やったぜ!

とか喜んだのですが、IEnumaratorは再利用出来ないから2回め以降動かないっていう・・・。

「どうせだからあんまりよく知らないasyn awaitでやってみよう」

と思ったのが発端です。

自分でもあきれるくらいのダメ太郎だった処理

適当にIEnumaratorを引数に

public void SetOnClick(IEnumerator onClick){

処理の前後にフラグ操作を入れるので

IEnumerator OnClick(System.Collections.IEnumerator onClick)
{
    _isProgress = true;
    yield return onClick;
    _isProgress = false;
}

それをボタンのリスナーに登録

_button.onClick.AddListener(
    () =>
    {
        PlaySe(SeList.TestSe);
        if (_isProgress) return;
        StartCoroutine(OnClick(onClick));
    });

3秒待たせる
みたいなので登録したりする感じ

public IEnumerator OnClickButton3()
{
    Debug.Log("OnClickButton3 Start");
    yield return new WaitForSeconds(3);
    Debug.Log("OnClickButton3 End");
}

これでボタン押す→ボタンが押せなくなる→通信待つ→結果処理してからボタン有効化
みたいな流れができる
予定だった

1回めは良かったんだけどね・・・

そうだasync/awaitしてみよう

まずはasync/awaitをそもそも理解してなかったので
https://www.slideshare.net/UnityTechnologiesJapan/unite-tokyo-2018asyncawait
完全に理解する

理解した気になる!

引数を変更

public void SetOnClick(Func<Task> onClick)

ボタンのリスナーへの登録をasyncにしてawait onClick の前後にフラグ操作を入れる

 _button.onClick.AddListener(
    async () =>
    {
        PlaySe((int)SeList.TestSe);
        if (_isProgress) return;
        _isProgress = true;
        await onClick();
        _isProgress = false;
    });

とりあえず3秒待つ

private async Task AsyncMethod()
{
    Debug.Log("AsyncMethod Start");
    await Task.Delay(3000);
    Debug.Log("AsyncMethod End");
}

そういうボタンを生成する。

button = AddComponent<ButtonBase>(_rootObject.transform);
    button.Setup(
        res,
        new Vector2(120, 250),
        new Vector2(200f, 50f),
        "Async Method Callback");
    button.SetOnClick(AsyncMethod);
    button.FrameIn(SceneComponent.FrameInStartPos.Left);

dc12fc04-737a-2e97-eb52-c404d507f217.gif
それとなく動いた!

async/awaitでやりづらかったこと

とりあえずこれでボタン内部で処理待ちできるようになったわけですが
リソース読み込みとかで処理待つときってyield returnで待つこと多いですよね(しない?)

で このボタンの登録方法だとリソースの読み込みとかで完了を待つ場合

        var ret = Resources.LoadAsync<T>(path);
        ret.completed += res =>
        {
            if (res.isDone)
            {
                progress = false;
                Debug.Log("LoadAsync isDone");
            }
        };
        bool isCancel = false;
        var cancelToken = tokenSource.Token;
        await Task.Run(() =>
        {
            //クソオブクソ処理
            while (progress)
            {
                if (cancelToken.IsCancellationRequested)
                {
                    isCancel = true;
                    return;
                }
            }
        }, cancelToken);

みたいに すごく その アレ な感じになってしまうのが悩み
UniRxだとWaitWhileとかもawaitできたりするんですが
宗教上の理由で導入出来ないときとかは・・・
しかもTaskはWebGLで動かないので別途考えないと行けなかったり・・・

うーん
Await可能リソース読み込み作るかぁ・・・。

おわり

プロジェクトによってボタンの同時押し制御とかが結構違ったことしてたりするんだけど
どれもメリットもあればデメリットもある
そしてデメリットはどれも重かったりする・・・

一般的に同時、連打対策ってどうやるのが良いのかねぇ
今回はボタン自体にフラグ持たせてみたけどEventSystem使うほうが良いのかな?
個別に無効にする場合ならinteractableをfalseにするとかでもいいかなって思ったけども・・・

作業者にUIの有効無効制御をわざわざ書かせるとだいたい設定、解除忘れしてたりするからなぁ
個人的にそのへんはシステム側が汲み取っておいてあげたいところ。