Godotでプレイヤーや敵の挙動をデバッグするとき、print()スパムやデバッガのウォッチだけに頼っていると、だんだん「今このキャラが何をしているのか」が直感的に分かりづらくなってきますよね。
特に「状態(State)」や「速度(Velocity)」を頻繁にいじるアクションゲームやAIでは、画面上にそのまま出ていてほしいことが多いです。

ありがちなのは、各キャラシーンの中に Label を置いて、そのノードに直接スクリプトを書いてしまうパターンです:

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── Label ← Player専用のスクリプト直書き…

これを敵キャラや動く床にもコピペしていくと、「デバッグ用コードがあちこちに散らばる」「継承ツリーが汚れる」という、Godotあるある地獄にハマります。
そこで今回は「どのノードにもポン付けできるデバッグラベル」として、コンポーネント指向の DebugLabel コンポーネントを作ってみましょう。

【Godot 4】なんでも頭上ステータス表示!「DebugLabel」コンポーネント

DebugLabel は、親ノードの任意のプロパティ(例: velocity, state, hp など)を、自動で頭上に表示し続けるためのコンポーネントです。

  • プレイヤーの velocitystate
  • 敵AIの current_statetarget_position
  • 動く床の speedis_active

などを、一切継承をいじらずに、ただノードとしてアタッチするだけで可視化できます。

フルコード:DebugLabel.gd


extends Node2D
class_name DebugLabel
##
## DebugLabel コンポーネント
## 親ノードのプロパティを定期的に読み取り、頭上にラベル表示する。
## - 継承不要
## - どのシーンにも Node2D としてポン付け可能
##

@export var target_node_path: NodePath = NodePath("..")
## どのノードを監視するか。
## デフォルトは親ノード("..")。
## 別のノードを監視したい場合は、インスペクタから変更してください。

@export var properties: Array[StringName] = [ &"velocity", &"state" ]
## 表示したいプロパティ名の配列。
## 例:
##   ["velocity", "state"]
##   ["hp", "max_hp", "current_state"]
## プロパティが存在しない場合は "(missing)" と表示されます。

@export var refresh_interval: float = 0.1
## 何秒ごとに表示を更新するか。
## 0 以下にすると毎フレーム (_process) で更新します。

@export var offset: Vector2 = Vector2(0, -32)
## 監視対象ノードからの相対位置。
## 頭上に表示したい場合はマイナスY方向に設定します。

@export var follow_target_position: bool = true
## true の場合、監視対象ノードのグローバル位置に追従します。
## false の場合、この DebugLabel 自身の位置は固定されます。

@export var auto_hide_in_release: bool = true
## true の場合、エクスポートビルド(リリース)では自動的に非表示になります。
## 開発中だけ見たいデバッグ情報に便利です。

@export var font_size: int = 14
## ラベルのフォントサイズ。
## Theme を使わない簡易な調整用。

@export var text_color: Color = Color(1, 1, 0)
## ラベルの文字色。デフォルトは黄色。

@export var background_color: Color = Color(0, 0, 0, 0.6)
## ラベルの背景色。アルファ付きで半透明にすると見やすいです。

@export var show_node_name: bool = true
## 先頭に監視対象ノードの名前を表示するかどうか。

@export var custom_prefix: String = ""
## 任意のプレフィックス文字列。
## 例: "[PLAYER] ", "[ENEMY] " など。


var _target: Node = null
var _label: Label = null
var _panel: Panel = null
var _time_accum: float = 0.0


func _ready() -> void:
    # リリースビルドでは自動的に非表示にするオプション
    if auto_hide_in_release and OS.has_feature("release"):
        visible = false
        process_mode = Node.PROCESS_MODE_DISABLED
        return

    _resolve_target()
    _create_ui()

    # 初回更新
    _update_text(force_empty_if_no_target = false)

    # 更新間隔に応じて process を有効化
    set_process(true)


func _process(delta: float) -> void:
    if not is_instance_valid(_target):
        # ターゲットが消えたら再取得を試みる
        _resolve_target()

    if follow_target_position and is_instance_valid(_target):
        # 監視対象のグローバル位置 + オフセットに追従
        global_position = (_target as Node2D).global_position + offset if _target is Node2D else global_position

    if refresh_interval <= 0.0:
        # 毎フレーム更新
        _update_text()
    else:
        _time_accum += delta
        if _time_accum >= refresh_interval:
            _time_accum = 0.0
            _update_text()


func _resolve_target() -> void:
    ## target_node_path からターゲットノードを取得する。
    if target_node_path.is_empty():
        _target = get_parent()
        return

    var node := get_node_or_null(target_node_path)
    if node:
        _target = node
    else:
        _target = get_parent()


