CharacterController.isGrounded - Unreliable or bad code?

I’ve started (semi-)serious development with Unity yesterday and started out with a First-Person Controller Script. For the movement I’m using a Character Controller, which works just fine.

Once I started coding the gravity interaction however, things got a bit iffy. I started coding the falling part of gravity, which works fine and then started coding the jumping part.

I used controller.isGrounded to find out if the CharacterController had contact to the ground, therefore enabling it to jump, but for some reason it seems to “oscillate” so to speak. What I mean by that is that sometimes controller.isGrounded returns false, even though it is sitting on the ground. Interestingly enough, Physics.RayCast seems to produce a much more reliable result.

Can someone explain why that is? Is it actually faulty or is it me expecting the wrong things out of controller.isGrounded? I’d like to know so I can improve and maybe correct expectations towards the usage of controller.isGrounded.

If you want a more thorough explanation of the code, feel free to message me and/or post in the thread!

using UnityEngine;
using System.Collections;

public class ControlScript : MonoBehaviour {

    public float maxSpeed = 0.15f;
    public float acceleration = 1;
    public float momentum = 0;
    public float horizontalMomentum = 0;
    public float jumpHeight = 0.15f;
    public float verticalMomentum = 0;
    public int rotationSpeed = 36;
    public float hitscanLength = 1.6f; //Length of collision hitscan Ray
    public bool isGrounded; //To display isGrounded status in Inspector
    CharacterController controller;

    public Vector3 movementVector;

    // Use this for initialization
    void Start () {

    controller = GetComponent<CharacterController>();
   
    }
   
    // Update is called once per frame
    void FixedUpdate ()
    {
        isGrounded = controller.isGrounded; //Use this to check controller.isGrounded in Inspector
        //isGrounded = Physics.Raycast(this.transform.position, Vector3.down, hitscanLength, 1); //Use this to check using RayCast

        /*-- Depth Movement --*/
        if(Input.GetKey(KeyCode.W))
        {   
            momentum += ((1 * acceleration) * Time.deltaTime);
            momentum = Mathf.Clamp(momentum, 0, maxSpeed);
        }
        else if(Input.GetKey(KeyCode.S))
        {
            momentum -= ((1 * acceleration) * Time.deltaTime);
            momentum = Mathf.Clamp(momentum, -maxSpeed, 0);
        }
        else
        {
            if(momentum < 0)
            {
                momentum += ((1 * acceleration) * Time.deltaTime);
                //momentum = Mathf.Clamp(momentum, 0, 0);
                if(momentum > -0.002f)
                {
                    momentum = 0;
                }
            }
            if(momentum > 0)
            {
                momentum -= ((1 * acceleration) * Time.deltaTime);
                //momentum = Mathf.Clamp(momentum, 0, 0);
                if(momentum < 0.002f)
                {
                    momentum = 0;
                }
            }
        }

        /*-- Horizontal Movement -- */
        if(Input.GetKey(KeyCode.A))
        {   
            horizontalMomentum -= ((1 * acceleration) * Time.deltaTime);
            horizontalMomentum = Mathf.Clamp(horizontalMomentum, -maxSpeed, 0);
        }
        else if(Input.GetKey(KeyCode.D))
        {
            horizontalMomentum += ((1 * acceleration) * Time.deltaTime);
            horizontalMomentum = Mathf.Clamp(horizontalMomentum, 0, maxSpeed);
        }
        else
        {
            if(horizontalMomentum < 0)
            {
                horizontalMomentum += ((1 * acceleration) * Time.deltaTime);
                if(horizontalMomentum > -0.002f)
                {
                    horizontalMomentum = 0;
                }
            }
            if(horizontalMomentum > 0)
            {
                horizontalMomentum -= ((1 * acceleration) * Time.deltaTime);
                if(horizontalMomentum < 0.002f)
                {
                    horizontalMomentum = 0;
                }
            }
        }

        /*-- Gravity and Jumping --*/
        if(isGrounded) //If the character controller is on the ground...
        {
            verticalMomentum = 0; //... kill the vertical Momentum.
            if(Input.GetKeyDown(KeyCode.Space))  //If the user presses space...
            {
                verticalMomentum = .45f;  //Apply verticalMomentum...
            }
        }
        else  //If in freefall...
        {
            verticalMomentum += ((Physics.gravity.y * Time.deltaTime) / 10); //... Apply gravity acceleration...
            verticalMomentum = Mathf.Clamp(verticalMomentum, -.85f, 255); //... but clamp it at a certain speed.
        }

        //Debug.DrawLine(this.transform.position, new Vector3(this.transform.position.x, this.transform.position.y - hitscanLength, this.transform.position.z), Color.red, 0, false);

        movementVector = new Vector3(horizontalMomentum, verticalMomentum, momentum);
        controller.Move(movementVector);
    }
}
1 Like

