Unityを触り始めた頃って、つい Update() に「プレイヤー移動」「ジャンプ」「UI更新」「カメラ制御」…と全部まとめて書いてしまいがちですよね。PC向けならまだ動くのですが、モバイル対応を始めた途端に、

  • 画面タッチの処理が Update() にベタ書きされてカオスになる
  • シーンごとにUIの書き換えが必要になってコピペ地獄
  • 「Aボタン」「Bボタン」を追加したいだけなのに、入力コードをいじる必要が出てくる

こうなると、ちょっとした仕様変更やデバッグのたびに巨大スクリプトを開いて、スクロールしながら探すハメになります。これはまさに「Godクラス」状態ですね。

そこでこの記事では、スマホ画面に「A/Bボタン」を表示して、Unityの新しいInput Systemの TouchScreenButton に入力を送る専用コンポーネント 「VirtualButtons」 を用意して、

  • 「UIの見た目」担当(Canvas / Button)
  • 「入力イベントを送る」担当(TouchScreenButton)
  • 「それらをまとめて制御する」担当(VirtualButtons)

というふうに責務を分離していきましょう。

【Unity】スマホでもA/B入力をサクッと実装!「VirtualButtons」コンポーネント

ここでは、

  • 画面左下に「Aボタン」
  • 画面右下に「Bボタン」

を表示し、それぞれ TouchScreenButton コンポーネントを通じて Input System のアクション を発火させる仕組みを作ります。

ポイントは:

  • UIのボタンは UnityEngine.UI.Button を使う
  • 実際の「入力」は UnityEngine.InputSystem.OnScreen.TouchScreenButton に任せる
  • VirtualButtons は「A/Bボタンのセットアップ」と「有効/無効切り替え」だけを担当する

というシンプルな責務に絞ることです。

フルコード:VirtualButtons コンポーネント


using UnityEngine;
using UnityEngine.UI; // UI Button, Image など
using UnityEngine.InputSystem; // 新Input System
using UnityEngine.InputSystem.OnScreen; // TouchScreenButton

/// <summary>
/// スマホ画面に A / B ボタンを表示し、
/// TouchScreenButton 経由で Input System に入力を送るコンポーネント。
/// 
/// - A/Bボタンの UI(Button) と TouchScreenButton をまとめて管理
/// - 有効/無効の切り替え
/// - エディタ上での簡単なセットアップ補助
/// 
/// ※このスクリプト単体では InputAction は生成しません。
///   あらかじめ InputActionAsset を作成し、
///   TouchScreenButton の "Control Path" に対応するボタンを設定してください。
/// </summary>
public class VirtualButtons : MonoBehaviour
{
    [Header("Aボタン UI")]
    [SerializeField] private Button aButton; // Aボタンの UI Button
    [SerializeField] private Image aButtonImage; // Aボタンの見た目(任意)
    [SerializeField] private TouchScreenButton aTouchButton; // Aボタンに対応する TouchScreenButton

    [Header("Bボタン UI")]
    [SerializeField] private Button bButton; // Bボタンの UI Button
    [SerializeField] private Image bButtonImage; // Bボタンの見た目(任意)
    [SerializeField] private TouchScreenButton bTouchButton; // Bボタンに対応する TouchScreenButton

    [Header("共通設定")]
    [SerializeField] private bool startEnabled = true; // 開始時にボタンを有効にするか
    [SerializeField] private Canvas targetCanvas; // UI を乗せる Canvas(省略可)

    // 内部状態
    private bool isInitialized;

    private void Awake()
    {
        InitializeIfNeeded();

        // 起動時の有効/無効を反映
        SetButtonsActive(startEnabled);
    }

    /// <summary>
    /// 参照が足りない場合に、可能な範囲で自動取得する。
    /// エディタでのセットアップ漏れを減らす目的。
    /// </summary>
    private void InitializeIfNeeded()
    {
        if (isInitialized) return;

        // Canvas が未設定なら同じ階層から探す
        if (targetCanvas == null)
        {
            targetCanvas = GetComponentInParent<Canvas>();
        }

        // Aボタン関連が未設定なら子階層から探す
        if (aButton == null || aButtonImage == null || aTouchButton == null)
        {
            TryAutoAssignAButton();
        }

        // Bボタン関連が未設定なら子階層から探す
        if (bButton == null || bButtonImage == null || bTouchButton == null)
        {
            TryAutoAssignBButton();
        }

        isInitialized = true;
    }