func _create_ui() -> void:
    ## ラベルと背景用の Panel を動的に生成する。
    ## DebugLabel 自身は Node2D なので、2D空間上の位置を自由に指定できます。
    # すでに作成済みならスキップ
    if _label and is_instance_valid(_label):
        return

    _panel = Panel.new()
    add_child(_panel)

    # Panel を UI 用に設定
    _panel.name = "DebugLabelPanel"
    _panel.z_index = 10000  # 手前に出したいときは大きめの値に
    _panel.mouse_filter = Control.MOUSE_FILTER_IGNORE

    # Panel の背景色を設定
    var style := StyleBoxFlat.new()
    style.bg_color = background_color
    _panel.add_theme_stylebox_override("panel", style)

    # Label を生成
    _label = Label.new()
    _label.name = "DebugLabelText"
    _label.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT
    _label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
    _label.autowrap_mode = TextServer.AUTOWRAP_OFF

    # フォントサイズと色を簡易設定
    var font := ThemeDB.get_default_font()
    var font_override := FontVariation.new()
    font_override.base_font = font
    font_override.size = font_size
    _label.add_theme_font_override("font", font_override)
    _label.add_theme_color_override("font_color", text_color)

    _panel.add_child(_label)

    # レイアウト調整
    _panel.size = Vector2(200, font_size + 6)
    _label.position = Vector2(4, 3)


func _update_text(force_empty_if_no_target: bool = true) -> void:
    ## ターゲットのプロパティを読み取り、ラベルのテキストを更新する。
    if not _label or not is_instance_valid(_label):
        return

    if not is_instance_valid(_target):
        if force_empty_if_no_target:
            _label.text = ""
        else:
            _label.text = "[DebugLabel] No target"
        return

    var lines: Array[String] = []

    # 先頭にノード名やプレフィックスを付ける
    var header := ""
    if custom_prefix != "":
        header += custom_prefix
    if show_node_name:
        header += str(_target.name)
    if header != "":
        lines.append(header)

    # プロパティ一覧を表示
    for prop_name in properties:
        var line := ""
        line += str(prop_name) + ": "

        if _target.has_method("get") and _target.has_property(str(prop_name)):
            # 通常のプロパティ
            var value = _target.get(str(prop_name))
            line += _stringify_value(value)
        else:
            # プロパティが存在しない場合
            # ただし「メソッドを呼んで値を返す」ような使い方も一応サポート
            if _target.has_method(str(prop_name)):
                var method_result = _target.call(str(prop_name))
                line += _stringify_value(method_result)
            else:
                line += "(missing)"

        lines.append(line)

    _label.text = "\n".join(lines)

    # テキスト長に応じて Panel のサイズを調整
    await get_tree().process_frame  # レイアウト更新を待つ
    var text_size: Vector2 = _label.get_minimum_size()
    _panel.size = text_size + Vector2(8, 6)


func _stringify_value(value: Variant) -> String:
    ## Vector2 や Dictionary など、よく使う型をそれなりに読みやすく文字列化する。
    match typeof(value):
        TYPE_VECTOR2:
            var v := value as Vector2
            return "({0:.2f}, {1:.2f})".format([v.x, v.y])
        TYPE_VECTOR3:
            var v3 := value as Vector3
            return "({0:.2f}, {1:.2f}, {2:.2f})".format([v3.x, v3.y, v3.z])
        TYPE_FLOAT:
            return "{0:.3f}".format([value])
        TYPE_DICTIONARY, TYPE_ARRAY:
            return JSON.stringify(value)
        _:
            return str(value)

使い方の手順

ここからは、実際にプレイヤーや敵、動く床などに DebugLabel を付ける具体的な手順を見ていきましょう。

① コンポーネントスクリプトを用意する

  1. 上記の DebugLabel.gd をプロジェクト内に保存します。
    例: res://addons/debug_tools/DebugLabel.gd
  2. Godot エディタを再読み込みすると、ノード追加ダイアログDebugLabel が検索できるようになります(class_name DebugLabel のおかげ)。

② プレイヤーに頭上デバッグラベルを付ける例

まずは典型的な 2D プレイヤーキャラを例にします。

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── DebugLabel (Node2D)
  1. Player シーンを開く。
  2. ルートの Player (CharacterBody2D) を選択し、「子ノードを追加」から DebugLabel を追加。
  3. DebugLabel のインスペクタで、以下のように設定:
    • target_node_path: デフォルトのまま ..(親=Player)
    • properties: ["velocity", "state"]
    • offset: (0, -40) など、頭上に来るように調整
    • custom_prefix: "[PLAYER] "(任意)

