Edited at

【Unity】CharacterController IsGrounded の判定を改善する

More than 3 years have passed since last update.

UniRxについての記事のまとめはこちら


Unity開発において、プレイヤキャラクタを作る際によく使われるコンポーネントにCharacterControllerがあります。

このコンポーネントは、オブジェクトが移動する時の床や壁の当たり判定、坂道の傾斜や段差の乗り越えなどの判定を勝手に計算してくれるとても便利なコンポーネントです。

ただしこのCharacterController、接地判定のIsGroundedの精度がイマイチだったりします。

坂道や段差を移動中、明らかに接地しているにも関わらずfalse判定が返ってきたりします。

そのためIsGroundedを基準にジャンプの可否を決定している場合、なかなかジャンプしてくれず非常にレスポンスの悪い挙動になってしまったりします。



(斜面を移動中はIsGroundedがtrue/falseで激しく値が変動してしまう)

そこで今回は、このIsGroundedの判定を上手いこと誤魔化して改善してみようと思います。


方法1: Raycastと併用して判定を甘くする

IsGroundedは、厳密にピッタリと床に接してないとtrueになってくれません。

そこでこの判定を甘くし、ある程度床に近ければ「地面に接している」と判定するようにしてみます。

判定にはPhysics.Raycastという、オブジェクトとの衝突を調べる機能を利用します。

(名前の通り、ある点から仮想的な光線を出し、それが物体にぶつかるかを調べる機能です)

このRaycastをGameObjectの足元から発射し、Raycastが地面と衝突するかどうかで地面に接しているか判定してみます。



(オブジェクトの底から真下にRaycastを放ち、衝突するかどうかで接地しているか調べる)


CheckGroundedWithRaycast

/// <summary>

/// 地面に接地しているかどうかを調べる
/// </summary>
public bool CheckGrounded()
{
//CharacterController取得
var controller = GetComponent<CharacterController>();
//CharacterControlle.IsGroundedがtrueならRaycastを使わずに判定終了
if (controller.isGrounded) { return true; }
//放つ光線の初期位置と姿勢
//若干身体にめり込ませた位置から発射しないと正しく判定できない時がある
var ray = new Ray(this.transform.position + Vector3.up * 0.1f, Vector3.down);
//探索距離
var tolerance = 0.3f;
//Raycastがhitするかどうかで判定
//地面にのみ衝突するようにレイヤを指定する
return Physics.Raycast(ray, tolerance, (int)GameLayer.Field);
}

この様に、CharacterController.IsGroundedとRaycastを併用することで斜面や段差での挙動を改善することができます。

ただし、このCheckGrounded()は実行する度に毎回Raycastを実行してしまいます。

実際に使う際はRaycastの結果をフレーム毎にキャッシュするなどすると良いかと思います。


方法2:IsGroundedの値の変動が落ち着くまでIsGroundedの値を無視する

IsGroundedの値をよく観察していると、「ジャンプや着地の直後」「斜面を移動中」はtrue/falseを変動し値がブレまくってることがわかります。

そこで、この値がぶれている間は値を無視し、値が落ち着いた時に利用するようにしてみます。

(最後に値が変化してからnミリ秒経過した時にその値で確定させる)

こういった時間が絡んだ判定処理にはRxが向いているので、RxのUnity向け実装のUniRxを使い書いてみます。


CheckGroundedComponent.cs

using UnityEngine;

using UniRx;
using System;

public class CheckGroundedComponent : MonoBehaviour
{
private bool _isGrounded;

/// <summary>
/// 地面に接地しているかどうか
/// </summary>
public bool IsGrounded { get { return _isGrounded; } }

void Start()
{
var controller = GetComponent<CharacterController>();
controller
.ObserveEveryValueChanged(x => x.isGrounded)
.ThrottleFrame(5)
.Subscribe(x => _isGrounded = x);
}
}


CharacterController.IsGroundedをラップし、値が最後に変化してから5フレーム安定するまではその値を無視するようにしてみました。

こういった「値の変化を監視する」「時間を使って判定する」といった処理はRxを使うと本当に簡単に記述できるのでオススメします。

学習コストはかなり高いですけどね…。


最後に

UniRxもっと流行るべき