    /// <summary>
    /// Aボタン関連を子オブジェクトから自動で探す。
    /// 名前に "A" を含むオブジェクトを優先して取得する簡易実装。
    /// </summary>
    private void TryAutoAssignAButton()
    {
        // Button を探す
        if (aButton == null)
        {
            aButton = FindButtonInChildrenByNameContains("A");
            if (aButton == null)
            {
                aButton = GetComponentInChildren<Button>();
            }
        }

        // Image を探す
        if (aButtonImage == null && aButton != null)
        {
            aButtonImage = aButton.GetComponent<Image>();
        }

        // TouchScreenButton を探す
        if (aTouchButton == null && aButton != null)
        {
            aTouchButton = aButton.GetComponent<TouchScreenButton>();
        }
    }

    /// <summary>
    /// Bボタン関連を子オブジェクトから自動で探す。
    /// 名前に "B" を含むオブジェクトを優先して取得する簡易実装。
    /// </summary>
    private void TryAutoAssignBButton()
    {
        // Button を探す
        if (bButton == null)
        {
            bButton = FindButtonInChildrenByNameContains("B");
            if (bButton == null)
            {
                // Aボタン取得時に使った GetComponentInChildren で
                // すでに A が取られている可能性があるので、
                // ここではあえて何もしない or 別の方法で探してもOK。
                // 必要であれば全 Button から "B" を含む名前を探すなども可。
                bButton = FindSecondButtonIfExists();
            }
        }

        // Image を探す
        if (bButtonImage == null && bButton != null)
        {
            bButtonImage = bButton.GetComponent<Image>();
        }

        // TouchScreenButton を探す
        if (bTouchButton == null && bButton != null)
        {
            bTouchButton = bButton.GetComponent<TouchScreenButton>();
        }
    }

    /// <summary>
    /// 子階層から、名前に特定の文字列を含む Button を探す。
    /// </summary>
    private Button FindButtonInChildrenByNameContains(string keyword)
    {
        Button[] buttons = GetComponentsInChildren<Button>(true);
        foreach (var btn in buttons)
        {
            if (btn.name.Contains(keyword))
            {
                return btn;
            }
        }
        return null;
    }

    /// <summary>
    /// 2つ目の Button を取得する簡易メソッド。
    /// A/B の自動判定がうまくいかなかった場合のフォールバック。
    /// </summary>
    private Button FindSecondButtonIfExists()
    {
        Button[] buttons = GetComponentsInChildren<Button>(true);
        if (buttons.Length >= 2)
        {
            return buttons[1];
        }
        return null;
    }

    /// <summary>
    /// A/B ボタン全体の有効/無効を切り替える。
    /// 物理的な GameObject の Active を切り替える方式。
    /// </summary>
    public void SetButtonsActive(bool isActive)
    {
        if (aButton != null)
        {
            aButton.gameObject.SetActive(isActive);
        }
        if (bButton != null)
        {
            bButton.gameObject.SetActive(isActive);
        }
    }

    /// <summary>
    /// A ボタンだけ有効/無効を切り替える。
    /// </summary>
    public void SetAButtonActive(bool isActive)
    {
        if (aButton != null)
        {
            aButton.gameObject.SetActive(isActive);
        }
    }

    /// <summary>
    /// B ボタンだけ有効/無効を切り替える。
    /// </summary>
    public void SetBButtonActive(bool isActive)
    {
        if (bButton != null)
        {
            bButton.gameObject.SetActive(isActive);
        }
    }

    /// <summary>
    /// A ボタンの色を変更する(ハイライトやクールダウン表示などに)。
    /// </summary>
    public void SetAButtonColor(Color color)
    {
        if (aButtonImage != null)
        {
            aButtonImage.color = color;
        }
    }

    /// <summary>
    /// B ボタンの色を変更する。
    /// </summary>
    public void SetBButtonColor(Color color)
    {
        if (bButtonImage != null)
        {
            bButtonImage.color = color;
        }
    }

    /// <summary>
    /// ボタンのインタラクティブ状態をまとめて切り替える。
    /// 画面上には表示したまま、押せなくする用途。
    /// </summary>
    public void SetInteractable(bool isInteractable)
    {
        if (aButton != null)
        {
            aButton.interactable = isInteractable;
        }
        if (bButton != null)
        {
            bButton.interactable = isInteractable;
        }
    }

