UnityでUIを作り始めたとき、ついなんでもかんでも1つのスクリプトの Update() に書きがちですよね。
「UIが開いたら特定のボタンを選択状態にする」「ゲームパッド操作のときだけフォーカスを移動したい」といった処理も、Update() の中で if 文だらけになってしまうと、すぐに何をしているスクリプトなのか分からなくなります。
しかも、UIのパネルごとに「最初に選ばれていてほしいボタン」が違うのに、1つの巨大なUI管理スクリプトで全部コントロールしようとすると、Godクラス化してメンテナンスが地獄になります。
そこでこの記事では、「UIごとに、開いたときのフォーカス先をコンポーネント1つで完結させる」ための小さなコンポーネント
FocusTrap を作っていきます。
このコンポーネントをUIパネルに付けておくと、
- ゲームパッド操作時にそのパネルが開いた瞬間
- 指定したボタン(親のボタンなど)に自動でフォーカス(選択)を移す
といった挙動を、ほぼノーコードで実現できます。
【Unity】UIの初期フォーカスを自動制御!「FocusTrap」コンポーネント
ここでは、以下の要件を満たす FocusTrap を実装します。
- UIパネル(GameObject)が
SetActive(true)になったタイミングで動作 - ゲームパッド入力が有効なときだけ、指定ボタンにフォーカスを当てる
- マウス操作中は勝手にフォーカスを奪わない(オプション設定)
- Unityの
EventSystemとSelectable(Button, Toggle, Sliderなど)を利用
フルコード
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using UnityEngine.InputSystem; // 新Input System用
/// <summary>
/// 指定したUI要素にフォーカス(選択状態)を当てるコンポーネント。
/// - このコンポーネントを持つGameObjectが有効化されたとき(OnEnable)に発動
/// - ゲームパッド操作時に、指定ボタンへ自動でフォーカスを移す
/// - オプションで「マウス操作中はフォーカスを奪わない」などの制御が可能
///
/// 想定用途:
/// - ポーズメニューを開いたら「再開」ボタンにフォーカス
/// - オプション画面を開いたら最初のタブボタンにフォーカス
/// - サブウィンドウを開いたら「OK」ボタンにフォーカス
/// </summary>
[DisallowMultipleComponent]
public class FocusTrap : MonoBehaviour
{
[Header("フォーカス対象")]
[SerializeField]
private Selectable targetSelectable;
// UIが開いたときにフォーカスを当てたいボタンなど
// 例: 親の「戻る」ボタン、メニューの最初のボタン
[Header("動作条件")]
[SerializeField]
[Tooltip("ゲームパッド入力が有効なときのみフォーカスを移動するか")]
private bool onlyWhenGamepadActive = true;
[SerializeField]
[Tooltip("マウス入力が最近使われている場合はフォーカスを奪わない")]
private bool ignoreWhenMouseRecentlyUsed = true;
[SerializeField]
[Tooltip("マウスが最近使われたとみなす秒数(ignoreWhenMouseRecentlyUsedがtrueのとき有効)")]
private float mouseRecentThresholdSeconds = 1.0f;
[Header("遅延設定")]
[SerializeField]
[Tooltip("有効化直後はレイアウト更新前のことがあるので、少し待ってからフォーカスを当てたい場合に設定")]
private float focusDelaySeconds = 0.02f;
// マウスが最後に動いた時間
private float _lastMouseMoveTime;
// 新Input Systemのデバイス判定用
private PlayerInput _playerInput;
private void Awake()
{
// 同じGameObjectにPlayerInputがあれば取得(任意)
_playerInput = GetComponentInParent<PlayerInput>();
}
private void OnEnable()
{
// 自動でフォーカスをセットしたいので、OnEnableで処理を開始
// ただし、まだUIのレイアウトが落ち着いていないことがあるため
// コルーチンで少し待ってから実行する
if (gameObject.activeInHierarchy)
{
StartCoroutine(FocusRoutine());
}
}
private void Update()
{
// マウスが動いたら時間を記録
// ※マウス入力を無視したい場合の判定に使う
Vector2 mouseDelta = Mouse.current != null ? Mouse.current.delta.ReadValue() : Vector2.zero;
if (mouseDelta.sqrMagnitude > 0.0f)
{
_lastMouseMoveTime = Time.unscaledTime;
}
}
/// <summary>
/// 実際にフォーカスをセットする処理(コルーチン)
/// </summary>
private System.Collections.IEnumerator FocusRoutine()
{
// 遅延時間が設定されていれば待機
if (focusDelaySeconds > 0f)
{
yield return new WaitForSecondsRealtime(focusDelaySeconds);
}
else
{
// 1フレームだけ待つ簡易版
yield return null;
}
TrySetFocus();
}
/// <summary>
/// 条件をチェックしてからフォーカスを設定する
/// </summary>
private void TrySetFocus()
{
// 対象が設定されていなければ何もしない
if (targetSelectable == null)
{
Debug.LogWarning($"[FocusTrap] targetSelectable が設定されていません: {name}", this);
return;
}
// 自身または対象が非アクティブなら何もしない
if (!gameObject.activeInHierarchy || !targetSelectable.gameObject.activeInHierarchy)
{
return;
}
// マウスが最近使われているならスキップ(オプション)
if (ignoreWhenMouseRecentlyUsed)
{
float elapsed = Time.unscaledTime - _lastMouseMoveTime;
if (elapsed < mouseRecentThresholdSeconds)
{
// 直近でマウスが動いているので、ゲームパッド用の自動フォーカスは抑制
return;
}
}
// ゲームパッドが有効なときのみ動作させたい場合のチェック
if (onlyWhenGamepadActive && !IsGamepadCurrentControlScheme())
{
return;
}
// EventSystemが存在しないとフォーカスを設定できない
EventSystem eventSystem = EventSystem.current;
if (eventSystem == null)
{
Debug.LogWarning("[FocusTrap] EventSystem がシーンに存在しません。", this);
return;
}
// すでに同じオブジェクトが選択されている場合は何もしない
if (eventSystem.currentSelectedGameObject == targetSelectable.gameObject)
{
return;
}
// 実際にフォーカスを設定
eventSystem.SetSelectedGameObject(targetSelectable.gameObject);
// ナビゲーションが正常に働くように、Selectable側にも選択状態を通知
targetSelectable.OnSelect(null);
}
/// <summary>
/// 現在の入力スキームがゲームパッド系かどうかを判定する。
/// PlayerInput があればそれを優先し、なければ接続中のGamepadを見て簡易判定。
/// </summary>
private bool IsGamepadCurrentControlScheme()
{
// PlayerInput があれば currentControlScheme を利用
if (_playerInput != null)
{
// ここでは "Gamepad" を含むスキーム名をゲームパッド扱いとする
string scheme = _playerInput.currentControlScheme;
if (string.IsNullOrEmpty(scheme))
{
return false;
}
return scheme.Contains("Gamepad");
}
// PlayerInput がない場合は、単純にGamepadが接続されているかどうかで判定
return Gamepad.current != null;
}
/// <summary>
/// インスペクタからフォーカス対象を自動設定するためのヘルパー。
/// 親階層の最初のSelectableを検索してセットします。
/// エディタ上でコンテキストメニューから呼び出せます。
/// </summary>
[ContextMenu("親階層から最初のSelectableを自動設定")]
private void AutoAssignSelectableFromParent()
{
if (targetSelectable != null)
{
Debug.Log("[FocusTrap] すでに targetSelectable が設定されています。", this);
return;
}
Selectable found = GetComponentInParent<Selectable>();
if (found != null)
{
targetSelectable = found;
Debug.Log($"[FocusTrap] 親階層から {found.name} を targetSelectable に設定しました。", this);
}
else
{
Debug.LogWarning("[FocusTrap] 親階層に Selectable が見つかりませんでした。", this);
}
}
}
使い方の手順
ここでは例として、ポーズメニューとオプション画面での利用方法を説明します。
手順①:前提のUIセットアップ
- シーンに EventSystem を配置しておきます(通常はUIを作ると自動で入ります)。
- 新Input Systemを使っている場合は、PlayerInput コンポーネントをプレイヤーなどにアタッチし、
Gamepad用の Control Scheme を設定しておきます。 - Canvas 内に、ポーズメニュー用のパネル(例:
PauseMenuPanel)を作成し、
その子に「再開」「オプション」「タイトルへ戻る」などのButtonを配置します。
手順②:FocusTrap コンポーネントを追加
PauseMenuPanelの GameObject を選択します。- Add Component から
FocusTrapを追加します。 - Target Selectable に、最初にフォーカスさせたいボタン(例:
ResumeButton)をドラッグ&ドロップします。
親のボタンにしたい場合は、そのボタンを指定すればOKです。 - ゲームパッド操作時だけ動かしたいなら Only When Gamepad Active をオンにしておきます(デフォルトでオン)。
手順③:UIの表示・非表示の制御
ポーズメニューを開く処理を、例えば以下のようなシンプルなスクリプトで制御します。
using UnityEngine;
using UnityEngine.InputSystem;
public class SimplePauseController : MonoBehaviour
{
[SerializeField]
private GameObject pauseMenuPanel; // PauseMenuPanel (FocusTrap付き)
private bool _isPaused;
private void Start()
{
// 開始時はポーズメニューを非表示にしておく
if (pauseMenuPanel != null)
{
pauseMenuPanel.SetActive(false);
}
}
// 新Input Systemのアクションから呼び出す想定
public void OnPause(InputAction.CallbackContext context)
{
if (!context.performed)
return;
TogglePause();
}
private void TogglePause()
{
_isPaused = !_isPaused;
if (pauseMenuPanel != null)
{
pauseMenuPanel.SetActive(_isPaused);
// SetActive(true) されたタイミングで FocusTrap.OnEnable が呼ばれ、
// 自動的に指定ボタンへフォーカスが移ります。
}
Time.timeScale = _isPaused ? 0f : 1f;
}
}
これで、ゲームパッドでポーズボタンを押したときに、
PauseMenuPanel がアクティブになり、FocusTrap が自動で「再開」ボタンにフォーカスを当ててくれます。
手順④:他のUI(例:オプション画面、サブウィンドウ)にも流用
- オプション画面:
OptionsPanelにも同様にFocusTrapを付けて、
最初のタブボタンや「戻る」ボタンをTarget Selectableに指定します。 - サブウィンドウ(確認ダイアログなど):
「OK」「キャンセル」ボタンを持つダイアログパネルにFocusTrapを付けて、
デフォルトで押してほしいボタン(例:OK)を指定します。
どのUIパネルでも、「開いた瞬間にどのボタンにフォーカスしてほしいか」をそのパネル自身に閉じ込められるので、
巨大なUIマネージャースクリプトに条件分岐を書き足していく必要がなくなります。
メリットと応用
FocusTrap を使うことで、以下のようなメリットがあります。
- Godクラス化を防げる
「UIが開いたらどのボタンを選ぶか」を、各パネルの責務として分離できます。
ポーズメニュー、オプション、インベントリなど、それぞれのプレハブにFocusTrapを付けるだけで完結します。 - プレハブ単位で完結するのでレベルデザインが楽
UIプレハブを別シーンに持っていっても、そのプレハブ内にFocusTrapが完結しているので、
シーン側のスクリプトをいじらなくても「開いたときのフォーカス」が維持されます。 - ゲームパッドとマウスの共存がしやすい
「ゲームパッドで操作しているときだけ自動フォーカス」「マウスを動かしたら自動フォーカスを抑制」など、
入力デバイスに応じた気持ちよい挙動を簡単に作れます。 - ナビゲーションの起点を明示できる
UIデザイナーやレベルデザイナーが、「この画面はこのボタンから始まる」という意図を、
FocusTrapの設定としてプレハブに残せます。
改造案:フォーカス時にゲームパッドカーソル用のSEを鳴らす
フォーカスが当たった瞬間に効果音を鳴らしたい場合、TrySetFocus() の最後に以下のようなメソッドを呼び出すのもアリですね。
private void PlayFocusSound()
{
// ここでは簡易例として、同じGameObjectに付いているAudioSourceを利用
AudioSource audio = GetComponent<AudioSource>();
if (audio == null || audio.clip == null)
{
return;
}
// タイムスケールに影響されないように、unscaledTimeベースで鳴らしたい場合は
// AudioSourceの設定で「Ignore Listener Pause」なども検討しましょう。
audio.Play();
}
TrySetFocus() の最後に PlayFocusSound(); を呼ぶようにすれば、
UIが開いてフォーカスが移動したタイミングで、カーソル移動用のSEを鳴らせます。
このように、小さなコンポーネントに責務を切り出しておくと、
「フォーカス制御+演出」「フォーカス制御+アニメーション」などの拡張も簡単になります。
巨大なUI管理スクリプトに手を入れなくても、パネル単位でどんどん改善していけるのがいいですね。




