Unityで2Dゲームを実装する際に、地面に立っているか・地面に触れているかどうかを判定するスクリプトを実装します。
実装
GroundCheck.cs
using Sirenix.OdinInspector;
using UnityEngine;
using Updates;
namespace Movements
{
[DisallowMultipleComponent]
public class GroundCheck : MonoBehaviour
{
[Title("初期値設定")] public float GroundLimitAngle = 30f;
public float FootThreshold = 0.4f;
// 検知できる衝突の最大数
[SerializeField] private int maxCollisionCount = 8;
[Title("デバッグ")] [SerializeField] [ReadOnly]
private bool isGrounded = true;
public bool IsGrounded => isGrounded;
[SerializeField] [ReadOnly] private bool touchGround = false;
public bool TouchGround => touchGround;
[SerializeField] [ReadOnly] private Vector2 normal;
public Vector2 Normal => normal;
// 地面として扱わないゲームオブジェクト
[ReadOnly] public GameObject NotGroundObject;
private ContactPoint2D[] collisionContacts;
private int currentContactCount = 0;
private Transform tf;
private void Awake()
{
tf = transform;
}
private void Start()
{
collisionContacts = new ContactPoint2D[maxCollisionCount];
}
public void FixedUpdate()
{
CheckGrounded();
// フラグを下ろす
currentContactCount = 0;
}
private void CheckGrounded()
{
// 最初にいくつあるかを数える
// 衝突が1つだけなら、angleの判定を行う
if (currentContactCount <= 0)
{
isGrounded = false;
touchGround = false;
return;
}
touchGround = true;
if (currentContactCount == 1)
{
// 足元の判定を行う
if ((collisionContacts[0].point - (Vector2)tf.position).y >= FootThreshold)
{
isGrounded = false;
return;
}
var targetNormal = collisionContacts[0].normal;
var angle = Vector2.Angle(Vector2.up, targetNormal);
// 衝突対象がBoxCollider2Dで、頂点に衝突した場合
// 前回の状態を維持する
if (collisionContacts[0].collider is BoxCollider2D boxCollider)
{
var colliderAngle = Vector2.Angle(boxCollider.transform.up, targetNormal);
if (!Mathf.Approximately(colliderAngle % 90f, 0f))
{
return;
}
}
if (angle > GroundLimitAngle)
{
isGrounded = false;
}
else
{
isGrounded = true;
normal = targetNormal;
}
return;
}
else
{
var averageNormal = Vector2.zero;
var minNormal = Vector2.zero;
var minAngle = float.MaxValue;
var normalCount = 0;
for (var i = 0; i < currentContactCount; i++)
{
// 足元の判定を行う
if ((collisionContacts[i].point - (Vector2)tf.position).y >= FootThreshold)
{
continue;
}
var n = collisionContacts[i].normal;
var a = Vector2.Angle(Vector2.up, n);
if (a < minAngle)
{
minNormal = n;
minAngle = a;
}
averageNormal += n;
normalCount++;
}
averageNormal /= normalCount;
var averageAngle = Vector2.Angle(Vector2.up, averageNormal);
Vector2 currentNormal;
var currentAngle = 0f;
if (minAngle < averageAngle)
{
currentNormal = minNormal;
currentAngle = minAngle;
}
else
{
currentNormal = averageNormal;
currentAngle = averageAngle;
}
if (currentAngle > GroundLimitAngle)
{
isGrounded = false;
}
else
{
isGrounded = true;
normal = currentNormal;
}
}
}
private void OnCollisionStay2D(Collision2D col)
{
TryGetGround(col);
}
private void TryGetGround(Collision2D col)
{
if (NotGroundObject != null && col.gameObject == NotGroundObject) return;
collisionContacts[currentContactCount] = col.contacts[0];
currentContactCount++;
}
}
}
使い方
接触判定を行いたいGameObjectにアタッチして使います。
IsTouchGround
はどこでかを問わず他の物に接触しているかどうかを、IsGround
プロパティは実際に地面に立っているかどうかを返します。
Normal
プロパティは自分が今立っている地面の法線ベクトルを返します。