Best ways to check isGrounded

Hello!
How are you guys doing ground checks?
I am using CharacterController. And the isGrounded flag has not worked out for me at all.
I have used different methods. My current one is to send a series of raycasts down using Random.insideUnitCircle. If one of them hits it returns true. Else false. The first cast is at the center of the character with no randomness. If one raycast hits, the method returns. So no unnececary raycasting is being done.
I am currently running this method 30 times a second and doing at max 20 casts.
Since sometimes you are at a very smal edge where the random circle doesn’t hit. I’ve added two vars. One called IsGrounded. And one called SmoothIsGrounded. SmoothIsgrounded is true if we get grounded. And false if IsGrounded has been false for two checks (Used for animation).

At the moment I am drawing a ray of the raycasts that doesn’t hit. (Wasted processing time). Looks like this:

Accuracy wise I would say my method is great.

How are you guys doing this? Should I change how I determin this or is this actually an alright method?

SetGrounded get’s called every frame.

    private bool GroundCheck()
    {
        Ray centerRay = new Ray(RayOrigin.position, -_myTransform.up);
        Debug.DrawRay(RayOrigin.position, -_myTransform.up * RayDistance, Color.green, 0.1f);
        if (Physics.Raycast(centerRay, RayDistance, IgnorePlayerMask))
        {
            return true;
        }
        else
        {
            for (int i = 0; i < MaxAmountGroundCheckRays - 1; i++)
            {
                Vector2 circle = Random.insideUnitCircle * RayDistanceFromCenter;
                Ray ray = new Ray(RayOrigin.position + new Vector3(circle.x, 0, circle.y), -_myTransform.up);
                Debug.DrawRay(RayOrigin.position + new Vector3(circle.x, 0, circle.y), -_myTransform.up * RayDistance, Color.red, 0.1f);
                if (Physics.Raycast(ray, RayDistance, IgnorePlayerMask))
                {
                    return true;
                }
            }
            return false;
        }
    }

    void SetGrounded()
    {
        if (Time.time - _lastGroundCheck >= 1f / GroundChecksPerSecond)
        {
            _lastGroundCheck = Time.time;
            bool isGrounded = GroundCheck();

            if (!isGrounded && !groundedLastCheck)
                SmoothRayGrounded = false;
            else
            {
                SmoothRayGrounded = true;
            }
            groundedLastCheck = isGrounded;
            IsRayGrounded = isGrounded;
        }
    }

Don’t throw random raycasts…

Use a SphereCast or a CapsuleCast, and make it the same size as the collider.

Here’s an old GroundingResolver I have used in some games:

using UnityEngine;

using com.spacepuppy;
using com.spacepuppy.Movement;

namespace com.mansion.Movement
{

    public class GroundingResolver : SPComponent
    {


        public enum GroundingState
        {
            Unknown = -2,
            Hanging = -1,
            Grounded = 0,
            Jumping = 1,
            Descending = 2,
            Falling = 3
        }

        #region Fields

        [Tooltip("Distance to project below the player to check ground. Should be greater than or equal to the skin width of the attached CharacterController.")]
        public float GroundingSkinWidth = 0.05f;

        public float TerminalFallDistance = 15.0f;

        [Tooltip("Duration of time considered just jumped. This way if we're near the ground, we don't signal as grounded if we're initiating a jump.")]
        public float JustJumpedCooldown = 0.1f;

        [Tooltip("The ground normal is calculated using a CapsuleCast which improperly calculates the surface normal. Set this true to repair the surface normal, only if necessary, as it's a lot more extra work.")]
        public bool RepairSurfaceNormal = false;

        private MovementMotor _motor;

        private Vector3 _groundNormal;
        private GroundingState _currentState;
        private Vector3 _lastGroundedPos;
        private Vector3 _lastGroundNormal;
        private float _lastGroundedTime;
        private float _lastJumpedTime;

        #endregion

        #region CONSTRUCTOR

        protected override void Awake()
        {
            base.Awake();

            _motor = this.GetComponent<MovementMotor>();
        }

        protected override void OnStartOrEnable()
        {
            base.OnStartOrEnable();

            _motor.BeforeUpdateMovement -= this.OnBeforeUpdateMovement;
            _motor.BeforeUpdateMovement += this.OnBeforeUpdateMovement;
        }

        protected override void OnDisable()
        {
            base.OnDisable();

            _motor.BeforeUpdateMovement -= this.OnBeforeUpdateMovement;
        }

        #endregion

        #region IGroundingResolver Interface

        /// <summary>
        /// Returns true if the last ground test trace hit something
        /// </summary>
        public bool IsGrounded
        {
            get { return _groundNormal != Vector3.zero; }
        }

        public Vector2 GroundNormal
        {
            get { return _groundNormal; }
        }

        public Vector2 LastGroundedPosition { get { return _lastGroundedPos; } }

        /// <summary>
        /// The time at which we last calculated being on the ground.
        /// </summary>
        public float LastGroundedTime { get { return _lastGroundedTime; } }

        public Vector3 DesiredJumpNormal { get { return Vector3.up; } }

        public float LastJumpedTime { get { return _lastJumpedTime; } }

        public void SetJumping()
        {
            _currentState = GroundingState.Jumping;
            _lastGroundedPos = this.entityRoot.transform.position;
            _lastJumpedTime = Time.time;
        }

        #endregion

        #region Properties

        public GroundingState CurrentState { get { return _currentState; } }

