序
uGUI ボタンの連打、同時押し対応は皆さんどうしているだろうか。
行き当たりばったりプロジェクトだと、その場で作るわけだが、
意外とこれで決定版というものはない気がしている。
恐らく一番多いであろうと思うのは、Button クラスを継承して、
プロジェクト独自の Button クラスを作るパターンだろうか。
しかしながら、これには一つ面倒な点が存在する。
標準 Button の Inspector は、Editor 拡張によって使いやすいよう表示されており、
自前でクラスを継承した場合、Editor 拡張の方も作らないとダメじゃんとガッカリする事になる。
各状態の色指定を設定しなければいけないなど、結構手間がかかる。
(本気でやるなら公式の公開ソースから探して、そこから作るのがよさそう。)
特にイベント周りに手を入れようとしなければ、そのままの表示でしたorz
スワイプとかに対応しようとしてカスタムボタンにあるケースが多いですね…。
また、UIデザイナーさんがUIのひな型を作ってくれる場合、ボタンの配置はやってくれることが多いため、
エンジニアの方で再度置きなおしという手間を省くことができる。
しっかり作って、代々継承していけば問題ないわけだが、
今回は新規に作る場合に、手軽に導入できて、「あ、やべ。ボタン標準のやつのままじゃん。」という時に、
簡単に導入できるものを考える。
どういうものが欲しいか
- 連打、同時押しに対応する
- 標準ボタンを変更しない。
- UniRXを使わない (地味に重要)
- 後から簡単に追加できる(既存構造に影響しない。)
こんなところだろうか。
このあたりの仕様が決まれば、自ずとやる事が見えてくる。
後から簡単に追加できる(既存構造に影響しない。)
このことから、コンポーネントで追加する形を採用する。
標準ボタンを変更しない。
連打、同時押しに対応する
標準ボタンを変更しないので、標準機能をそのまま活用する。
具体的には、 Intractable の変更で連打対応する。
同時押しに関しては、static なイベントを用意しておいて各々がそこにイベントを登録し、
イベントが発火した場合、Intractable を変更する。
ソースコード
まんまコピーし、コンポーネントとしてUIButtonSyncBlocker
を追加すれば動作する。
特別なことはやっていない。ただし、要.Net4.x
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 だけでの記載は避けた次第だ。
以上