現場レベルのゲーム制作が、すべてここで学べます。
UnityでUIを開発したり、スクリプトでUIを操作していると「コードで値を変えたら意図しない関数が連鎖して無限ループしてしまう」という経験をしたことはないでしょうか。
この問題を解決するのが suppressCallbackパターン です。
Unity・C#の具体例を5つ使って丁寧に解説します。
suppressCallbackとは
suppressCallbackとは、コールバック(イベント処理)を一時的に抑制するためのbool型フラグ変数です。
|
1 |
private bool suppressCallback = false; |
このフラグを true にしている間は、イベントハンドラの冒頭で早期リターンさせることで、不要な処理の連鎖を防ぎます。
|
1 2 3 4 |
void OnToggleChanged(bool isOn) { if (suppressCallback) return; // フラグが立っていたら何もしない // 以降の処理... } |
なぜsuppressCallbackが必要なのか
UnityのToggleやSliderなどのUIコンポーネントは、値が変わると自動的にイベントを呼び出します。これはプレイヤーが操作したときには便利ですが、コードから値を書き換えたときにも同じイベントが発火してしまいます。
実例1:経営シミュレーションの強制OFF処理
所持金が不足したときに施設を強制OFFにするケースです。これは特に重要な実装パターンで、安全に停止させるための処理が複数組み合わさっています。
|
1 2 3 4 5 6 7 8 9 10 11 12 |
private void ForceOffBecauseNoMoney() { StopBilling(); // ① 課金処理を止める // ② トグルをOFFにするがイベントは発火させない suppressCallback = true; if (toggle != null) toggle.SetIsOnWithoutNotify(false); suppressCallback = false; ApplyActive(false); // ③ 対象オブジェクトを無効化 } |
suppressCallbackがない場合の危険な流れ
- お金不足を検知
- StopBilling()
- toggle.isOn = false(普通にOFFにする)
- OnToggleChanged(false) が発火! また呼ばれる
- StopBilling() がまた呼ばれる… 二重処理・ループ
suppressCallbackがある場合の安全な流れ
- お金不足を検知
- StopBilling()
- suppressCallback = true(フラグを立てる)
- SetIsOnWithoutNotify(false)(UIだけ更新)
- suppressCallback = false
- ApplyActive(false)(1回だけ意図通りに実行)
SetIsOnWithoutNotifyだけでは不十分な理由
Unityには SetIsOnWithoutNotify() というAPIがあり、これ自体はUnityの標準イベントを発火させません。しかし、ゲーム独自のロジックまでは止められません。
suppressCallbackは独自処理も含めて止める二重の安全装置として機能します。
関連記事:【Unity】SetIsOnWithoutNotifyの使い方|onValueChangedを発火させずにToggleを変更する

実例2:スライダーと入力欄の相互連動
音量設定画面など、スライダーと数値入力欄が連動しているUIは無限ループの典型的な発生源です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
void OnSliderChanged(float value) { if (suppressCallback) return; suppressCallback = true; inputField.text = value.ToString(); // ← OnInputChangedを呼んでしまう suppressCallback = false; ApplyVolume(value); } void OnInputChanged(string text) { if (suppressCallback) return; suppressCallback = true; slider.value = float.Parse(text); // ← OnSliderChangedを呼んでしまう suppressCallback = false; ApplyVolume(float.Parse(text)); } |
実例3:セーブデータのロード時
ゲーム起動時にセーブデータをUIに反映する処理でも必須です。
|
1 2 3 4 5 6 7 8 9 10 11 |
void LoadSaveData(SaveData data) { suppressCallback = true; // ロード中はイベントを全部無視 bgmToggle.isOn = data.bgmEnabled; difficultyDropdown.value = data.difficulty; masterVolume.value = data.volume; suppressCallback = false; // UIの見た目だけ復元できた。余計な処理は一切走っていない ✅ } |
セーブデータの読み込みはあくまで「UIの復元」であり、「プレイヤーが操作した」わけではありません。フラグがないと、ロードのたびにBGM処理・難易度変更処理が実行されてしまいます。
実例4:「全選択」ボタン
チェックボックスが多数ある画面に「全選択」ボタンを実装するケースです。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
void OnSelectAllClicked() { suppressCallback = true; // 個別イベントを全部止める foreach (var checkbox in itemCheckboxes) checkbox.isOn = true; suppressCallback = false; OnSelectionChanged(); // まとめて1回だけ処理 ✅ } void OnSingleCheckboxChanged(bool value) { if (suppressCallback) return; // 全選択中は無視 OnSelectionChanged(); } |
チェックボックスが100個あれば、フラグなしでは OnSelectionChanged が100回呼ばれます。通信処理やDB更新を伴う場合は致命的なパフォーマンス問題になります。
実例5:Undo / Redo(やり直し機能)
やり直し機能の実装でも、suppressCallbackは欠かせません。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
void Undo() { suppressCallback = true; // 「巻き戻し中」フラグ var prevState = history.Pop(); hpSlider.value = prevState.hp; nameInput.text = prevState.name; suppressCallback = false; RefreshUI(); } void OnHpSliderChanged(float value) { if (suppressCallback) return; // Undo中は履歴に積まない! history.Push(CurrentState()); // ← 呼ばれると履歴が壊れる ApplyHp(value); } |
フラグがないと、Undoで値を戻したときに「Undoした操作」が新たな履歴として積まれ、Redoが完全に壊れます。
場面別まとめ
| 場面 | フラグなしで起こる問題 |
|---|---|
| スライダー ↔ 入力欄の連動 | 無限ループ |
| セーブデータのロード | ロードのたびに処理が走る |
| 全選択ボタン | 重い処理が N 回呼ばれる |
| Undo / Redo | 履歴データが壊れる |
| 強制OFF処理(残金不足など) | 二重処理・ループの危険 |
suppressCallbackのベストプラクティス
try-finallyで確実にフラグをリセットする
例外が発生してもフラグが残らないよう、try-finallyを使うとより安全です。
|
1 2 3 4 5 6 7 8 9 |
suppressCallback = true; try { toggle.SetIsOnWithoutNotify(false); } finally { suppressCallback = false; } |
フラグの意図をコメントで明記する
後から読む人(未来の自分も含む)のために、なぜフラグを使うのかを残しましょう。
|
1 2 |
// UIの値をコードから変更するため、イベントを一時停止 suppressCallback = true; |
規模が大きくなったら専用クラスに切り出す
管理対象が増えたら、フラグ管理を専用クラスに委ねることも検討しましょう。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class SuppressScope : IDisposable { private readonly Action<bool> setter; public SuppressScope(Action<bool> setter) { this.setter = setter; setter(true); } public void Dispose() => setter(false); } // 使用例 using (new SuppressScope(v => suppressCallback = v)) { toggle.SetIsOnWithoutNotify(false); } |
現場レベルのゲーム制作が、すべてここで学べます。







コメント