🆗

【uGUI】ボタン同時押し/連打対策で閃いた

2024/06/30に公開

はじめに

こんにちは、最近改めてボタンの連打や同時押しの対策を考えてみたところ、良さげな案を思いついたので共有です📝

環境:Unity 2022.3.11f1

よくある手法

まず、他の記事や今まで参加してきたプロジェクトでよく見る対策方法と懸念点を列挙します

  • UniRx(R3)のThrottleFirst等で対策する
    ⇨ 連打対策は簡単だが、同時押しは少し工夫が必要(コードが多少複雑になる)
    ⇨ 対応し忘れがあるとROM更新が必要となる(アセットバンドルで修正できない)
    ※コンポーネント化してうまく作ればできなくはなさそうではあるがルールが複雑になる
  • 標準Buttonを継承して対策を盛り込む
    ⇨ 標準Buttonを使用されないように設計やルール付けが必要
    ⇨ 一括置換や、標準Buttonが使われていないか判定する処理を作ることになる(ヒューマンエラー対策)
  • 独自コンポーネントを作って対策する(ラッパー、フルスクラッチ等)
    ⇨ 「標準Buttonを継承して対策を盛り込む」と同様の問題点がある
  • マルチタッチを無効化する
    ⇨ 同時押しは対策できても連打が対策できない
    ⇨ マルチタッチをする画面があとから出てくると苦労することになる

思いついた手法

「標準Buttonを継承して対策を盛り込む」の亜種みたいな感じですが、
「標準Buttonのコードに手をいれる」という手法を思いつきました。
(思いついたというか思考の初期段階で枝刈りされちゃっていたというか・・・)

「標準Buttonを継承して対策を盛り込む」では標準ボタンとの兼ね合い部分が課題となっていましたが、
この手法であればデフォルトの挙動自体をカスタムすることができるので、そこを一切考えずに済むようになります。

エンジンのコードに手を加えることになるので多少ダーティーではありますが、
対応漏れが原理的に発生しなくなるので、みんなハッピー🎉なのでは?と思いました。

対応手順

uGUIはデフォルトでPackageManagerで導入されるようになっており、その状態ではソースを編集することはできません。

なので、まずは下記手順で埋め込みパッケージにします
といっても難しい操作はなく、Library/PackageCache/com.unity.ugui@1.0.0フォルダを切り取って、Packagesフォルダ内にペーストするだけで完了です。
(バージョン管理に追加するのもお忘れなきよう)

あとは以下のようにButton.csをゴニョっと書き換えるだけで勝手に全ボタンに対策が反映されます🤗
(Runtime/UI/Core/Button.cs)

    [AddComponentMenu("UI/Button", 30)]
    public class Button : Selectable, IPointerClickHandler, ISubmitHandler
    {
        [Serializable]
        /// <summary>
        /// Function definition for a button click event.
        /// </summary>
        public class ButtonClickedEvent : UnityEvent {}

+#region ボタンの同時押し/連打対策で追加
+       [SerializeField]
+       private bool m_EnableThrottleFirst = true;
+
+       private static int s_LastPressFrame = -1;
+       private static double s_LastPressTime = 0.0;
+#endregion
        private void Press()
        {
            if (!IsActive() || !IsInteractable())
                return;

+#region ボタンの同時押し/連打対策で追加
+           if (m_EnableThrottleFirst)
+           {
+               var frame = Time.frameCount;
+               var time = Time.realtimeSinceStartupAsDouble;
+               if (s_LastPressFrame != -1 && (frame == s_LastPressFrame || (time - s_LastPressTime) <= 0.25))
+               {
+                   return;
+               }
+               s_LastPressFrame = frame;
+               s_LastPressTime = time;
+           }
+#endregion

            UISystemProfilerApi.AddMarker("Button.onClick", this);
            m_OnClick.Invoke();
        }

このままだと、EnableThrottleFirstがインスペクタに現れないのでエディタも編集📝
(Editor/UI/ButtonEditor.cs)

    public class ButtonEditor : SelectableEditor
    {
        SerializedProperty m_OnClickProperty;
+       SerializedProperty m_EnableThrottleFirstProperty;

        protected override void OnEnable()
        {
            base.OnEnable();
            m_OnClickProperty = serializedObject.FindProperty("m_OnClick");
+           m_EnableThrottleFirstProperty = serializedObject.FindProperty("m_EnableThrottleFirst");
        }

        public override void OnInspectorGUI()
        {
            base.OnInspectorGUI();
            EditorGUILayout.Space();

            serializedObject.Update();
+           EditorGUILayout.PropertyField(m_EnableThrottleFirstProperty);
            EditorGUILayout.PropertyField(m_OnClickProperty);
            serializedObject.ApplyModifiedProperties();
        }
    }

以上で対応作業は完了です🎉
ThrottleFirst相当の処理は適宜プロジェクトに合わせて色々調整してみてくださいね🤗

注意事項

エンジンのコードに手を加えるため、もしかするとライセンス周りで何かあったりするかも・・・?

また、ボタンにシリアライズ変数を追加しているので、既存プロジェクトに導入すると多くのアセットバンドルに差分が発生する可能性があります。

Discussion