Unityを触り始めた頃は、つい Update() の中に「移動処理」「入力処理」「アニメーション」「デバッグ表示」など、すべてを1つのスクリプトに詰め込みがちですよね。とりあえず動くので気持ちはわかりますが、時間が経つと次のような問題が出てきます。

  • どの変数がどこで変更されているのか追いづらい
  • デバッグ表示のコードが本来のゲームロジックを読みにくくする
  • オブジェクトごとに「ちょっとだけ違う表示」をしたくなったときにコピペ地獄になる

そこで、デバッグ用の処理はコンポーネントとして切り出してしまいましょう。
今回は「親オブジェクトの状態(VelocityやStateなど)を常に頭上に表示する」ための DebugLabel コンポーネントを作ります。

ゲームロジック側は「速度や状態を普通に更新するだけ」、デバッグ表示は「DebugLabelが勝手に読んでくれる」という分離をすると、コードがかなりスッキリしますよ。

【Unity】頭上に状態を常時表示!「DebugLabel」コンポーネント

このコンポーネントは、次のような用途を想定しています。

  • プレイヤーの現在速度・ステート(Idle / Run / Jumpなど)を頭上に表示
  • 敵AIの「現在ステート」「ターゲット座標」「HP」などをデバッグ表示
  • 動く床やギミックの「現在のフェーズ」「移動速度」などを可視化

「何を表示するか」は インターフェース で定義し、各オブジェクト側が好きな情報を提供する設計にします。
DebugLabel は「文字列をもらって表示するだけ」の役割に限定し、シングル・レスポンシビリティを守る形ですね。


フルコード:DebugLabel & 情報提供インターフェース

以下の2つのスクリプトを用意します。

  1. IDebugLabelDataProvider … 「ラベルに表示するテキストを提供する」インターフェース
  2. DebugLabel … 「親から文字列をもらって、頭上に表示する」コンポーネント

どちらも同じアセンブリ(例: Scripts フォルダ)に置いてください。


// IDebugLabelDataProvider.cs
using UnityEngine;

/// <summary>DebugLabel に表示するテキストを提供するインターフェース</summary>
public interface IDebugLabelDataProvider
{
    /// <summary>
    /// ラベルに表示したいテキストを返す
    /// 複数行を表示したい場合は '\n' を含めて返す
    /// </summary>
    string GetDebugLabelText();
}

// DebugLabel.cs
using UnityEngine;
using UnityEngine.UI;

[DisallowMultipleComponent]
public class DebugLabel : MonoBehaviour
{
    [Header("表示対象")]
    [Tooltip("情報を提供してくれるコンポーネント(未指定なら親から自動取得を試みます)")]
    [SerializeField] private MonoBehaviour dataProviderBehaviour;

    [Header("見た目設定")]
    [Tooltip("ラベルのローカルオフセット(親の頭上に配置するなど)")]
    [SerializeField] private Vector3 labelLocalOffset = new Vector3(0f, 2f, 0f);

    [Tooltip("テキストの色")]
    [SerializeField] private Color textColor = Color.white;

    [Tooltip("テキストのフォントサイズ")]
    [SerializeField] private int fontSize = 18;

    [Tooltip("背景の色(アルファを下げると半透明になります)")]
    [SerializeField] private Color backgroundColor = new Color(0f, 0f, 0f, 0.5f);

    [Tooltip("ワールド空間に配置するかどうか(true 推奨)")]
    [SerializeField] private bool useWorldSpaceCanvas = true;

    [Tooltip("カメラに常に正面を向ける(ビルボード)")]
    [SerializeField] private bool faceToCamera = true;

    // 内部参照
    private IDebugLabelDataProvider dataProvider;
    private Canvas canvas;
    private Text text;
    private Camera mainCamera;

    private void Awake()
    {
        mainCamera = Camera.main;

        // dataProviderBehaviour が設定されていればそこからインターフェースを取得
        if (dataProviderBehaviour != null)
        {
            dataProvider = dataProviderBehaviour as IDebugLabelDataProvider;
            if (dataProvider == null)
            {
                Debug.LogWarning(
                    $"[DebugLabel] dataProviderBehaviour に IDebugLabelDataProvider を実装したコンポーネントを指定してください。({name})");
            }
        }
        else
        {
            // 未指定なら、親オブジェクトから自動で探す
            dataProvider = GetComponentInParent<IDebugLabelDataProvider>();
            if (dataProvider == null)
            {
                Debug.LogWarning(
                    $"[DebugLabel] 親から IDebugLabelDataProvider を見つけられませんでした。({name})");
            }
        }

        SetupCanvasAndText();
    }

