
ボタンタップ時に発生する不具合の対策について

IT技術

はじめに
今回は、Unityにおけるボタンの連打・同時押し対策についてまとめました。
業務でゲーム開発を進める中で、これらの対策を初めて実装する機会があり、多くの学びがあったので、これからUnityでゲームを開発される方の参考になれば幸いです!
前提として、アーキテクチャにはMVPを採用し、ライブラリにはUniTaskおよびR3を使用しています。
どんな不具合が起きたのか
実際に発生した不具合は以下の2点です。
- ボタンを連打した後、タップしても処理が実行されず、ゲームが進行不能になる
- 複数のボタンを同時にタップした際、それぞれの処理が並行して実行され、想定外の挙動になる
いずれも致命的な問題ですが、比較的シンプルな対策で解消できたため、今回はその方法をご紹介します!
基本の実装例
不具合が発生していた当時は、主にViewからPresenterに対してボタンタップイベントをSubjectで通知する実装を行っていました。
View
1private readonly Subject<string> _onButtonClick = new();
2public IObservable<string> OnButtonClick => _onButtonClick;
3
4view.OnButtonClick.Subscribe(_ =>
5{
6 _onButtonClick.OnNext("通知したい内容");
7}).AddTo(this);
Presenter
1_view.OnButtonClick
2 .Subscribe(() =>
3 {
4 ShowPopup();
5 }).AddTo(this);
6
7public void ShowPopup()
8{
9 // Popup表示処理
10}
この状態では通常のタップ操作では問題ありませんが、連打や同時押しには無防備で、不具合が発生しやすい状況でした。
問題ごとの対策方法
対策方法は処理内容に応じて異なりますが、以下のような対応を行いました。
- SubscribeAwait を使用して非同期処理の重複実行を防止
- R3の ThrottleFirst を使用して一定時間内の連続タップを抑止
- View側のみで完結する処理は購読を外して制御
A. ボタンを素早く連打したとき
SubscribeAwait を使うことで、非同期処理が完了するまで次のタップイベントを処理しないようにできます。
1_view.OnButtonClick
2 .SubscribeAwait(async (_, cancellationToken) =>
3 {
4 await ShowPopup(cancellationToken);
5 }).AddTo(this);
このようにすることで、ShowPopupの処理が完了するまで Subject でイベントが通知されても購読側では処理されなくなり、重複実行を防げます。
なぜSubjectで通知されなくなるのか?
正確には、通知自体は OnNext で発行されています。
しかし SubscribeAwait を使っている場合、非同期処理が進行中の間は、新たな通知が届いても購読側の処理が開始されません。
つまり、「通知はされているが処理されない状態」になるため、結果的に通知されていないように見えるのです。
これは、非同期処理が完了するまで SubscribeAwait が次のイベント処理を受け付けない仕組みになっているためです。
B. 異なるボタンを同時タップしたとき
同時に複数のボタンをタップして処理が重なるのを防ぐには、ThrottleFirst が有効です。
1var defaultButtonInputSpan = TimeSpan.FromMilliseconds(100);
2
3_view.OnButtonClick
4 .ThrottleFirst(defaultButtonInputSpan)
5 .Subscribe(() =>
6 {
7 ShowPopup();
8 }).AddTo(this);
ThrottleFirst は、指定した時間内に受け取ったイベントのうち、最初の一つだけを処理し、それ以降は無視します。
これにより、同時押しによる多重実行を防止できます。
また、View側で完結する処理の場合は、購読を外すだけでもシンプルに対応できます。
1[SerializeField]
2private BasicButton _button;
3
4public void Setup()
5{
6 _button.OnClick
7 .SubscribeAwait(async (_, ct) => await ShowPopup(ct))
8 .AddTo(this);
9}
C. アニメーションが絡む処理
アニメーションが含まれる場合、単純なボタン制御では不十分になることがあります。
※ここでは連打防止については割愛
例えば、
ボタンタップ → Model処理 → アニメーション → View更新
という流れになると、アニメーションの完了を待ってから次のイベントが処理されるため、体感的に遅延を感じることがあります。
このような場合は、以下のように UniTask.Void や Forget() を使って非同期処理の完了を待たずに次の処理に進ませることが可能です。
1_view.OnButtonClick
2 .SubscribeAwait(async (_, cancellationToken) =>
3 {
4 await _model.ConsumeItem();
5 UniTask.Void(async () =>
6 {
7 await _view.Animation(cancellationToken);
8 _view.ShowItem(cancellationToken);
9 });
10 }).AddTo(this);
または:
1_view.OnButtonClick
2 .SubscribeAwait(async (_, cancellationToken) =>
3 {
4 await _model.ConsumeItem();
5 _view.Animation(cancellationToken).Forget();
6 _view.ShowItem(cancellationToken);
7 }).AddTo(this);
どちらも、ユーザー操作の反応をスムーズに保つのに役立ちます。
まとめ
今回は、Unityにおけるボタンタップ時の不具合とその対策方法についてご紹介しました。
ボタン制御は一見シンプルに見えても、アニメーションや非同期処理が絡むと一気に複雑になります。
今回の経験を通じて、UI制御の大切さと、ユーザー体験を考慮した設計の重要性を改めて実感しました。
今後もUnityでの開発を通じて学んだことを発信していきたいと思います!
ライトコードでは、エンジニアを積極採用中!
ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。
採用情報へ

初めまして! たか と申します! 前職も同じくエンジニアとして働いておりました! 趣味は、映画・アニメを見たり、スポーツが好きです。 まだまだ未熟ですが、よろしくお願いします!