Unityを触り始めた頃、「とりあえず全部 Update に書いてしまう」コードになりがちですよね。
キーボード入力の判定も、UIの表示切り替えも、チュートリアルの進行管理も、全部ひとつのスクリプトに押し込んでしまうと…

  • どの入力がどの UI を動かしているのか分かりづらい
  • チュートリアルだけ別のシーンで使いたいのに、巨大なスクリプトごと持ってこないといけない
  • 少しだけ仕様変更したいのに、影響範囲が読めなくて怖い

こういうときこそ、「入力を画面に見せるだけ」の小さなコンポーネントに分けておくと、とても扱いやすくなります。
この記事では、押されたボタンに対応するキーアイコンを UI 上に表示するためのコンポーネント 「InputVisualizer」 を作っていきます。

【Unity】押したキーをそのままUIに!「InputVisualizer」コンポーネント

InputVisualizer は、指定したキーが押されたときに、対応するアイコンを UI の親オブジェクト(例: チュートリアル用のパネル)内に表示するためのコンポーネントです。
入力の監視と UI の生成だけを担当させることで、「入力の可視化」という責務にきれいに分離できます。

前提として、以下のようなシンプルな構成を想定しています。

  • Unity UI(Canvas / Image など)
  • キーごとのアイコンプレハブ(例: Aキーの画像、Spaceキーの画像)
  • InputVisualizer がそれらを監視して表示

フルコード:InputVisualizer.cs


using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;

/// <summary>
/// 押されたキーに応じて、対応するアイコンプレハブを親の中に生成して表示するコンポーネント。
/// - チュートリアル用の「今押しているキーを見せたい」用途を想定。
/// - 責務は「入力を監視し、UIアイコンを生成・表示する」ことだけに限定。
/// </summary>
public class InputVisualizer : MonoBehaviour
{
    [Serializable]
    private class KeyIconEntry
    {
        [Tooltip("監視するキー。例: Space, A, LeftArrow など")]
        public KeyCode keyCode = KeyCode.None;

        [Tooltip("このキーが押されたときに表示するアイコンプレハブ (Image や任意の UI プレハブ)")]
        public GameObject iconPrefab;
    }

    [Header("表示先の親オブジェクト (通常は RectTransform)")]
    [SerializeField]
    private Transform iconParent;

    [Header("監視するキーとアイコンの対応表")]
    [SerializeField]
    private List<KeyIconEntry> keyIconEntries = new List<KeyIconEntry>();

    [Header("同じキーのアイコンを複数表示するか")]
    [SerializeField]
    [Tooltip("OFF の場合、同じキーのアイコンは 1 つだけに制限されます")]
    private bool allowDuplicateIcons = false;

    [Header("アイコンの自動削除設定")]
    [SerializeField]
    [Tooltip("0 以下の場合は自動削除しません")]
    private float iconLifetimeSeconds = 1.0f;

    [Header("押している間だけ表示するモード")]
    [SerializeField]
    [Tooltip("true の場合、キーを離したタイミングでアイコンを削除します")]
    private bool holdToShow = false;

    // 内部用: KeyCode ごとに現在表示中のアイコンを管理
    private readonly Dictionary<KeyCode, List<GameObject>> _spawnedIcons
        = new Dictionary<KeyCode, List<GameObject>>();

    // KeyCode -> IconPrefab の高速参照用辞書
    private readonly Dictionary<KeyCode, GameObject> _keyToPrefab
        = new Dictionary<KeyCode, GameObject>();

    private void Awake()
    {
        // iconParent が未設定の場合、デフォルトで自分自身を親にする
        if (iconParent == null)
        {
            iconParent = transform;
        }

        // 設定されたリストから辞書を構築
        _keyToPrefab.Clear();
        foreach (var entry in keyIconEntries)
        {
            if (entry == null) continue;
            if (entry.keyCode == KeyCode.None) continue;
            if (entry.iconPrefab == null) continue;

            // 同じ KeyCode が複数設定されていた場合は最初のものを優先
            if (_keyToPrefab.ContainsKey(entry.keyCode)) continue;

            _keyToPrefab.Add(entry.keyCode, entry.iconPrefab);
        }
    }