    /// <summary>
    /// Canvas と Text コンポーネントを生成・初期化する
    /// </summary>
    private void SetupCanvasAndText()
    {
        // 子オブジェクトとして Canvas を作成
        GameObject canvasObj = new GameObject("DebugLabelCanvas");
        canvasObj.transform.SetParent(transform, false);
        canvasObj.transform.localPosition = labelLocalOffset;

        canvas = canvasObj.AddComponent<Canvas>();
        canvas.renderMode = useWorldSpaceCanvas ? RenderMode.WorldSpace : RenderMode.ScreenSpaceOverlay;

        // WorldSpace の場合はカメラを設定(ない場合も動くが、スケールなどの扱いが変わる)
        if (useWorldSpaceCanvas)
        {
            canvas.worldCamera = mainCamera;
            // ワールド空間キャンバスはスケールが大きくなりがちなので調整
            canvasObj.transform.localScale = Vector3.one * 0.01f;
        }

        // 文字を描画するために CanvasScaler と GraphicRaycaster は任意(今回は簡略化のため省略してもOK)

        // 背景用の Image を追加
        GameObject bgObj = new GameObject("Background");
        bgObj.transform.SetParent(canvasObj.transform, false);

        Image bgImage = bgObj.AddComponent<Image>();
        bgImage.color = backgroundColor;

        RectTransform bgRect = bgObj.GetComponent<RectTransform>();
        bgRect.anchorMin = new Vector2(0f, 0f);
        bgRect.anchorMax = new Vector2(1f, 1f);
        bgRect.offsetMin = new Vector2(-8f, -4f);   // 余白
        bgRect.offsetMax = new Vector2(8f, 4f);

        // Text コンポーネントを追加
        GameObject textObj = new GameObject("LabelText");
        textObj.transform.SetParent(bgObj.transform, false);

        text = textObj.AddComponent<Text>();
        text.color = textColor;
        text.fontSize = fontSize;
        text.alignment = TextAnchor.MiddleCenter;

        // デフォルトフォントを設定(環境によっては null になるので保険)
        if (text.font == null)
        {
            text.font = Resources.GetBuiltinResource<Font>("Arial.ttf");
        }

        RectTransform textRect = text.GetComponent<RectTransform>();
        textRect.anchorMin = new Vector2(0f, 0f);
        textRect.anchorMax = new Vector2(1f, 1f);
        textRect.offsetMin = new Vector2(4f, 2f);
        textRect.offsetMax = new Vector2(-4f, -2f);
    }

    private void LateUpdate()
    {
        // 情報提供元がなければ何もしない
        if (dataProvider == null || text == null)
        {
            return;
        }

        // テキスト更新
        text.text = dataProvider.GetDebugLabelText();

        // カメラに正面を向ける(ビルボード)
        if (faceToCamera && mainCamera != null && canvas != null && canvas.renderMode == RenderMode.WorldSpace)
        {
            // カメラの方向を向くように回転
            Vector3 cameraForward = mainCamera.transform.forward;
            canvas.transform.rotation = Quaternion.LookRotation(cameraForward, Vector3.up);
        }
    }

    /// <summary>
    /// 実行中でもデバッグ中にプロパティ変更を反映させたい場合は、OnValidate で補助する
    /// </summary>
    private void OnValidate()
    {
        // エディタ上で色やフォントサイズを変えたときに即時反映
        if (text != null)
        {
            text.color = textColor;
            text.fontSize = fontSize;
        }
    }
}

例:プレイヤーの Velocity と State を提供するコンポーネント

次に、IDebugLabelDataProvider を実装した「プレイヤー用の情報提供コンポーネント」の例です。
Rigidbody の速度と、簡単なステートを表示してみます。


// PlayerDebugInfo.cs
using UnityEngine;

/// <summary>プレイヤーの速度やステートを DebugLabel に提供する例</summary>
[RequireComponent(typeof(Rigidbody))]
public class PlayerDebugInfo : MonoBehaviour, IDebugLabelDataProvider
{
    [Header("疑似ステート(実際のプロジェクトでは別クラスでもOK)")]
    [SerializeField] private string currentState = "Idle";

    [Header("速度のしきい値")]
    [SerializeField] private float runningSpeedThreshold = 2.0f;

    private Rigidbody rb;

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

