【Unity】FocusTrap (フォーカス制御) コンポーネントの作り方

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

脱・初心者!Godot 4 ゲーム開発の「2歩目」

新品価格
¥831から
(2025/12/13 21:39時点)

Godot4ローグライク入門 ~ダンジョン自動生成~

新品価格
¥831から
(2025/12/13 21:44時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

UnityでUIを作り始めたとき、ついなんでもかんでも1つのスクリプトの Update() に書きがちですよね。
「UIが開いたら特定のボタンを選択状態にする」「ゲームパッド操作のときだけフォーカスを移動したい」といった処理も、Update() の中で if 文だらけになってしまうと、すぐに何をしているスクリプトなのか分からなくなります。

しかも、UIのパネルごとに「最初に選ばれていてほしいボタン」が違うのに、1つの巨大なUI管理スクリプトで全部コントロールしようとすると、Godクラス化してメンテナンスが地獄になります。

そこでこの記事では、「UIごとに、開いたときのフォーカス先をコンポーネント1つで完結させる」ための小さなコンポーネント
FocusTrap を作っていきます。

このコンポーネントをUIパネルに付けておくと、

  • ゲームパッド操作時にそのパネルが開いた瞬間
  • 指定したボタン(親のボタンなど)に自動でフォーカス(選択)を移す

といった挙動を、ほぼノーコードで実現できます。

【Unity】UIの初期フォーカスを自動制御!「FocusTrap」コンポーネント

ここでは、以下の要件を満たす FocusTrap を実装します。

  • UIパネル(GameObject)が SetActive(true) になったタイミングで動作
  • ゲームパッド入力が有効なときだけ、指定ボタンにフォーカスを当てる
  • マウス操作中は勝手にフォーカスを奪わない(オプション設定)
  • Unityの EventSystemSelectable(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セットアップ

  1. シーンに EventSystem を配置しておきます(通常はUIを作ると自動で入ります)。
  2. 新Input Systemを使っている場合は、PlayerInput コンポーネントをプレイヤーなどにアタッチし、
    Gamepad 用の Control Scheme を設定しておきます。
  3. Canvas 内に、ポーズメニュー用のパネル(例:PauseMenuPanel)を作成し、
    その子に「再開」「オプション」「タイトルへ戻る」などの Button を配置します。

手順②:FocusTrap コンポーネントを追加

  1. PauseMenuPanel の GameObject を選択します。
  2. Add Component から FocusTrap を追加します。
  3. Target Selectable に、最初にフォーカスさせたいボタン(例:ResumeButton)をドラッグ&ドロップします。
    親のボタンにしたい場合は、そのボタンを指定すればOKです。
  4. ゲームパッド操作時だけ動かしたいなら 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管理スクリプトに手を入れなくても、パネル単位でどんどん改善していけるのがいいですね。

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

脱・初心者!Godot 4 ゲーム開発の「2歩目」

新品価格
¥831から
(2025/12/13 21:39時点)

Godot4ローグライク入門 ~ダンジョン自動生成~

新品価格
¥831から
(2025/12/13 21:44時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

URLをコピーしました!