プレイヤー側のスクリプト例:


extends CharacterBody2D

@export var move_speed: float = 200.0

var state: String = "idle"

func _physics_process(delta: float) -> void:
    var input_dir := Vector2.ZERO
    input_dir.x = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
    input_dir.y = Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up")

    if input_dir.length() > 0.0:
        state = "move"
        velocity = input_dir.normalized() * move_speed
    else:
        state = "idle"
        velocity = Vector2.ZERO

    move_and_slide()

この状態で実行すると、プレイヤーの頭上に以下のような表示が自動で出ます:

Player
velocity: (123.45, -67.89)
state: move

③ 敵AIに使い回す例

次に、敵キャラにもまったく同じ DebugLabel をそのままアタッチしてみます。

Enemy (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── DebugLabel (Node2D)

敵のスクリプト例:


extends CharacterBody2D

enum EnemyState { IDLE, PATROL, CHASE }

var current_state: EnemyState = EnemyState.IDLE
var velocity: Vector2 = Vector2.ZERO

func _physics_process(delta: float) -> void:
    match current_state:
        EnemyState.IDLE:
            velocity = Vector2.ZERO
        EnemyState.PATROL:
            velocity.x = 50.0
        EnemyState.CHASE:
            velocity.x = 120.0

    move_and_slide()

Enemy シーン内の DebugLabel には、例えば次のように設定します:

  • properties: ["velocity", "current_state"]
  • custom_prefix: "[ENEMY] "

これだけで、プレイヤーと敵の両方の状態が画面上で同時に追えるようになります。
プレイヤー用・敵用の別々のラベルスクリプトは不要で、すべて同じ DebugLabel コンポーネントで済みます。

④ 動く床やギミックにもアタッチする例

最後に、動く床(MovingPlatform)的なギミックに付けてみましょう。

MovingPlatform (Node2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── DebugLabel (Node2D)

動く床スクリプト例:


extends Node2D

@export var speed: float = 80.0
@export var amplitude: float = 32.0

var base_position: Vector2
var t: float = 0.0

func _ready() -> void:
    base_position = global_position

func _process(delta: float) -> void:
    t += delta * speed
    global_position.y = base_position.y + sin(t) * amplitude

DebugLabel の設定例:

  • properties: ["speed", "amplitude", "global_position"]
  • offset: (0, -24)

このように、Node2D であればなんでも頭上にステータスを出せる汎用コンポーネントとして使い回せます。

メリットと応用

DebugLabel を導入することで、次のようなメリットがあります。

  • シーン構造がスッキリ:プレイヤー用・敵用・ギミック用といった個別のデバッグラベルスクリプトを作らずに済む。
  • 継承に縛られないCharacterBody2D だろうが Node2D だろうが、継承ツリーをいじらずにポン付けできる。
  • デバッグコードの分離:本体ロジックとデバッグ表示が別スクリプトになるので、あとで削除/無効化しやすい。
  • レベルデザインが楽:ステージ上の任意のオブジェクトに後から DebugLabel を付け足すだけで挙動確認ができる。

特に「状態遷移が複雑なAI」や「物理挙動がシビアなアクション」では、画面上にステータスが出ているかどうかでデバッグ効率がまったく変わってきます。
コンポーネント指向でこういったデバッグ機能をまとめておくと、新しいシーンを作るたびに同じ仕組みを再利用できて、開発スピードも上がりますね。

ちょっとした改造案:キー入力で一括オン/オフ切り替え

例えば、ゲーム中に F3 キーで全ての DebugLabel を一括表示/非表示できるようにしたい場合、次のようなユーティリティ関数をどこかの「DebugManager」的なオートロードに追加すると便利です。


func toggle_all_debug_labels() -> void:
    ## シーンツリー内のすべての DebugLabel を検索し、visible をトグルする。
    var tree := get_tree()
    if not tree:
        return

    var debug_labels: Array = []
    tree.get_root().propagate_call("_collect_debug_labels", [debug_labels], false)

    if debug_labels.is_empty():
        return

    # 最初のラベルの状態を基準にトグル
    var new_visible := not debug_labels[0].visible
    for label in debug_labels:
        label.visible = new_visible

# 各シーン側の任意のノードに、こんなメソッドを生やしておくと propagate_call で拾える
func _collect_debug_labels(out_array: Array) -> void:
    if self is DebugLabel:
        out_array.append(self)

こういう「デバッグ用の機能」こそ、継承ではなくコンポーネントとして独立させておくと、後からいくらでも拡張しやすくなります。
ぜひ自分のプロジェクト用にカスタマイズしつつ、「継承より合成」なデバッグ環境を整えていきましょう。