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 を組み込む手順を見ていきましょう。

① スクリプトを用意する

  1. 上記の GDScript をまるごとコピーして、res://addons/components/hitbox_visualizer.gd などに保存します。
  2. Godot を再読み込みすると、HitboxVisualizer というクラス名がエディタから見えるようになります。

② インプットマップにトグル用アクションを追加

  1. メニューから Project > Project Settings > Input Map を開きます。
  2. 新しいアクション名として toggle_hitbox_visualizer を追加します。
  3. キーボードの F2 など、好きなキーを割り当てます。

これで、ゲーム実行中に F2 を押すと当たり判定の可視化を切り替えられる準備が整いました。

③ プレイヤーにコンポーネントをアタッチする

プレイヤーシーンの構成例はこんな感じです:

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D        # 実際の当たり判定
 ├── Area2D                  # 攻撃判定など
 │    └── CollisionShape2D   # ヒットボックス
 └── HitboxVisualizer (Node) # ← このノードにスクリプトをアタッチ
  1. Player シーンを開く。
  2. Player の子ノードとして Node を追加し、名前を HitboxVisualizer に変更。
  3. そのノードに、先ほど作った HitboxVisualizer スクリプトをアタッチ。
  4. インスペクタで以下のように設定:
    • enable_on_start: On(ゲーム開始時から見えるように)
    • local_only: On(プレイヤーのコリジョンだけ制御)
    • toggle_action: "toggle_hitbox_visualizer"
    • affect_2d: On
    • affect_3d: Off
    • recursive: 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 グループだけ表示」

といった細かい制御もできます。
例えば、攻撃判定用の CollisionShape2Dhitbox グループを、被弾判定用に 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」が画面に出て分かりやすくなります。

このように、「当たり判定可視化」という一見ニッチな機能も、継承ではなくコンポーネントとして切り出すことで、プロジェクト全体の見通しと再利用性がかなり良くなります。ぜひ自分のプロジェクト流にカスタマイズしてみてください。