UniRxについての記事のまとめはこちら
Unity開発において、プレイヤキャラクタを作る際によく使われるコンポーネントにCharacterControllerがあります。
このコンポーネントは、オブジェクトが移動する時の床や壁の当たり判定、坂道の傾斜や段差の乗り越えなどの判定を勝手に計算してくれるとても便利なコンポーネントです。
ただしこのCharacterController、接地判定のIsGroundedの精度がイマイチだったりします。
坂道や段差を移動中、明らかに接地しているにも関わらずfalse判定が返ってきたりします。
そのためIsGroundedを基準にジャンプの可否を決定している場合、なかなかジャンプしてくれず非常にレスポンスの悪い挙動になってしまったりします。
(斜面を移動中はIsGroundedがtrue/falseで激しく値が変動してしまう)
そこで今回は、このIsGroundedの判定を上手いこと誤魔化して改善してみようと思います。
方法1: Raycastを利用する
IsGroundedは、厳密にピッタリと床に接してないとtrueになってくれません。
そこでこの判定を甘くし、ある程度床に近ければ「地面に接している」と判定するようにしてみます。
判定にはPhysics.Raycastという、オブジェクトとの衝突を調べる機能を利用します。
(名前の通り、ある点から仮想的な光線を出し、それが物体にぶつかるかを調べる機能です)
このRaycastをGameObjectの足元から発射し、Raycastが地面と衝突するかどうかで地面に接しているか判定してみます。
(オブジェクトの底から真下にRaycastを放ち、衝突するかどうかで接地しているか調べる)
using UnityEngine;
public class RaycastCheck : MonoBehaviour
{
// Rayの長さ
[SerializeField] private float _rayLength = 1f;
// Rayをどれくらい身体にめり込ませるか
[SerializeField] private float _rayOffset;
// Rayの判定に用いるLayer
[SerializeField] private LayerMask _layerMask = default;
private CharacterController _characterController;
private bool _isGround;
private void Start()
{
// CharacterControllerを取得
_characterController = GetComponent<CharacterController>();
}
private void FixedUpdate()
{
// 接地判定
_isGround = CheckGrounded();
}
private bool CheckGrounded()
{
// 放つ光線の初期位置と姿勢
// 若干身体にめり込ませた位置から発射しないと正しく判定できない時がある
var ray = new Ray(origin: transform.position + Vector3.up * _rayOffset, direction: Vector3.down);
// Raycastがhitするかどうかで判定
// レイヤの指定を忘れずに
return Physics.Raycast(ray, _rayLength, _layerMask);
}
// Debug用にRayを可視化する
private void OnDrawGizmos()
{
// 接地判定時は緑、空中にいるときは赤にする
Gizmos.color = _isGround ? Color.green : Color.red;
Gizmos.DrawRay(transform.position + Vector3.up * _rayOffset, Vector3.down * _rayLength);
}
}
このようにRaycastを使うことで地面との接地判定を甘くすることができます。
ただしこの方法には次のような欠点もあるので、そこは工夫が必要です。
- Rayの長さを長くしすぎると、ジャンプしているのに接地したままの判定になることがある
- ジャンプ直後はRayを一時的に短くするなどの工夫が必要
- 地面が凸凹している場合は正しく判定できないことがある
- SphereCastやBoxCastなどを代わりに使うなどするとよい
方法2:IsGroundedの値の変動が落ち着くまでIsGroundedの値を無視する
IsGroundedの値をよく観察していると、「ジャンプや着地の直後」「斜面を移動中」はtrue/falseを変動し値がブレまくってることがわかります。
そこで、この値がぶれている間は値を無視し、値が落ち着いた時に利用するようにしてみます。
(最後に値が変化してからnミリ秒経過した時にその値で確定させる)
こういった時間が絡んだ判定処理にはRxが向いているので、RxのUnity向け実装のUniRxを使い書いてみます。
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フレーム安定するまではその値を無視するようにしてみました。
方法1と方法2どっちが良いか
細かく挙動を調整できるという点で「方法1」の方が良いです。
方法2は覚えておく程度で、実際に使う機会はそんなにないと思います。