今回は、Unityの「判定設計」について、自分なりの考えをまとめてみました。
はじめに
Unityを触り始めた頃は、こんな感じでタグを文字列で書いて判定を取ることが多いと思います。
private void OnTriggerEnter2D(Collider2D other)
{
if (other.gameObject.CompareTag("Coin"))
{
Destroy(other.gameObject);
}
}
シンプルで分かりやすいのですが、実はこの方法には後々トラブルの原因になりやすいポイントが潜んでいます。
なぜタグ直書きが危険なのか
タグを文字列で扱うと、次のような問題が起きやすくなります。
- typo(スペルミス)で簡単に壊れる
- インスペクターでタグ設定を忘れる事故が起きる
- リファクタリングが効かない(検索置換が難しい)
- プロジェクトが大きくなるほど管理が破綻しやすい
最初は気にならなくても、規模が大きくなるほど影響が出てきます。
タグを使わない設計のメリット
そこで「タグを使わない」という選択肢が出てきます。
- 文字列を排除できる
- 型安全になる
- 判定が高速
- Prefabのタグ設定漏れがなくなる
特に「文字列を排除できる」というのは大きなメリットです。
しかしレイヤーだけに寄せると起きる問題
とはいえ、レイヤーだけで全部を管理しようとすると別の問題が出てきます。
- レイヤーは32個しかない
- 「種類」「属性」などの意味的な分類には向かない
- 敵のタイプやアイテムの種類をレイヤーで表現すると破綻する
レイヤーは本来「物理的な分類(衝突・Raycast)」のための仕組みなので、無理に使うと逆に管理が難しくなります。
個人的最適解:タグとレイヤーを責務ごとに使い分ける
結論としては、タグとレイヤーを役割ごとに分けて使うのが一番バランスが良いです。
✔ レイヤー → 物理的分類(衝突・Raycast)
例:Ground / Player / Enemy / UI
✔ タグ → 意味的分類(種類・属性)
例:EnemyType / ItemType / NPC / Collectible
「物理的な判定はレイヤー」「意味的な分類はタグ」という使い分けがしっくりきます。
タグを安全に使う方法(文字列を排除する)
タグを使う場合でも、文字列を直書きしないだけで安全性が一気に上がります。
定数でタグ名を管理する
public static class Tags
{
public const string Player = "Player";
public const string Enemy = "Enemy";
public const string Coin = "Coin";
}
使用例
private void OnTriggerEnter2D(Collider2D other)
{
if (other.gameObject.CompareTag(Tags.Coin))
{
Destroy(other.gameObject);
}
}
これだけでtypoの心配がなくなり、リファクタリングも簡単になります。
Layerを使用した判定の取り方
レイヤーを使う場合は、LayerMaskに対象レイヤーが含まれているかをチェックします。
[SerializeField] private LayerMask _mask;
private void OnTriggerEnter2D(Collider2D other)
{
bool isInMask = (_mask.value & (1 << other.gameObject.layer)) != 0;
if (isInMask)
{
Destroy(other.gameObject);
}
}
拡張メソッドでさらに使いやすくする
毎回ビット演算を書くのは面倒なので、拡張メソッドを作っておくと便利です。
using UnityEngine;
namespace Extensions
{
public static class LayerMaskExtensions
{
private static bool IsLayerInMask(int layer, LayerMask mask)
{
return (mask.value & (1 << layer)) != 0;
}
public static bool IsInLayerMask(this RaycastHit hit, LayerMask mask)
{
return IsLayerInMask(hit.collider.gameObject.layer, mask);
}
public static bool IsInLayerMask(this Collider other, LayerMask mask)
{
return IsLayerInMask(other.gameObject.layer, mask);
}
public static bool IsInLayerMask(this Collision collision, LayerMask mask)
{
return IsLayerInMask(collision.gameObject.layer, mask);
}
public static bool IsInLayerMask(this Collider2D other, LayerMask mask)
{
return IsLayerInMask(other.gameObject.layer, mask);
}
public static bool IsInLayerMask(this Collision2D collision, LayerMask mask)
{
return IsLayerInMask(collision.gameObject.layer, mask);
}
public static bool IsInLayerMask(this GameObject gameObject, LayerMask mask)
{
return IsLayerInMask(gameObject.layer, mask);
}
public static bool IsInLayerMask(this Component component, LayerMask mask)
{
return IsLayerInMask(component.gameObject.layer, mask);
}
}
}
使用例
[SerializeField] private LayerMask _mask;
private void OnTriggerEnter2D(Collider2D other)
{
if (other.IsInLayerMask(_mask))
{
Destroy(other.gameObject);
}
}
コードがスッキリして読みやすくなります。
まとめ:壊れない判定設計のために
- 文字列直書きは避ける
- タグは定数化して安全に使う
- レイヤーは物理的分類に使う
- 拡張メソッドでLayerMaskを扱いやすくする
- タグとレイヤーを適材適所で使い分けるのが最も堅牢
判定周りはゲーム全体の安定性に直結する部分なので、早めに設計を整えておくと後々とても楽になります。