Godotでプレイヤーや敵の挙動をデバッグするとき、print()スパムやデバッガのウォッチだけに頼っていると、だんだん「今このキャラが何をしているのか」が直感的に分かりづらくなってきますよね。
特に「状態(State)」や「速度(Velocity)」を頻繁にいじるアクションゲームやAIでは、画面上にそのまま出ていてほしいことが多いです。
ありがちなのは、各キャラシーンの中に Label を置いて、そのノードに直接スクリプトを書いてしまうパターンです:
Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── Label ← Player専用のスクリプト直書き…
これを敵キャラや動く床にもコピペしていくと、「デバッグ用コードがあちこちに散らばる」「継承ツリーが汚れる」という、Godotあるある地獄にハマります。
そこで今回は「どのノードにもポン付けできるデバッグラベル」として、コンポーネント指向の DebugLabel コンポーネントを作ってみましょう。
【Godot 4】なんでも頭上ステータス表示!「DebugLabel」コンポーネント
DebugLabel は、親ノードの任意のプロパティ(例: velocity, state, hp など)を、自動で頭上に表示し続けるためのコンポーネントです。
- プレイヤーの
velocityとstate - 敵AIの
current_stateとtarget_position - 動く床の
speedやis_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 を付ける具体的な手順を見ていきましょう。
① コンポーネントスクリプトを用意する
- 上記の
DebugLabel.gdをプロジェクト内に保存します。
例:res://addons/debug_tools/DebugLabel.gd - Godot エディタを再読み込みすると、ノード追加ダイアログで
DebugLabelが検索できるようになります(class_name DebugLabelのおかげ)。
② プレイヤーに頭上デバッグラベルを付ける例
まずは典型的な 2D プレイヤーキャラを例にします。
Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── DebugLabel (Node2D)
Playerシーンを開く。- ルートの
Player (CharacterBody2D)を選択し、「子ノードを追加」からDebugLabelを追加。 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)
こういう「デバッグ用の機能」こそ、継承ではなくコンポーネントとして独立させておくと、後からいくらでも拡張しやすくなります。
ぜひ自分のプロジェクト用にカスタマイズしつつ、「継承より合成」なデバッグ環境を整えていきましょう。