    /// <summary>
    /// A/Bボタンに紐づく TouchScreenButton の ControlPath を
    /// ランタイムで変更したい場合に使う。
    /// 例: 設定画面でボタン配置をカスタマイズするなど。
    /// </summary>
    public void SetControlPathForA(string controlPath)
    {
        if (aTouchButton != null)
        {
            aTouchButton.controlPath = controlPath;
        }
    }

    public void SetControlPathForB(string controlPath)
    {
        if (bTouchButton != null)
        {
            bTouchButton.controlPath = controlPath;
        }
    }

#if UNITY_EDITOR
    // エディタ上でのデバッグ用:インスペクタから押せるボタンを追加してもOK。
    [ContextMenu("Enable Buttons")]
    private void ContextEnableButtons()
    {
        SetButtonsActive(true);
    }

    [ContextMenu("Disable Buttons")]
    private void ContextDisableButtons()
    {
        SetButtonsActive(false);
    }
#endif
}

この VirtualButtons コンポーネントは、あくまで「UIボタンと TouchScreenButton をまとめて扱う役」です。実際の「ジャンプ」「攻撃」などの処理は、別のコンポーネント(例:PlayerActions)で InputAction を受け取って実装するように分離しておくと、きれいな設計になります。

使い方の手順

ここからは、具体的なセットアップ手順を説明します。例として「プレイヤーのジャンプを A ボタン」「攻撃を B ボタン」に割り当てるケースを想定します。

手順① Input System の準備

  1. Package Manager から Input System をインストールします(Unity6 では標準で入っていることが多いです)。
  2. Edit > Project Settings > Player > Other Settings で「Active Input Handling」を Input System Package (New) または両方に設定しておきます。
  3. Project ビューで右クリック → Create > Input Actions を選び、PlayerInputActions などのアセットを作成します。
  4. そのアセットをダブルクリックして、以下のようなアクションを作ります:
    • Action Map: Player
    • Action: Jump(Type: Button)
    • Action: Attack(Type: Button)

ここでは「ゲームパッドの A ボタン / B ボタン相当」として、例えば:

  • Jump の Binding: <Gamepad>/buttonSouth
  • Attack の Binding: <Gamepad>/buttonEast

などを設定しておきます(PC用のキーボード、ゲームパッド入力と共存させる場合にも便利です)。

手順② UI キャンバスとボタンの作成

  1. シーンに Canvas を作成します(UI > Canvas)。
  2. Canvas の子として Button - TextMeshPro などのボタンを 2 つ作成し、それぞれ
    • AButton
    • BButton

    と名前を付けます。

  3. 各ボタンの RectTransform を調整して、
    • AButton: 画面左下(Anchor: Bottom Left)
    • BButton: 画面右下(Anchor: Bottom Right)

    に配置します。

  4. ボタンの子にある Text (または TMP Text) の表示をそれぞれ「A」「B」にしておくと分かりやすいです。

手順③ TouchScreenButton と VirtualButtons の設定

  1. AButton オブジェクトを選択し、Add Component から TouchScreenButton を追加します。
  2. 同様に BButton にも TouchScreenButton を追加します。
  3. それぞれの TouchScreenButton の
    • Control Path に、対応する InputAction のパスを設定します。例:
      • AButton: <Gamepad>/buttonSouth(Jump 用)
      • BButton: <Gamepad>/buttonEast(Attack 用)

    ここは Input Actions ウィンドウの Binding 設定と揃えるイメージです。

  4. Canvas 直下に空の GameObject を作り、名前を VirtualButtonsRoot などにします。
  5. このオブジェクトに、先ほどの VirtualButtons スクリプトをアタッチします。
  6. インスペクタで以下の参照を設定します:
    • A Button には AButton をドラッグ&ドロップ
    • A Button Image には AButton の Image コンポーネント
    • A Touch Button には AButton の TouchScreenButton
    • B Button も同様に設定
    • Target Canvas にはこの Canvas を指定(空でも自動取得を試みます)

これで、スマホで画面をタップすると、TouchScreenButton 経由で <Gamepad>/buttonSouth / buttonEast が「押されたこと」になり、Input System のアクションが発火します。

手順④ プレイヤー側で InputAction を受け取る

最後に、プレイヤーキャラクターに「Jump」「Attack」を処理するスクリプトを付けます。ここも責務を分けるために、PlayerInput とアクション処理を分けるときれいですが、シンプルな例として以下のようなコンポーネントを用意します。


using UnityEngine;
using UnityEngine.InputSystem;

