Unityを触り始めた頃は、Update() に「入力処理」「移動」「アニメーション」「当たり判定のデバッグ表示」など、ありとあらゆる処理を全部突っ込んでしまいがちですよね。
一見動くので満足してしまいますが、時間が経つほど「どこを触ればいいのか分からない」「当たり判定のバグを追いにくい」といった問題がどんどん積み上がっていきます。
特にアクションゲームやシューティングなどで重要になるのが「当たり判定」。
コライダーのサイズや位置がズレているのに、見た目では分からず、ゲームプレイ中に「なんか判定おかしくない?」と感じることも多いはずです。
そこでこの記事では、「当たり判定のデバッグ表示」だけを担当する小さなコンポーネントとして、HitboxVisualizer を用意してみましょう。
ゲーム実行中にキー入力で「Visible Collision(当たり判定の可視化)」を ON / OFF できるようにして、当たり判定の調整やデバッグを一気に楽にします。
【Unity】ゲーム中に当たり判定をON/OFF表示!「HitboxVisualizer」コンポーネント
ここでは、以下のような方針でコンポーネントを作ります。
- Collider(Box / Sphere / Capsule / Mesh / 2D)を自動検出して、Gizmos風の線で可視化
- ゲーム実行中にキー入力(例: F1)で表示の ON / OFF を切り替え
- プレイヤー、敵、弾、トラップなど、どんなオブジェクトにも「アタッチするだけ」で使える
- 描画処理とゲームロジックを分離し、SRP(単一責任の原則)を守る
フルコード:HitboxVisualizer.cs
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
/// <summary>
/**
* 実行中にColliderの当たり判定を可視化するコンポーネント。
* - 対応: Collider / Collider2D(Box, Sphere, Capsule, Mesh, Circle, Polygon など)
* - F1キーで可視化のON/OFF切り替え(キーはインスペクターから変更可能)
* - Gizmosではなく、実際のカメラにLineを描画するので「Gameビュー」で確認可能
*
* ポイント:
* - 描画ロジックをこのコンポーネントに閉じ込めることで、ゲームロジックと分離
* - プレハブに付けておけば、どのシーンでも同じように可視化できる
*/
/// </summary>
[DefaultExecutionOrder(1000)] // なるべく後で描画されるように
public class HitboxVisualizer : MonoBehaviour
{
// --- 設定項目 ---
[Header("表示切り替え")]
[SerializeField]
private bool _startVisible = true; // ゲーム開始時に可視化ONにするか
[Tooltip("可視化のON/OFFを切り替えるキー(Keyboardクラスのキー名)")]
[SerializeField]
private Key _toggleKey = Key.F1;
[Header("描画設定")]
[SerializeField]
private Color _hitboxColor = new Color(0f, 1f, 0f, 0.8f); // 緑
[SerializeField]
private Color _triggerColor = new Color(1f, 1f, 0f, 0.8f); // 黄
[SerializeField, Tooltip("線の太さ(ワールド座標系)")]
private float _lineWidth = 0.02f;
[SerializeField, Tooltip("2D Colliderも対象にするか")]
private bool _include2D = true;
[SerializeField, Tooltip("子オブジェクトのColliderも含めるか")]
private bool _includeChildren = true;
[Header("パフォーマンス")]
[SerializeField, Tooltip("毎フレームコライダー情報を更新するか(動的に形が変わる場合のみON)")]
private bool _updateEveryFrame = false;
// --- 内部状態 ---
private bool _visible;
// 対象となるColliderたち
private readonly List<Collider> _colliders3D = new List<Collider>();
private readonly List<Collider2D> _colliders2D = new List<Collider2D>();
// 描画用のLineRendererをまとめて管理
private readonly List<LineRenderer> _lineRenderers = new List<LineRenderer>();
// LineRendererのマテリアル(単純なカラー表示用)
private static Material _lineMaterial;
// 一時バッファ(GC削減用)
private static readonly Vector3[] _boxCorners = new Vector3[8];
// --- Unity ライフサイクル ---
private void Awake()
{
// 実行開始時の表示状態
_visible = _startVisible;
// マテリアルをまだ作っていなければ生成
if (_lineMaterial == null)
{
// Unity標準の「Sprites/Default」を利用(シンプルで扱いやすい)
Shader shader = Shader.Find("Sprites/Default");
if (shader == null)
{
shader = Shader.Find("Unlit/Color");
}
_lineMaterial = new Material(shader);
_lineMaterial.name = "HitboxVisualizer_LineMaterial";
}
CacheColliders();
BuildAllLines();
ApplyVisibility();
}
private void Update()
{
HandleToggleInput();
if (!_visible)
{
return;
}
if (_updateEveryFrame)
{
UpdateAllLines();
}
}
private void OnDestroy()
{
// 生成したLineRendererをクリーンアップ
foreach (var lr in _lineRenderers)
{
if (lr != null)
{
Destroy(lr.gameObject);
}
}
_lineRenderers.Clear();
}
// --- 入力処理 ---
/// <summary>
/// F1キー(デフォルト)で可視化のON/OFFを切り替える
/// 新Input SystemのKeyboardクラスを利用
/// </summary>
private void HandleToggleInput()
{
// キーボードが存在しない環境では何もしない(モバイルなど)
if (Keyboard.current == null)
{
return;
}
// 指定キーが押された瞬間にトグル
if (Keyboard.current[_toggleKey].wasPressedThisFrame)
{
_visible = !_visible;
ApplyVisibility();
}
}
// --- コライダー検出&キャッシュ ---
/// <summary>
/// 対象となるCollider / Collider2Dを取得してキャッシュする
/// </summary>
private void CacheColliders()
{
_colliders3D.Clear();
_colliders2D.Clear();
if (_includeChildren)
{
GetComponentsInChildren(_colliders3D);
if (_include2D)
{
GetComponentsInChildren(_colliders2D);
}
}
else
{
var col3d = GetComponent<Collider>();
if (col3d != null)
{
_colliders3D.Add(col3d);
}
if (_include2D)
{
var col2d = GetComponent<Collider2D>();
if (col2d != null)
{
_colliders2D.Add(col2d);
}
}
}
}
// --- LineRenderer生成&更新 ---
/// <summary>
/// すべてのColliderに対応するLineRendererを生成し、形状を構築する
/// </summary>
private void BuildAllLines()
{
// 既存のLineRendererを削除
foreach (var lr in _lineRenderers)
{
if (lr != null)
{
Destroy(lr.gameObject);
}
}
_lineRenderers.Clear();
// 3D Collider用ライン生成
foreach (var col in _colliders3D)
{
if (col == null) continue;
CreateLinesForCollider3D(col);
}
// 2D Collider用ライン生成
foreach (var col2d in _colliders2D)
{
if (col2d == null) continue;
CreateLinesForCollider2D(col2d);
}
UpdateAllLines();
}
/// <summary>
/// すべてのLineRendererの形状を更新する
/// (コライダーのサイズやTransformが変わったときに再計算)
/// </summary>
private void UpdateAllLines()
{
int lrIndex = 0;
// 3D Collider
foreach (var col in _colliders3D)
{
if (col == null) continue;
lrIndex = UpdateLinesForCollider3D(col, lrIndex);
}
// 2D Collider
foreach (var col2d in _colliders2D)
{
if (col2d == null) continue;
lrIndex = UpdateLinesForCollider2D(col2d, lrIndex);
}
}
/// <summary>
/// 表示・非表示の切り替えをLineRendererに反映
/// </summary>
private void ApplyVisibility()
{
foreach (var lr in _lineRenderers)
{
if (lr != null)
{
lr.enabled = _visible;
}
}
}
// --- 3D Collider用のライン生成・更新 ---
private void CreateLinesForCollider3D(Collider col)
{
// 今回は「バウンディングボックス」で表現するシンプルな実装にします。
// 必要に応じてColliderの種類ごとに細かく描画してもOKです。
// 1つのColliderにつき、1本のLineRenderer(ループ)を使う
var lr = CreateLineRenderer(col.transform);
_lineRenderers.Add(lr);
}
private int UpdateLinesForCollider3D(Collider col, int lrIndex)
{
if (lrIndex >= _lineRenderers.Count)
{
return lrIndex;
}
var lr = _lineRenderers[lrIndex];
if (lr == null)
{
return lrIndex + 1;
}
// コライダーのバウンディングボックスから8頂点を取得
var bounds = col.bounds;
// 8つのコーナーを計算
// front: +z, back: -z
Vector3 center = bounds.center;
Vector3 ext = bounds.extents;
_boxCorners[0] = center + new Vector3(-ext.x, -ext.y, -ext.z); // back-bottom-left
_boxCorners[1] = center + new Vector3(ext.x, -ext.y, -ext.z); // back-bottom-right
_boxCorners[2] = center + new Vector3(ext.x, ext.y, -ext.z); // back-top-right
_boxCorners[3] = center + new Vector3(-ext.x, ext.y, -ext.z); // back-top-left
_boxCorners[4] = center + new Vector3(-ext.x, -ext.y, ext.z); // front-bottom-left
_boxCorners[5] = center + new Vector3(ext.x, -ext.y, ext.z); // front-bottom-right
_boxCorners[6] = center + new Vector3(ext.x, ext.y, ext.z); // front-top-right
_boxCorners[7] = center + new Vector3(-ext.x, ext.y, ext.z); // front-top-left
// バウンディングボックスのエッジを順番に並べる(線でつなぐ)
// 角8つ + 始点に戻る1点 で合計17点
Vector3[] points = new Vector3[17];
int i = 0;
// back face
points[i++] = _boxCorners[0];
points[i++] = _boxCorners[1];
points[i++] = _boxCorners[2];
points[i++] = _boxCorners[3];
points[i++] = _boxCorners[0];
// bottom edge
points[i++] = _boxCorners[4];
points[i++] = _boxCorners[5];
points[i++] = _boxCorners[1];
// right edge
points[i++] = _boxCorners[2];
points[i++] = _boxCorners[6];
// top edge
points[i++] = _boxCorners[7];
points[i++] = _boxCorners[3];
// left edge
points[i++] = _boxCorners[0];
points[i++] = _boxCorners[4];
// front face
points[i++] = _boxCorners[5];
points[i++] = _boxCorners[6];
points[i++] = _boxCorners[7];
lr.positionCount = points.Length;
lr.SetPositions(points);
// isTriggerかどうかで色を変える
Color c = col.isTrigger ? _triggerColor : _hitboxColor;
lr.startColor = c;
lr.endColor = c;
return lrIndex + 1;
}
// --- 2D Collider用のライン生成・更新 ---
private void CreateLinesForCollider2D(Collider2D col2d)
{
var lr = CreateLineRenderer(col2d.transform);
_lineRenderers.Add(lr);
}
private int UpdateLinesForCollider2D(Collider2D col2d, int lrIndex)
{
if (lrIndex >= _lineRenderers.Count)
{
return lrIndex;
}
var lr = _lineRenderers[lrIndex];
if (lr == null)
{
return lrIndex + 1;
}
// 2D Colliderは、最終的にワールド座標のポリゴンとして描画します。
// Collider2Dには「GetPath」で頂点列を取得できるものが多いので、それを利用します。
// 2Dコライダーの形状を表す頂点リスト
List<Vector3> worldPoints = new List<Vector3>();
// 各Collider2Dの具体的な型によって処理を分岐
if (col2d is BoxCollider2D box)
{
// BoxCollider2Dは中心とサイズから四隅を計算
Transform t = box.transform;
Vector2 size = box.size;
Vector2 offset = box.offset;
// ローカル空間での4頂点
Vector3 p0 = new Vector3(offset.x - size.x / 2f, offset.y - size.y / 2f, 0f);
Vector3 p1 = new Vector3(offset.x + size.x / 2f, offset.y - size.y / 2f, 0f);
Vector3 p2 = new Vector3(offset.x + size.x / 2f, offset.y + size.y / 2f, 0f);
Vector3 p3 = new Vector3(offset.x - size.x / 2f, offset.y + size.y / 2f, 0f);
// ワールド座標に変換
worldPoints.Add(t.TransformPoint(p0));
worldPoints.Add(t.TransformPoint(p1));
worldPoints.Add(t.TransformPoint(p2));
worldPoints.Add(t.TransformPoint(p3));
worldPoints.Add(t.TransformPoint(p0)); // 閉じる
}
else if (col2d is CircleCollider2D circle)
{
// CircleCollider2Dは円周をいくつかの線分で近似
Transform t = circle.transform;
int segments = 24;
float radius = circle.radius;
Vector2 offset = circle.offset;
for (int i = 0; i <= segments; i++)
{
float angle = (float)i / segments * Mathf.PI * 2f;
float x = Mathf.Cos(angle) * radius + offset.x;
float y = Mathf.Sin(angle) * radius + offset.y;
Vector3 local = new Vector3(x, y, 0f);
worldPoints.Add(t.TransformPoint(local));
}
}
else if (col2d is CapsuleCollider2D capsule)
{
// カプセルも円弧+直線で近似(ざっくり実装)
Transform t = capsule.transform;
int segments = 16;
Vector2 size = capsule.size;
Vector2 offset = capsule.offset;
// 向きによって形が変わる
bool vertical = capsule.direction == CapsuleDirection2D.Vertical;
float radius = vertical ? size.x / 2f : size.y / 2f;
float height = vertical ? size.y : size.x;
float straight = Mathf.Max(0f, height - 2f * radius);
// 上半分の円弧
for (int i = 0; i <= segments; i++)
{
float angle = Mathf.PI * (i / (float)segments);
float x = Mathf.Cos(angle) * radius;
float y = Mathf.Sin(angle) * radius;
Vector2 local;
if (vertical)
{
local = new Vector2(x, y + straight / 2f);
}
else
{
local = new Vector2(y + straight / 2f, x);
}
local += offset;
worldPoints.Add(t.TransformPoint(local));
}
// 下半分の円弧
for (int i = 0; i <= segments; i++)
{
float angle = Mathf.PI * (i / (float)segments) + Mathf.PI;
float x = Mathf.Cos(angle) * radius;
float y = Mathf.Sin(angle) * radius;
Vector2 local;
if (vertical)
{
local = new Vector2(x, y - straight / 2f);
}
else
{
local = new Vector2(y - straight / 2f, x);
}
local += offset;
worldPoints.Add(t.TransformPoint(local));
}
// 始点に戻る
if (worldPoints.Count > 0)
{
worldPoints.Add(worldPoints[0]);
}
}
else
{
// PolygonCollider2D, EdgeCollider2D など
// GetPathでローカル座標の頂点列を取得してワールド座標へ変換
var pathCount = col2d.pathCount;
Transform t = col2d.transform;
for (int pathIndex = 0; pathIndex < pathCount; pathIndex++)
{
var path = new List<Vector2>();
col2d.GetPath(pathIndex, path);
if (path.Count == 0) continue;
for (int i = 0; i < path.Count; i++)
{
worldPoints.Add(t.TransformPoint(path[i]));
}
// PolygonCollider2Dなら閉じる
if (!(col2d is EdgeCollider2D))
{
worldPoints.Add(t.TransformPoint(path[0]));
}
}
}
// 頂点がない場合はスキップ
if (worldPoints.Count < 2)
{
lr.positionCount = 0;
}
else
{
lr.positionCount = worldPoints.Count;
lr.SetPositions(worldPoints.ToArray());
}
// isTriggerかどうかで色を変える
Color c = col2d.isTrigger ? _triggerColor : _hitboxColor;
lr.startColor = c;
lr.endColor = c;
return lrIndex + 1;
}
// --- LineRenderer生成ユーティリティ ---
private LineRenderer CreateLineRenderer(Transform parent)
{
// LineRenderer専用の子オブジェクトを作成
GameObject go = new GameObject("HitboxVisualizer_Line");
go.transform.SetParent(parent, false);
go.transform.localPosition = Vector3.zero;
go.transform.localRotation = Quaternion.identity;
go.transform.localScale = Vector3.one;
var lr = go.AddComponent<LineRenderer>();
lr.sharedMaterial = _lineMaterial;
lr.useWorldSpace = true;
lr.loop = false;
lr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
lr.receiveShadows = false;
lr.motionVectorGenerationMode = MotionVectorGenerationMode.ForceNoMotion;
lr.allowOcclusionWhenDynamic = false;
lr.startWidth = _lineWidth;
lr.endWidth = _lineWidth;
lr.numCapVertices = 0;
lr.numCornerVertices = 0;
// デバッグ表示なので、SortingLayerなどはお好みで
lr.sortingOrder = 10000;
return lr;
}
}
使い方の手順
ここからは、実際にシーンに組み込む手順を見ていきましょう。
-
スクリプトを用意する
上記のHitboxVisualizer.csをプロジェクトのScriptsフォルダなどに保存します。
Unityが自動コンパイルしたら、インスペクターからアタッチできるようになります。 -
プレイヤーや敵にアタッチする
例として、以下のようなオブジェクトに付けると便利です。- プレイヤーキャラクター(
CharacterControllerやRigidbody+Collider) - 敵キャラクター(攻撃判定や被弾判定を持つオブジェクト)
- 弾・ミサイルなどのプロジェクタイル
- トゲ床や動くトラップなどのギミック
それぞれの GameObject に
HitboxVisualizerコンポーネントを追加しましょう。
子オブジェクトにもコライダーがある場合は、インスペクターの「子オブジェクトも含める」にチェックを入れておくと、まとめて可視化できます。 - プレイヤーキャラクター(
-
インスペクターで表示設定を調整する
- Start Visible:ゲーム開始時から可視化しておきたい場合は ON
- Toggle Key:標準では
F1。他のキーに変えたい場合はプルダウンから選択 - Hitbox Color / Trigger Color:通常判定とトリガー判定の色を分けておくと便利
- Line Width:線が細すぎて見えない場合は少し太くする
- Include 2D:2Dゲームなら ON、3Dゲームで2Dコライダーを使っていなければ OFF でもOK
- Update Every Frame:コライダーのサイズや位置が動的に変化する(攻撃判定が伸びるなど)場合のみ ON
-
プレイしてF1でON/OFFを試す
再生ボタンを押してゲームを実行し、F1キーを押してみましょう。- プレイヤーの周りに緑色の枠が表示されていれば、当たり判定の可視化成功です。
- 敵の
isTriggerな攻撃判定は、黄色の枠で表示されるはずです。 - 弾やトラップも同様に、コライダーに沿った線が表示されます。
これで、アニメーションや移動に対して当たり判定がどのように動いているか、Gameビュー上で直感的に確認できるようになります。
メリットと応用
HitboxVisualizer を使うメリットは、単に「見えるようになる」ことだけではありません。
- プレハブごとにデバッグ設定を持てる
プレイヤーや敵のプレハブにあらかじめHitboxVisualizerを付けておけば、どのシーンに配置しても同じように当たり判定を確認できます。
シーンごとに「デバッグ用の巨大なマネージャースクリプト」を書く必要がなくなります。 - レベルデザインの調整が楽になる
トゲ床や動く足場などに付けておくと、「この位置だとプレイヤーが少し浮いて見える」「当たってないのにダメージを受けている」などの問題をすぐに発見できます。
デザイナーが自分で判定を確認しながら配置を微調整できるのも大きなメリットですね。 - 責務が分離されていてテストしやすい
当たり判定の「ロジック」と「可視化」が分かれているので、ゲームの本番ビルドではこのコンポーネントを外す、あるいはビルドスクリプトで自動無効化する、といった運用も簡単です。
「デバッグ用のif文」がゲームロジックに混ざらないので、コードがすっきり保てます。
さらに、プロジェクトに合わせて少し改造してみると、もっと便利になります。
例えば、「特定のレイヤーだけ可視化する」「ゲーム全体のデバッグメニューからON/OFFを切り替える」などですね。
以下は、特定のレイヤーだけ可視化するフィルタ を追加する改造案の一例です。
/// <summary>
/// 指定したLayerMaskに含まれるColliderだけを可視化対象にする
/// (インスペクターから _layerMask を設定しておく)
/// </summary>
[SerializeField]
private LayerMask _layerMask = ~0; // デフォルトは「全レイヤー」
private void FilterCollidersByLayer()
{
_colliders3D.RemoveAll(col =>
col == null || ((_layerMask.value & (1 << col.gameObject.layer)) == 0));
_colliders2D.RemoveAll(col2d =>
col2d == null || ((_layerMask.value & (1 << col2d.gameObject.layer)) == 0));
}
CacheColliders() の最後で FilterCollidersByLayer() を呼ぶようにすれば、「プレイヤーだけ」「敵だけ」といった絞り込み表示も簡単に実現できます。
こうして小さな責務のコンポーネントを積み重ねていくと、デバッグもレベルデザインもかなり快適になりますね。