    private void Update()
    {
        // 監視対象のキーをループ
        foreach (var kvp in _keyToPrefab)
        {
            var key = kvp.Key;
            var prefab = kvp.Value;

            // 押された瞬間を検知
            if (Input.GetKeyDown(key))
            {
                HandleKeyDown(key, prefab);
            }

            // holdToShow モードの場合は、キーを離した瞬間も監視
            if (holdToShow && Input.GetKeyUp(key))
            {
                HandleKeyUp(key);
            }
        }
    }

    /// <summary>
    /// キーが押された瞬間の処理。
    /// </summary>
    private void HandleKeyDown(KeyCode key, GameObject prefab)
    {
        if (!allowDuplicateIcons)
        {
            // 既にこのキーのアイコンが存在している場合は何もしない
            if (_spawnedIcons.TryGetValue(key, out var list) && list.Count > 0)
            {
                return;
            }
        }

        // アイコンを生成
        var icon = Instantiate(prefab, iconParent);

        // UI の場合、位置やスケールをリセットしておくと扱いやすい
        var rect = icon.transform as RectTransform;
        if (rect != null)
        {
            rect.anchoredPosition = Vector2.zero;
            rect.localScale = Vector3.one;
        }

        // 管理リストに追加
        if (!_spawnedIcons.ContainsKey(key))
        {
            _spawnedIcons[key] = new List<GameObject>();
        }
        _spawnedIcons[key].Add(icon);

        // 自動削除モード (holdToShow=false かつ iconLifetimeSeconds>0)
        if (!holdToShow && iconLifetimeSeconds > 0f)
        {
            StartCoroutine(DestroyAfterDelayCoroutine(key, icon, iconLifetimeSeconds));
        }
    }

    /// <summary>
    /// holdToShow モードでキーを離したときの処理。
    /// </summary>
    private void HandleKeyUp(KeyCode key)
    {
        if (!_spawnedIcons.TryGetValue(key, out var list)) return;

        // 現在のアイコンをすべて削除
        for (int i = 0; i < list.Count; i++)
        {
            var icon = list[i];
            if (icon != null)
            {
                Destroy(icon);
            }
        }
        list.Clear();
    }

    /// <summary>
    /// 一定時間後にアイコンを破棄するコルーチン。
    /// </summary>
    private System.Collections.IEnumerator DestroyAfterDelayCoroutine(
        KeyCode key,
        GameObject icon,
        float delaySeconds)
    {
        yield return new WaitForSeconds(delaySeconds);

        // 実際に削除する前に、まだリストに存在しているか確認
        if (_spawnedIcons.TryGetValue(key, out var list))
        {
            list.Remove(icon);
        }

        if (icon != null)
        {
            Destroy(icon);
        }
    }

    /// <summary>
    /// 外部からアイコンをすべて消したいときに呼ぶユーティリティ。
    /// 例: チュートリアルのステップが変わったときなど。
    /// </summary>
    public void ClearAllIcons()
    {
        foreach (var kvp in _spawnedIcons)
        {
            var list = kvp.Value;
            for (int i = 0; i < list.Count; i++)
            {
                var icon = list[i];
                if (icon != null)
                {
                    Destroy(icon);
                }
            }
        }

        _spawnedIcons.Clear();
    }
}

使い方の手順