    private void Update()
    {
        // ここではデモ用に、速度から簡易的なステートを決めています
        float horizontalSpeed = new Vector3(rb.velocity.x, 0f, rb.velocity.z).magnitude;

        if (horizontalSpeed < 0.1f)
        {
            currentState = "Idle";
        }
        else if (horizontalSpeed < runningSpeedThreshold)
        {
            currentState = "Walk";
        }
        else
        {
            currentState = "Run";
        }
    }

    /// <summary>
    /// DebugLabel に表示するテキストを返す
    /// </summary>
    public string GetDebugLabelText()
    {
        Vector3 v = rb.velocity;
        return
            $"State : {currentState}\n" +
            $"Velocity : ({v.x:F2}, {v.y:F2}, {v.z:F2})\n" +
            $"Speed : {v.magnitude:F2}";
    }
}

このように、GetDebugLabelText() の中で好きな情報を組み立てて返せば、DebugLabel が自動で頭上に表示してくれます。


使い方の手順

  1. スクリプトをプロジェクトに追加する
    • IDebugLabelDataProvider.cs
    • DebugLabel.cs
    • PlayerDebugInfo.cs(例。自分のプロジェクト用に別クラスを作ってもOK)
  2. プレイヤーに情報提供コンポーネントをアタッチ
    • プレイヤーの GameObject を選択(例: Player
    • Rigidbody が付いていなければ追加
    • PlayerDebugInfo を追加
    • 必要なら runningSpeedThreshold などを調整
  3. DebugLabel を配置する
    例として、プレイヤーの頭上にラベルを出したい場合:
    • プレイヤーの子オブジェクトとして空の GameObject を作成(名前例: PlayerDebugLabel
    • その子オブジェクトに DebugLabel を追加
    • labelLocalOffset(0, 2, 0) など、頭上に来るように調整
    • dataProviderBehaviour は空のままでOK(親の PlayerDebugInfo を自動で探します)

    敵キャラや動く床などでも同じ手順で、各オブジェクトに「DebugLabel の子」をぶら下げるだけで再利用できます。

  4. 再生して動作確認
    プレイモードでキャラクターを動かしてみると、頭上に
    • State : Idle / Walk / Run
    • Velocity : (x, y, z)
    • Speed : 数値

    といった情報がリアルタイムに表示されます。
    敵AI用に別のクラス(例: EnemyDebugInfo)を作り、IDebugLabelDataProvider を実装すれば、
    同じ DebugLabel コンポーネントでまったく別の情報を表示できます。


メリットと応用

この DebugLabel コンポーネントを導入すると、以下のようなメリットがあります。

  • ゲームロジックからデバッグ表示を分離できる
    プレイヤー制御スクリプトに OnGUIDebug.Log を書き散らかさずに済みます。
    表示用のコードは GetDebugLabelText() に閉じ込められるので、読みやすさが段違いです。
  • プレハブ単位でデバッグ設定を完結できる
    プレイヤー・敵・ギミックごとに、プレハブ内で「DebugLabel の有無」や「表示内容」を完結できます。
    シーン側で特別な設定をしなくても、プレハブをポンと置くだけで同じデバッグ表示が使えるようになります。
  • レベルデザイン時の可視化が簡単
    たとえば「動く床の速度」「次のウェイポイントID」「敵のターゲット名」などをラベルに出せば、
    シーンビューでパラメータをいじりながら挙動を確認しやすくなります。

応用としては、次のような拡張も簡単です。

  • ラベルの表示・非表示をキーボードショートカットで切り替える
  • 特定のビルド(デバッグビルド時のみ)で自動的に有効にする
  • ラベルの色を「HP やステート」に応じて変える

例えば、「HPが減ったらラベルの色を赤くする」ような改造は、こんな感じで実装できます。


// 例:HP に応じてラベルの色を変える(DebugLabel に追加する改造案)
public void SetTextColorByHp(float currentHp, float maxHp)
{
    if (text == null) return;

    float ratio = maxHp > 0f ? currentHp / maxHp : 0f;

    // HP が少ないほど赤く、多いほど緑に寄せる
    Color low = Color.red;
    Color high = Color.green;
    Color c = Color.Lerp(low, high, ratio);

    text.color = c;
}

この関数を呼ぶ側(例えば EnemyDebugInfo)から、毎フレーム HP を渡して色を更新すれば、
「瀕死の敵は頭上ラベルが赤くなる」といった視覚的なデバッグも簡単に実現できます。

デバッグ表示をコンポーネント化しておくと、プロジェクトが大きくなっても「見たい情報をすぐ可視化できる」状態を保ちやすくなります。
Godクラス化したスクリプトを少しずつ分割して、こういった小さなコンポーネントに逃がしていくと、後々かなり楽になりますよ。