/// <summary>
/// PlayerInput コンポーネントから Jump / Attack アクションを受け取って処理するサンプル。
/// 実際の移動ロジックなどは別コンポーネントに分けるとより良い設計になります。
/// </summary>
[RequireComponent(typeof(PlayerInput))]
public class PlayerActionsSample : MonoBehaviour
{
    [SerializeField] private float jumpForce = 5f;

    private PlayerInput playerInput;
    private Rigidbody rb;

    private void Awake()
    {
        playerInput = GetComponent<PlayerInput>();
        rb = GetComponent<Rigidbody>();

        // "Player" アクションマップを有効化
        playerInput.SwitchCurrentActionMap("Player");

        // アクションのコールバック登録
        var actions = playerInput.actions;
        actions["Jump"].performed += OnJumpPerformed;
        actions["Attack"].performed += OnAttackPerformed;
    }

    private void OnDestroy()
    {
        if (playerInput == null) return;

        var actions = playerInput.actions;
        actions["Jump"].performed -= OnJumpPerformed;
        actions["Attack"].performed -= OnAttackPerformed;
    }

    private void OnJumpPerformed(InputAction.CallbackContext context)
    {
        if (rb == null) return;

        // 単純な上方向ジャンプ(重力オン前提)
        rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
    }

    private void OnAttackPerformed(InputAction.CallbackContext context)
    {
        // ここに攻撃アニメーション再生や弾の発射などを書く
        Debug.Log("Attack performed!");
    }
}
  1. プレイヤーの GameObject に RigidbodyPlayerInput を追加します。
  2. PlayerInput の Actions に、先ほど作った PlayerInputActions アセットを指定し、Default MapPlayer に設定します。
  3. 同じ GameObject に PlayerActionsSample をアタッチします。

これで、PC ではキーボード/ゲームパッド、スマホでは画面上の A/B ボタンから同じアクションを呼び出せるようになります。

メリットと応用

VirtualButtons コンポーネントを用意しておくと、

  • 「モバイル用の A/B ボタン UI」を 1 プレハブにまとめられる
  • どのシーンでも同じプレハブをポンと置くだけで、A/B 入力が有効になる
  • UI の見た目変更(色、サイズ、位置)はプレハブの編集だけで完結
  • ゲームロジック側は「Jump アクションが来たかどうか」だけを見ればよい

といったメリットがあります。レベルデザイン時には、

  • アクションゲームのシーンには VirtualButtons プレハブを有効化
  • タイトル画面や設定画面では VirtualButtons を無効化(SetButtonsActive(false)

のように、シーンごとに必要な入力だけを出し分けできるので、管理がかなり楽になります。

また、VirtualButtons 自体は「A/B の UI と TouchScreenButton をまとめる」だけの役割なので、

  • 別コンポーネントで「チュートリアル時はボタンを点滅させる」
  • 別コンポーネントで「スタミナが切れたら A ボタンをグレーアウト」

といった拡張も簡単です。

改造案:ボタンを点滅させる簡易ハイライト

例えば、「チュートリアル中は A ボタンを点滅させてプレイヤーに押させたい」という場合、以下のような小さなコンポーネントを追加で付けるだけで対応できます。


using UnityEngine;

/// <summary>
/// VirtualButtons と組み合わせて、Aボタンを点滅させるサンプル。
/// チュートリアル中だけ有効にする、などの用途を想定。
/// </summary>
public class VirtualButtonBlinker : MonoBehaviour
{
    [SerializeField] private VirtualButtons virtualButtons;
    [SerializeField] private Color blinkColor = Color.yellow;
    [SerializeField] private float blinkSpeed = 4f;

    private Color originalColor;
    private bool hasOriginalColor;

    private void Update()
    {
        if (virtualButtons == null) return;

        // 初回だけ元の色を取得
        if (!hasOriginalColor)
        {
            originalColor = Color.white;
            hasOriginalColor = true;
        }

        // sin波で 0-1 を往復させる
        float t = (Mathf.Sin(Time.time * blinkSpeed) + 1f) * 0.5f;
        Color current = Color.Lerp(originalColor, blinkColor, t);

        virtualButtons.SetAButtonColor(current);
    }
}

このように、小さなコンポーネントを積み重ねていくと、巨大な God スクリプトに頼らずに、

  • 「入力を受け取るコンポーネント」
  • 「UI を制御するコンポーネント」
  • 「演出(点滅、拡大縮小など)を担当するコンポーネント」

を分離したまま、柔軟なモバイル UI を構築できます。
ぜひ自分のプロジェクト用にカスタマイズしながら、コンポーネント指向の設計に慣れていきましょう。