        /// <summary>
        /// Returns true if the time since the last time jumped is less than JustJumpedCooldown.
        /// </summary>
        public bool JustJumped
        {
            get { return Time.time - _lastJumpedTime < this.JustJumpedCooldown; }
        }

        #endregion

        #region Methods

        public void SetGrounded(Vector2 groundNormal)
        {
            _groundNormal = groundNormal;

            _currentState = GroundingState.Grounded;
            var oldPos = _lastGroundedPos;
            _lastGroundedPos = this.entityRoot.transform.position;
        }

        public void SetDropping(bool takeCurrentPositionAsLastGroundedPosition)
        {
            _currentState = GroundingState.Descending;
            if (takeCurrentPositionAsLastGroundedPosition)
            {
                _lastGroundedPos = this.entityRoot.transform.position;
            }
        }

        public void SetHanging()
        {
            _currentState = GroundingState.Hanging;
        }






        public GroundingState UpdateGroundingState()
        {
            if (_currentState == GroundingState.Hanging)
            {
                return _currentState;
            }

            if (_currentState == GroundingState.Grounded)
            {
                _lastGroundedPos = _motor.Controller.LastPosition;
                if (!this.IsGrounded)
                {
                    //LEFT GROUND
                    _currentState = GroundingState.Descending;
                }
            }
            else if (_currentState > GroundingState.Grounded)
            {
                if (this.IsGrounded)
                {
                    //LANDED
                    _currentState = GroundingState.Grounded;
                    var oldPos = _lastGroundedPos;
                    _lastGroundedPos = this.entityRoot.transform.position;
                }
                else if (_currentState == GroundingState.Jumping)
                {
                    if (_motor.Controller.LastVelocity.y < 0) _currentState = GroundingState.Descending;
                }
                else if (_currentState == GroundingState.Descending)
                {
                    if (_lastGroundedPos.y - this.entityRoot.transform.position.y > this.TerminalFallDistance)
                    {
                        _currentState = GroundingState.Falling;
                    }
                }
                else if (_currentState == GroundingState.Falling)
                {

                }

            }
            else
            {
                _currentState = (this.IsGrounded) ? GroundingState.Grounded : GroundingState.Descending;
                _lastGroundedPos = this.entityRoot.transform.position;
            }

            return _currentState;
        }

        /// <summary>
        /// Retests the ground normal and returns true if grounded.
        /// </summary>
        /// <returns></returns>
        public bool UpdateGroundNormal()
        {
            if (this.JustJumped)
            {
                _groundNormal = Vector2.zero;
                return false;
            }

            var geom = _motor.Controller.GetGeom(true);
            var d = this.GroundingSkinWidth + _motor.Controller.SkinWidth;
            RaycastHit hit;
            if (geom.Cast(Vector3.down, out hit, d, Constants.MASK_SURFACE))
            {
                if (this.RepairSurfaceNormal)
                {
                    _groundNormal = com.spacepuppy.Geom.PhysicsUtil.RepairHitSurfaceNormal(hit, Constants.MASK_SURFACE);
                }
                else
                {
                    _groundNormal = hit.normal;
                }
                _lastGroundedTime = Time.time;
            }
            else
            {
                _groundNormal = Vector2.zero;
            }

            return _groundNormal != Vector3.zero;
        }

        #endregion

        #region IMovementEnhancer Interface

        private void OnBeforeUpdateMovement(object sender, System.EventArgs e)
        {
            this.UpdateGroundNormal();
            this.UpdateGroundingState();
        }

        #endregion


    }

}

Note, it does use some custom stuff from my framework… but there’s simple ways around it:

MovementMotor - this is my script that deals with the actual moving of the object, it abstracts away from both CharacterController & Rigidbody so you can select either or. You can just use CharacterController directly.

MovementMotor.BeforeUpdateMovement - I calculate everything before the movement update. This event is how I signal it, you could signal it in whatever manner you want.

MovementMotor.LastPosition & MovementMotor.LastVelocity - you can just use CharacterControler.velocity and CharacterController.transform.position instead.

MovementMotor.Controller.GetGeom - I have my own custom geometry structures that abstract out if something is sphere/capsule/cube/etc. You can just do SphereCast or CapsuleCast instead.

…

Also, the RepairSurfaceNormal thing.

So, I don’t know if this is still a thing, I haven’t used the surface normal of a capsule cast in a long while.

But the last time I did, the normal returned by CapsuleCast was buggy. It returned the wrong normal, if I recall correctly it returned the normal off the capsule, and not the surface it hit. Which is stupid, because the normal off the capsule is a simple calculation (it’s just the hit point - center of the capsule end). Anyways, this is there to calculate said surface normal, incase your game needs such information. Otherwise… I leave it false because it’s extra work that might not be necessary.

We had it for a game a long while back where you could walk on all surfaces, so we needed that information.

Hm, I see your point. When looking back at it it seems like a waste. I had no real success with the Spherecasts before but I do see their potential. Would there be any noticable overhead of being sortof wasteful like I am on per say a low end machine? Or is my raycast replaced with a spherecast more because it’s “the proper way and better looking”?

SphereCast and CapsuleCast are built in PhysX calculations. They’re highly optimized methods. Much faster than multiple Raycasts.

Alright, I’m going to be honest. I did not know they existed. I only knew of CheckSphere, that’s what I originaly thought you meant. But a spherecast is perfect. Thanks alot

1 Like