Godot 4 でアクションゲームやシューティングを作っていると、当たり判定がちゃんと置けているかを確認したくなりますよね。
とはいえ、毎回 Editor の「Debug > Visible Collision Shapes」をオンにしたり、Project Settings を触ったりするのはちょっと面倒です。
さらに、
- デバッグ中は当たり判定を見たいけど、本番ビルドでは隠したい
- プレイヤーだけ見たい、敵だけ見たい、みたいな「ピンポイント表示」をしたい
- チームで開発していて、人によって Debug 設定がバラバラになるのを避けたい
といったニーズも出てきます。でも、これを毎回シーンごとにスクリプトで書いたり、Node2D を継承したカスタムプレイヤーに全部押し込んだりすると、継承地獄とスクリプト肥大化まっしぐらですね。
そこで今回は、「どのノードにもポン付けできる」コンポーネントとして、当たり判定の可視化をオン・オフできる HitboxVisualizer を作ってみましょう。
コンポーネントをアタッチするだけで、ゲーム実行中にショートカットキーで CollisionShape の表示を切り替えられるようにしていきます。
【Godot 4】当たり判定が一目瞭然!「HitboxVisualizer」コンポーネント
以下が HitboxVisualizer コンポーネントのフルコードです。
どのシーンにも追加しやすいように、Node を継承したシンプルなコンポーネントにしています。
extends Node
class_name HitboxVisualizer
##
## HitboxVisualizer
## - 実行中に CollisionShape2D / CollisionPolygon2D / 3D のコリジョンを
## デバッグ表示としてオン・オフ切り替えるコンポーネント。
## - グローバル設定 (ProjectSettings) とローカルなオーバーライドの両方に対応。
##
## --- 設定パラメータ -----------------------------
@export_category("General")
## ゲーム開始時に「可視化オン」にするかどうか
@export var enable_on_start: bool = true
## このコンポーネントが有効なときだけ、グローバルの可視化設定を上書きするか
## true: このノード配下だけローカルに制御(推奨)
## false: ProjectSettings.debug/shapes/visible_collision を直接書き換える
@export var local_only: bool = true
## ショートカットキーでトグルしたい場合に使うアクション名
## InputMap に登録されていれば、押すたびに表示オン・オフを切り替える
@export var toggle_action: StringName = &"toggle_hitbox_visualizer"
@export_category("Target")
## 2D のコリジョンだけを対象にするか
@export var affect_2d: bool = true
## 3D のコリジョンだけを対象にするか
@export var affect_3d: bool = true
## 子孫ノードまで再帰的に探すかどうか
## false にすると、このノード直下の子だけを対象にする
@export var recursive: bool = true
## 特定のグループに属するノードだけを対象にしたい場合に指定
## 例: ["hitbox", "hurtbox"]
@export var target_groups: Array[StringName] = []
## --- 内部状態 -----------------------------------
var _is_visible: bool = false
func _ready() -> void:
# ゲーム開始時の状態をセット
_is_visible = enable_on_start
# グローバル設定を使うモードの場合は、ProjectSettings を直接変更
if not local_only:
_apply_global_debug_setting(_is_visible)
else:
# ローカルモードの場合は、このノード配下のコリジョンだけを制御
_apply_local_debug_setting(_is_visible)
# ショートカットアクションが未登録の場合は、軽く警告を出しておく
if toggle_action != StringName("") and not InputMap.has_action(toggle_action):
push_warning(
"HitboxVisualizer: InputMap にアクション '%s' が登録されていません。"
% [toggle_action]
)
func _process(_delta: float) -> void:
# toggle_action が設定されていれば、入力を監視してトグル
if toggle_action != StringName("") and Input.is_action_just_pressed(toggle_action):
toggle()
## --- 公開 API ------------------------------------
## 表示状態をトグル(オン・オフ切り替え)
func toggle() -> void:
set_visible(!_is_visible)
## 強制的に表示オン
func show_hitboxes() -> void:
set_visible(true)
## 強制的に表示オフ
func hide_hitboxes() -> void:
set_visible(false)
## 現在の状態を返す
func is_visible() -> bool:
return _is_visible
## 表示状態を直接セット
func set_visible(visible: bool) -> void:
if _is_visible == visible:
return
_is_visible = visible
if local_only:
_apply_local_debug_setting(_is_visible)
else:
_apply_global_debug_setting(_is_visible)
## --- 実装詳細 ------------------------------------
## プロジェクト全体の「Visible Collision Shapes」を切り替える
func _apply_global_debug_setting(enabled: bool) -> void:
# 2D のコリジョン表示
if affect_2d:
ProjectSettings.set_setting("debug/shapes/visible_collision_shapes", enabled)
# 3D のコリジョン表示
if affect_3d:
ProjectSettings.set_setting("debug/shapes/visible_collision_shapes_3d", enabled)
# 変更を即時反映
ProjectSettings.save()
## このノード配下だけコリジョン可視化を切り替える
func _apply_local_debug_setting(enabled: bool) -> void:
# 2D
if affect_2d:
_set_collision_debug_visible_2d(self, enabled, recursive)
# 3D
if affect_3d:
_set_collision_debug_visible_3d(self, enabled, recursive)
## 2D の CollisionShape2D / CollisionPolygon2D を処理
func _set_collision_debug_visible_2d(root: Node, enabled: bool, recurse: bool) -> void:
for child in root.get_children():
if not child is Node:
continue
# グループフィルタが指定されている場合は、グループに属しているかチェック
if target_groups.size() > 0 and not _node_in_any_group(child):
# グループに属していないノードはスキップするが、子孫は辿る
if recurse:
_set_collision_debug_visible_2d(child, enabled, recurse)
continue
if child is CollisionShape2D or child is CollisionPolygon2D:
# デバッグ表示用に Modulate や Editor の Helper を使う方法もあるが、
# ここでは単純に "editor_only" を使わない前提で可視状態を切り替える
child.visible = enabled
if recurse:
_set_collision_debug_visible_2d(child, enabled, recurse)
## 3D の CollisionShape3D / CollisionPolygon3D を処理
func _set_collision_debug_visible_3d(root: Node, enabled: bool, recurse: bool) -> void:
for child in root.get_children():
if not child is Node:
continue
if target_groups.size() > 0 and not _node_in_any_group(child):
if recurse:
_set_collision_debug_visible_3d(child, enabled, recurse)
continue
if child is CollisionShape3D or child is CollisionPolygon3D:
child.visible = enabled
if recurse:
_set_collision_debug_visible_3d(child, enabled, recurse)
## ノードが target_groups のいずれかに属しているかチェック
func _node_in_any_group(node: Node) -> bool:
if target_groups.is_empty():
return true
for group_name in target_groups:
if node.is_in_group(group_name):
return true
return false
使い方の手順
ここでは 2D アクションゲームのプレイヤーを例にして、HitboxVisualizer を組み込む手順を見ていきましょう。
① スクリプトを用意する
- 上記の GDScript をまるごとコピーして、
res://addons/components/hitbox_visualizer.gdなどに保存します。 - Godot を再読み込みすると、
HitboxVisualizerというクラス名がエディタから見えるようになります。
② インプットマップにトグル用アクションを追加
- メニューから Project > Project Settings > Input Map を開きます。
- 新しいアクション名として
toggle_hitbox_visualizerを追加します。 - キーボードの F2 など、好きなキーを割り当てます。
これで、ゲーム実行中に F2 を押すと当たり判定の可視化を切り替えられる準備が整いました。
③ プレイヤーにコンポーネントをアタッチする
プレイヤーシーンの構成例はこんな感じです:
Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D # 実際の当たり判定 ├── Area2D # 攻撃判定など │ └── CollisionShape2D # ヒットボックス └── HitboxVisualizer (Node) # ← このノードにスクリプトをアタッチ
- Player シーンを開く。
- Player の子ノードとして
Nodeを追加し、名前をHitboxVisualizerに変更。 - そのノードに、先ほど作った
HitboxVisualizerスクリプトをアタッチ。 - インスペクタで以下のように設定:
enable_on_start: On(ゲーム開始時から見えるように)local_only: On(プレイヤーのコリジョンだけ制御)toggle_action:"toggle_hitbox_visualizer"affect_2d: Onaffect_3d: Offrecursive: On(プレイヤー配下の全 CollisionShape2D を対象)
この状態でゲームを実行すると、プレイヤー配下の CollisionShape2D / CollisionPolygon2D が全て可視化されます。
F2(toggle_hitbox_visualizer に割り当てたキー)を押すと、表示オン・オフが切り替わります。
④ 敵キャラや動く床にも「ポン付け」する
コンポーネント指向の良いところは、同じ仕組みを他のシーンにもそのまま使い回せることです。
例えば敵キャラ:
Enemy (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D ├── Area2D │ └── CollisionShape2D └── HitboxVisualizer (Node)
動く床:
MovingPlatform (StaticBody2D) ├── Sprite2D ├── CollisionShape2D └── HitboxVisualizer (Node)
これらに同じ HitboxVisualizer コンポーネントを付けておけば、全て F2 一発で当たり判定を確認できます。
プレイヤー・敵・ギミックそれぞれのスクリプトに「デバッグ表示用のコード」を書く必要はありません。
メリットと応用
HitboxVisualizer をコンポーネントとして切り出しておくと、次のようなメリットがあります。
- シーン構造がスッキリ:プレイヤーや敵のスクリプトから「デバッグ表示ロジック」を追い出せます。
- 継承に縛られない:
CharacterBody2DだろうがArea2Dだろうが、好きなノードにポン付けできます。 - レベルデザインが捗る:レベルデザイナーが「このシーンだけ当たり判定を見たい」というとき、コンポーネントを追加するだけでOK。
- チーム開発での統一:全員が同じショートカットで同じようにヒットボックスを確認できるので、「あれ?自分の環境だと見えない」が減ります。
さらに、target_groups を活用すると、
- 「
hitboxグループだけ表示」 - 「
hurtboxグループだけ表示」
といった細かい制御もできます。
例えば、攻撃判定用の CollisionShape2D に hitbox グループを、被弾判定用に hurtbox グループを付けておけば、
@export var target_groups: Array[StringName] = [&"hitbox"]
とするだけで、「攻撃判定だけ見たい」という状況に対応できます。
改造案:デバッグ用のラベルを自動表示する
もう一歩踏み込んで、「今どのノードで可視化されているか」を画面にラベル表示することもできます。
例えば次のような関数を追加して、状態変更時にラベルを出すようにしてみましょう。
## 画面左上に現在の状態を一時的に表示する簡易デバッグ HUD
func _show_debug_label() -> void:
var root := get_tree().get_root()
if not root:
return
var label := Label.new()
label.text = "Hitbox Visualizer: " + (_is_visible ? "ON" : "OFF")
label.add_theme_color_override("font_color", Color.YELLOW)
label.position = Vector2(16, 16)
label.z_index = 100000 # 一番手前に出したいだけの雑実装
root.add_child(label)
# 1.5 秒後に自動で消す
label.create_timer(1.5).timeout.connect(func():
if is_instance_valid(label):
label.queue_free()
)
そして set_visible() の最後で _show_debug_label() を呼べば、トグルするたびに「ON / OFF」が画面に出て分かりやすくなります。
このように、「当たり判定可視化」という一見ニッチな機能も、継承ではなくコンポーネントとして切り出すことで、プロジェクト全体の見通しと再利用性がかなり良くなります。ぜひ自分のプロジェクト流にカスタマイズしてみてください。