ここでは、チュートリアル用の「プレイヤーに W/A/S/D を教えるパネル」を例に、InputVisualizer の使い方を手順で見ていきます。

  1. キーアイコンのプレハブを用意する
    • Canvas 配下に Image を作成し、「W」キーのテクスチャを貼る
    • 同様に「A」「S」「D」「Space」など、表示したいキー分だけ作る
    • それぞれを Prefab 化しておく(例: Key_W.prefab, Key_Space.prefab
  2. 表示用の親オブジェクトを作る
    • Canvas の下に「TutorialKeyPanel」などの空の UI オブジェクトを作る
    • HorizontalLayoutGroup や GridLayoutGroup を付けておくと、アイコンがきれいに並びます
  3. TutorialKeyPanel に InputVisualizer をアタッチ
    • TutorialKeyPanel を選択し、「Add Component」から InputVisualizer を追加
    • Icon Parent は空欄でも OK(自分自身が親として使われます)
    • Key Icon Entries に要素を追加し、以下のように設定:
      • Element 0: KeyCode = W, IconPrefab = Key_W.prefab
      • Element 1: KeyCode = A, IconPrefab = Key_A.prefab
      • Element 2: KeyCode = S, IconPrefab = Key_S.prefab
      • Element 3: KeyCode = D, IconPrefab = Key_D.prefab
      • Element 4: KeyCode = Space, IconPrefab = Key_Space.prefab
    • Allow Duplicate Icons:
      • 同じキーを連打した回数だけ並べたい → ON
      • 「押したことがあるか」を示すだけでよい → OFF
    • Hold To Show:
      • 押している間だけアイコンを表示したい → ON
      • 押した瞬間だけ一瞬光らせたい(+自動削除) → OFF
    • Icon Lifetime Seconds:
      • 0 より大きい値にすると、その秒数後に自動でアイコンを削除(Hold To Show = false のときのみ有効)
      • 0 以下にすると自動削除なし(自分で ClearAllIcons() を呼ぶ運用)
  4. シーンを再生して動作確認
    • Play を押して、W/A/S/D/Space を押してみましょう
    • 設定したキーに応じて、TutorialKeyPanel の中にアイコンが生成されます
    • チュートリアルシーンであれば、「このキーを押してみてください」とテキストを添えるだけで、視覚的に分かりやすい UI になります

同じコンポーネントを、敵のチュートリアルや動く床の操作説明などにもそのまま使い回せる点がポイントです。
それぞれのチュートリアルパネルに InputVisualizer をアタッチし、必要なキーとアイコンプレハブを登録するだけで完結します。

メリットと応用

InputVisualizer を導入すると、以下のようなメリットがあります。

  • 入力の可視化ロジックを 1 か所に集約
    プレイヤーの移動ロジックや敵 AI とは完全に分離されるので、「入力を UI に見せる」処理だけを安全に改造できます。
  • プレハブ単位でチュートリアル UI を量産しやすい
    「ジャンプのチュートリアル」「ダッシュのチュートリアル」などを、それぞれプレハブ化したパネルとして用意しておけば、シーンにポンと置くだけで入力表示が機能します。
  • レベルデザインが楽になる
    ステージデザイナーは、「この場所でプレイヤーに Space を押させたい」と思ったら、対応するチュートリアルパネルプレハブをステージに配置するだけ。コードを触る必要がありません。
  • テストやデバッグにも使える
    入力が正しく拾えているかを目視で確認できるので、キーバインドの調整や Input System の検証時にも役立ちます。

さらに、InputVisualizer 自体は「入力 → アイコン生成」のみを担当しているので、他のコンポーネントと組み合わせて拡張しやすいです。
例えば、以下のような改造案が考えられます。

  • アイコンが生成されたときに、軽く拡大縮小するアニメーションを付ける
  • 一度押したキーは色を変えて「達成済み」にする
  • ゲームパッド用のボタンアイコンに差し替える

最後に、簡単な改造案として「生成されたアイコンをフェードアウトさせる」ための関数例を載せておきます。
(InputVisualizer と同じクラス、もしくは別のコンポーネントから呼び出す想定です)


/// <summary>
/// 指定した Image を、duration 秒かけてフェードアウトさせてから削除する例。
/// InputVisualizer から生成直後に呼び出すことで、自然な消え方を演出できます。
/// </summary>
private System.Collections.IEnumerator FadeOutAndDestroy(Image image, float duration)
{
    if (image == null) yield break;

    float time = 0f;
    Color startColor = image.color;

    while (time < duration)
    {
        time += Time.deltaTime;
        float t = Mathf.Clamp01(time / duration);

        // アルファ値だけを徐々に 0 に近づける
        Color c = startColor;
        c.a = Mathf.Lerp(startColor.a, 0f, t);
        image.color = c;

        yield return null;
    }

    if (image != null)
    {
        Destroy(image.gameObject);
    }
}

このように、小さな責務に分割されたコンポーネントを組み合わせていくと、プロジェクト全体の見通しがぐっと良くなります。
入力表示専用の InputVisualizer を土台に、チュートリアル UI をどんどん育てていきましょう。