はじめに
unityの接地判定について調べるとたくさんの記事が出てくると思うが、そのほとんどが足元にtriggerを設置するかRaycastで判定をとっていると思う。だがそれは足の少し離れたところでも接地判定が有効になってしまうだけでなく、プレイヤーの当たり判定の変更などに伴って、それらの判定の位置も調整する必要が出てくる。
そこで正確で汎用的な接地判定の取得について考えてみた。
接地とみなせる条件
どのようにプレイヤーとオブジェクトが接触していたら接地とみなせるか。
接触した面がある程度上を向いていたら接地としていいと思う。
つまり接触した点の法線ベクトルとワールド単位の「上」ベクトルのなす角が一定以下ということ。これをもとに接地判定を考えた。
接地判定処理
接触点の法線ベクトルによって接地判定をする
private float _maxAngleToTreatAsGround;
private bool _isGround;
private void OnCollisionEnter(Collision other)
{
for (int i = 0; i < other.contactCount; i++)
{
if (Vector3.Angle(Vector3.up, other.GetContact(i).normal) < _maxAngleToTreatAsGround)
{
_isGround = true;
}
}
}
contactCount
で接触点の数を取得し、GetContact
ですべての接触点の情報を取得する。
EnterとExitはダメな場合がある
接地判定の解除法を考えたとき最初に思いつくのはOnCollisionExitだろう。
だがEnterとExitはオブジェクトに接触または離れたときにしか実行されない。
傾斜が段々大きくなっていく一つのオブジェクトがあったとき、正しい判定が取れない。
↑オブジェクトから離れていないので常に接地状態である
Stayだけを使う
OnCollisionStayはオブジェクトと接触しているとき、常に実行される。
つまり常にその接触点に地面判定をかけることが出来る。
だがこれだけだと接触判定を解除することが出来ないので、その解除処理を2つ考えてみた。
方法1. FixedUpdateの処理の最後にfalseにする
イベント関数の事項順序を見るとOnCollisionはFixedUpdateの後に同じタイミングで実行されていることがわかる。これはFixedUpdateの最後に接地判定をfalseにしてからStayで判定を行うという方法。Stayが呼ばれない、つまりオブジェクトと接していないときはfalseとなり、接触しているときはStayで判定を行う。
private void FixedUpdate()
{
// ~~_isGroundを使った何らかの処理~~
_isGround = false;
}
private void OnCollisionStay(Collision other)
{
for (int i = 0; i < other.contactCount; i++)
{
if (Vector3.Angle(Vector3.up, other.GetContact(i).normal) < _maxAngleToTreatAsGround)
{
_isGround = true;
}
}
}
方法2. CancelInvokeする
接地状態となったときに、一定時間後falseにする処理をはしらせる。次フレームでまだ接地状態ならその処理を先延ばしにする方法。この一定時間後というのが曖昧である。
private void OnCollisionStay(Collision other)
{
for (int i = 0; i < other.contactCount; i++)
{
if (Vector3.Angle(Vector3.up, other.GetContact(i).normal) < _maxAngleToTreatAsGround)
{
_isGround = true;
CancelInvoke(nameof(CancelGround));
Invoke(nameof(CancelGround), Time.fixedDeltaTime * 2);
}
}
}
void CancelGround()
{
_isGround = false;
}
おわりに
接地判定の方法に正解はないしゲームによっても違うだろうけど。
ただいろんな人の考え方を見たい。