I have the same problem. From what i heard it has to do with the sequence you use the test and the movement. There is another way to check colisions without raycasting try this : Unity - Scripting API: CharacterController.collisionFlags I will try it right now and post results.

Im getting the same behaviour with the collisionflags. mabe its the same method.

I’ve actually forgotten about this thread! It was actually bad code.
If you set your y (assuming downward gravity) velocity to zero, Unity, so to speak, “forgets” that it’s touching the ground. What you want to do is to set your Y velocity to the either your gravity times DeltaTime/FixedDeltaTime, to the controller’s skinWidth times DT/FDT. This thread and this post in particular were very helpful.

This is a top Google search result so I am going to add a bit more.

The above statement proved true for me: “If you set your y (assuming downward gravity) velocity to zero, Unity, so to speak, “forgets” that it’s touching the ground.”

The problem is that I could not avoid setting the y velocity to zero, so I needed another solution.

For me, this was causing issues when the player slowly approached a cliff/edge. As the “shoulder” of the capsule collider slowly goes down the edge/cliff the Character Controller is saying it is losing the grounded state for brief periods of time. This ended up firing falling animation trigger events which was awful.

I serialized a field and started counting how long the player was in the “not grounded” state.

private void ProcessGravity()
    {
        playerVelocity.y += gravityValue * Time.deltaTime;
        controller.Move( playerVelocity * Time.deltaTime );

        playerVelY = playerVelocity.y;
    }

    private void UpdateGroundedState()
    {
        // the below is used to prevent a fall event from triggering when
        // the player falls very tiny distances; this happens a lot when
        // the player slowly approaches a ledge

        // transitioning from grounded to not grounded
        if ( groundedPlayer && controller.isGrounded == false )
        {
            notGroundedTime += Time.deltaTime;
        }
        // previously grounded and still grounded
        else if ( !groundedPlayer && controller.isGrounded == false )
        {
            notGroundedTime += Time.deltaTime;
        }
        // transitioning from not grounded to grounded
        else if ( !groundedPlayer && controller.isGrounded == true )
        {
            notGroundedTime = 0.0f;
        }

        groundedPlayer = controller.isGrounded;
        if ( groundedPlayer && playerVelocity.y < 0 )
        {
            playerVelocity.y = 0.0f;
        }
    }

This shows how I am applying gravity and handling the grounded state. This is the Unity Character Controller example code modified quite a bit.

So basically what I do is count in seconds how long the playing is falling for and only fire a fall animation trigger/event if the player has been falling for a minimum amount of time. This fixed my issue.

    private void ProcessFalling()
    {
        if ( !groundedPlayer && notGroundedTime >= minFallTime )
        {
            heightBeforeDrop = this.transform.position.y;
            characterAnimator.SetTrigger( "fallStart" );
            playerMotionState = MotionState.fallStart;
        }
    }
2 Likes

Try setting minMoveDistance to 0… Docs say “In most situations this value should be left at 0” but they still made the default value 0.001 which also breaks isGrounded… Wasted so much time on this.

2 Likes

Still facing the same issue :frowning:

This concept of having a timer worked perfectly for me! I was getting crazy

1 Like