How to correctly setup 3d Character Movement in Unity?

Hey there, so I’m a fairly new user on Unity and just started to test out the engine and C#.

I have a question regarding Character Movement in Unity.

I’ve used this specific code found from the Documentation:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Example : MonoBehaviour
{
   private CharacterController controller;
   private Vector3 playerVelocity;
   private bool groundedPlayer;
   private float playerSpeed = 2.0f;
   private float jumpHeight = 1.0f;
   private float gravityValue = -9.81f;

    private void Start()
   {
       controller = gameObject.AddComponent<CharacterController>();
   }

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

        Vector3 move = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));
       controller.Move(move * Time.deltaTime * playerSpeed);

        if (move != Vector3.zero)
       {
           gameObject.transform.forward = move;
       }

        // Changes the height position of the player..
       if (Input.GetButtonDown("Jump") && groundedPlayer)
       {
           playerVelocity.y += Mathf.Sqrt(jumpHeight * -3.0f * gravityValue);
       }

        playerVelocity.y += gravityValue * Time.deltaTime;
       controller.Move(playerVelocity * Time.deltaTime);
   }
}

The movement works just fine; however, the jumping absolutely barely works. Sometimes it does and sometimes it does not jump. Could be a cooldown or something, not sure, I’m still a fairly new user.

Any tips or fixes for this?

Welcome!

First, please use code tags: Using code tags properly when you post code.

But secondly, what is weird is that this is the second time recently that I’ve seen this code reported broken.

I went and investigated and what appears to have happened is that Unity’s example used to work back on earlier versions of Unity.

BUT… on more recent Unity versions, I concur with you that it presently does NOT work reliably, at least for jumping.

If I had to guess why, I believe it is their two separate calls to .Move() interfering with ground sensing in some way.

I have fixed the script here, and for me it is now 100% reliable, plus I fixed the jump height calculations, and let you jump reliably coming down ramps. See script below. Let me know if it works for you. You’ll be the second person running it after me. :slight_smile:

I will report it broken on their website, and meanwhile, enclosed is a fully-operational package including a jumpable miniature scene to get you started.

The main fixes I made are:

  • to only call .Move() once per frame, with a fully-fleshed out velocity instead of twice the way you are above (and the way the Unity sample shows)

  • to let “on ground” linger momentarily after you are not on the ground, to facilitate jumping even when you are “stumbling” down a ramp, which the original code failed on.

Meanwhile, ENJOY!

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// Originally from Unity examples at:
// https://docs.unity3d.com/ScriptReference/CharacterController.Move.html
//
// 3:55 PM 10/3/2020
//
// Reworked by @kurtdekker so that it jumps reliably in modern Unity versions.
//
// To use:
//    - make your player shape about 1x2x1 in size
//    - put this script on the root of it
//
// That's it.

public class UnityExampleCharMover : MonoBehaviour
{
    private CharacterController controller;
    private float verticalVelocity;
    private float groundedTimer;        // to allow jumping when going down ramps
    private float playerSpeed = 2.0f;
    private float jumpHeight = 1.0f;
    private float gravityValue = 9.81f;

    private void Start()
    {
        // always add a controller
        controller = gameObject.AddComponent<CharacterController>();
    }

    void Update()
    {
        bool groundedPlayer = controller.isGrounded;
        if (groundedPlayer)
        {
            // cooldown interval to allow reliable jumping even whem coming down ramps
            groundedTimer = 0.2f;
        }
        if (groundedTimer > 0)
        {
            groundedTimer -= Time.deltaTime;
        }

        // slam into the ground
        if (groundedPlayer && verticalVelocity < 0)
        {
            // hit ground
            verticalVelocity = 0f;
        }

        // apply gravity always, to let us track down ramps properly
        verticalVelocity -= gravityValue * Time.deltaTime;

        // gather lateral input control
        Vector3 move = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));

        // scale by speed
        move *= playerSpeed;

        // only align to motion if we are providing enough input
        if (move.magnitude > 0.05f)
        {
            gameObject.transform.forward = move;
        }

        // allow jump as long as the player is on the ground
        if (Input.GetButtonDown("Jump"))
        {
            // must have been grounded recently to allow jump
            if (groundedTimer > 0)
            {
                // no more until we recontact ground
                groundedTimer = 0;

                // Physics dynamics formula for calculating jump up velocity based on height and gravity
                verticalVelocity += Mathf.Sqrt(jumpHeight * 2 * gravityValue);
            }
        }

        // inject Y velocity before we use it
        move.y = verticalVelocity;

        // call .Move() once only
        controller.Move(move * Time.deltaTime);
    }
}

EDIT: to jump continuously whenever you re-touch the ground (i.e., not have to keep spamming Jump), change line 69 above to GetButton() instead of GetButtonDown()

6379746–710721–UnityExampleCharMover.unitypackage (280 KB)

15 Likes

Much appreciated for the help,
I’m still new here and I thought that looking through Unity’s documentation should work just fine, I guess not.

Anyways, I shall try it out and will give you a reply whenever I try it.

Thank you once again.

1 Like

I’ve tested it out and I can confirm that now it works properly.

I hope that I haven’t took much of your time, thank you a lot.

1 Like

You’re welcome! I notified Unity, and I hope they fix the example, because now I have seen three people who have had issues with it in the past week!

1 Like

Hopefully they will.

Hey Kurt, Could you please help me out here, I am trying to integrate this into the New Input Manager System and that requires me to modify it obviously, so I have but the only issue is that I can’t get the character to Jump, it keeps giving me a Null reference. Is there any way you could please help me with this code, I would greatly appreciate it, also, since this is the new input system in Unity, I am sure this question may arise a few times in the near future. Thanks A Million.

NullReferenceException: Object reference not set to an instance of an object
CharacterControllerJump.Update () (at Assets/CharacterControllerJump.cs:60)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;

public class CharacterControllerJump : MonoBehaviour
{
    // Pulls in the Character Controller and sets all the parameters for the character controller
   
    public AudioSource jumpSound;
    public bool isGrounded1;
    public float NumberJumps = 0f;
    public float MaxJumps = 2;
    public float gravityValue = 10f;
    public float jumpHeight = 10.0f;
    private CharacterController controller;
    private float verticalVelocity;
    private float playerSpeed = 2.0f;

    //The Main Name of my Action Map Asset  //// and rhen a Generic Name I gave for this script, it can be anything. 
    XRIDefaultInputActions input_Actions;



    void update()
    {
        // slam into the ground
        if (isGrounded1 && verticalVelocity < 0)
        {
            // hit ground
            verticalVelocity = 0f;
        }

        // apply gravity always, to let us track down ramps properly
        verticalVelocity -= gravityValue * Time.deltaTime;

        // gather lateral input control
        Vector3 move = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));

        // scale by speed
        move *= playerSpeed;

        // only align to motion if we are providing enough input
        if (move.magnitude > 0.05f)
        {
            gameObject.transform.forward = move;
        }

        // inject Y velocity before we use it
        move.y = verticalVelocity;
        // call .Move() once only
        controller.Move(move * Time.deltaTime);


    }
    private void Update()
    {
        Vector3 move = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));
        move.y = verticalVelocity;
        controller.Move(move * Time.deltaTime);
        // scale by speed
        move *= playerSpeed;

        if (move.magnitude > 0.05f)
        {
            gameObject.transform.forward = move;
        }
    }
    private void Start()
    {
        controller = gameObject.AddComponent<CharacterController>();
    }

    private void Awake()
    {
        //Generic Name from above = new "Name of my Action Map Asset"
        input_Actions = new XRIDefaultInputActions();
        input_Actions.XRIRightHand.Jump.performed += x => Jump();
       
    }

    //This calls the Jump function from my Action Map Asset. 
    private void Jump()
    {

        if (isGrounded1)
        {
            //plays a Jump Sound
            jumpSound.Play();
            Debug.Log("Player is Grounded");
            //To test if Button is being heard by the New Input System
            Debug.Log("I AM JUMPING BITCH!!!");
            verticalVelocity += Mathf.Sqrt(jumpHeight * 2 * gravityValue);

        }

       
    }


    void OnCollisionEnter(Collision other)
    {





        isGrounded1 = true;
        NumberJumps = 0;

    }

    void OnCollisionExit(Collision other)
    {

    }

    //This Enables and Disables the Method. 

    #region - Enable/Disable -

    private void OnEnable()
    {
        input_Actions.Enable();
    }

    private void OnDisable()
    {
        input_Actions.Disable();
    }

    #endregion
}

The answer is always the same… ALWAYS. It is the single most common error ever.

Don’t waste your life spinning around and round on this error. Instead, learn how to fix it fast… it’s EASY!!

Some notes on how to fix a NullReferenceException error in Unity3D

  • also known as: Unassigned Reference Exception
  • also known as: Missing Reference Exception
  • also known as: Object reference not set to an instance of an object

http://plbm.com/?p=221

The basic steps outlined above are:

  • Identify what is null
  • Identify why it is null
  • Fix that.

Expect to see this error a LOT. It’s easily the most common thing to do when working. Learn how to fix it rapidly. It’s easy. See the above link for more tips.

This is the kind of mindset and thinking process you need to bring to this problem:

https://forum.unity.com/threads/why-do-my-music-ignore-the-sliders.993849/#post-6453695

Step by step, break it down, find the problem.

1 Like

Hey Kurt, Could you please help me out here, I am trying to integrate this into the New Input Manager System and that requires me to modify it obviously, so I have but the only issue is that I can’t get the character to Jump, it keeps giving me a Null reference. Is there any way you could please help me with this code, I would greatly appreciate it, also, since this is the new input system in Unity, I am sure this question may arise a few times in the near future. Thanks A Million.

NullReferenceException: Object reference not set to an instance of an object
CharacterControllerJump.Update () (at Assets/CharacterControllerJump.cs:60)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;

public class CharacterControllerJump : MonoBehaviour
{
    // Pulls in the Character Controller and sets all the parameters for the character controller
 
    public AudioSource jumpSound;
    public bool isGrounded1;
    public float NumberJumps = 0f;
    public float MaxJumps = 2;
    public float gravityValue = 10f;
    public float jumpHeight = 10.0f;
    private CharacterController controller;
    private float verticalVelocity;
    private float playerSpeed = 2.0f;

    //The Main Name of my Action Map Asset  //// and rhen a Generic Name I gave for this script, it can be anything.
    XRIDefaultInputActions input_Actions;



    void update()
    {
        // slam into the ground
        if (isGrounded1 && verticalVelocity < 0)
        {
            // hit ground
            verticalVelocity = 0f;
        }

        // apply gravity always, to let us track down ramps properly
        verticalVelocity -= gravityValue * Time.deltaTime;

        // gather lateral input control
        Vector3 move = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));

        // scale by speed
        move *= playerSpeed;

        // only align to motion if we are providing enough input
        if (move.magnitude > 0.05f)
        {
            gameObject.transform.forward = move;
        }

        // inject Y velocity before we use it
        move.y = verticalVelocity;
        // call .Move() once only
        controller.Move(move * Time.deltaTime);


    }
    private void Update()
    {
        Vector3 move = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));
        move.y = verticalVelocity;
        controller.Move(move * Time.deltaTime);
        // scale by speed
        move *= playerSpeed;

        if (move.magnitude > 0.05f)
        {
            gameObject.transform.forward = move;
        }
    }
    private void Start()
    {
        controller = gameObject.AddComponent<CharacterController>();
    }

    private void Awake()
    {
        //Generic Name from above = new "Name of my Action Map Asset"
        input_Actions = new XRIDefaultInputActions();
        input_Actions.XRIRightHand.Jump.performed += x => Jump();
      
    }

    //This calls the Jump function from my Action Map Asset.
    private void Jump()
    {

        if (isGrounded1)
        {
            //plays a Jump Sound
            jumpSound.Play();
            Debug.Log("Player is Grounded");
            //To test if Button is being heard by the New Input System
            Debug.Log("I AM JUMPING BITCH!!!");
            verticalVelocity += Mathf.Sqrt(jumpHeight * 2 * gravityValue);

        }

      
    }


    void OnCollisionEnter(Collision other)
    {





        isGrounded1 = true;
        NumberJumps = 0;

    }

    void OnCollisionExit(Collision other)
    {

    }

    //This Enables and Disables the Method.

    #region - Enable/Disable -

    private void OnEnable()
    {
        input_Actions.Enable();
    }

    private void OnDisable()
    {
        input_Actions.Disable();
    }

    #endregion
}

Please stop responding to the same old thread with unrelated nullref issues. START YOUR OWN NEW TOPIC.

I linked above how to fix a nullref. Nullrefs are NOT related to this post. Nullrefs do NOT need a post at all. See the above for how to fix them.

Hello @Kurt-Dekker , I am trying to implement your script, but as a result of the script, when falling down I fall too fast, much faster as I actually should with gravity. Did you experience also this behavior? You also mentioned that you have reported it to Unity. Have you opened a bug for that? Would by nice if Unity could solve the issue in the first place.
Thank you, Huxi

Can you run “Controller.Move” and "Controller.SimpleMove();" in the same frame?

Don’t.

1 Like

I can indeed confirm, DON’T!

You should check it, It’s easy to find problems by looking through your code.

Also, if you want movement here’s Unity’s movement code for fps:

using UnityEngine;
#if ENABLE_INPUT_SYSTEM
using UnityEngine.InputSystem;
#endif

namespace StarterAssets
{
[RequireComponent(typeof(CharacterController))]
#if ENABLE_INPUT_SYSTEM
[RequireComponent(typeof(PlayerInput))]
#endif
public class FirstPersonController : MonoBehaviour
{
[Header(“Player”)]
[Tooltip(“Move speed of the character in m/s”)]
public float MoveSpeed = 4.0f;
[Tooltip(“Sprint speed of the character in m/s”)]
public float SprintSpeed = 6.0f;
[Tooltip(“Rotation speed of the character”)]
public float RotationSpeed = 1.0f;
[Tooltip(“Acceleration and deceleration”)]
public float SpeedChangeRate = 10.0f;

[Space(10)]
[Tooltip(“The height the player can jump”)]
public float JumpHeight = 1.2f;
[Tooltip(“The character uses its own gravity value. The engine default is -9.81f”)]
public float Gravity = -15.0f;

[Space(10)]
[Tooltip(“Time required to pass before being able to jump again. Set to 0f to instantly jump again”)]
public float JumpTimeout = 0.1f;
[Tooltip(“Time required to pass before entering the fall state. Useful for walking down stairs”)]
public float FallTimeout = 0.15f;

[Header(“Player Grounded”)]
[Tooltip(“If the character is grounded or not. Not part of the CharacterController built in grounded check”)]
public bool Grounded = true;
[Tooltip(“Useful for rough ground”)]
public float GroundedOffset = -0.14f;
[Tooltip(“The radius of the grounded check. Should match the radius of the CharacterController”)]
public float GroundedRadius = 0.5f;
[Tooltip(“What layers the character uses as ground”)]
public LayerMask GroundLayers;

[Header(“Cinemachine”)]
[Tooltip(“The follow target set in the Cinemachine Virtual Camera that the camera will follow”)]
public GameObject CinemachineCameraTarget;
[Tooltip(“How far in degrees can you move the camera up”)]
public float TopClamp = 90.0f;
[Tooltip(“How far in degrees can you move the camera down”)]
public float BottomClamp = -90.0f;

// cinemachine
private float _cinemachineTargetPitch;

// player
private float _speed;
private float _rotationVelocity;
private float _verticalVelocity;
private float _terminalVelocity = 53.0f;

// timeout deltatime
private float _jumpTimeoutDelta;
private float _fallTimeoutDelta;

#if ENABLE_INPUT_SYSTEM
private PlayerInput _playerInput;
#endif
private CharacterController _controller;
private StarterAssetsInputs _input;
private GameObject _mainCamera;

private const float _threshold = 0.01f;

private bool IsCurrentDeviceMouse
{
get
{
#if ENABLE_INPUT_SYSTEM
return _playerInput.currentControlScheme == “KeyboardMouse”;
#else
return false;
#endif
}
}

private void Awake()
{
// get a reference to our main camera
if (_mainCamera == null)
{
_mainCamera = GameObject.FindGameObjectWithTag(“MainCamera”);
}
}

private void Start()
{
_controller = GetComponent();
_input = GetComponent();
#if ENABLE_INPUT_SYSTEM
_playerInput = GetComponent();
#else
Debug.LogError( “Starter Assets package is missing dependencies. Please use Tools/Starter Assets/Reinstall Dependencies to fix it”);
#endif

// reset our timeouts on start
_jumpTimeoutDelta = JumpTimeout;
_fallTimeoutDelta = FallTimeout;
}

private void Update()
{
JumpAndGravity();
GroundedCheck();
Move();
}

private void LateUpdate()
{
CameraRotation();
}

private void GroundedCheck()
{
// set sphere position, with offset
Vector3 spherePosition = new Vector3(transform.position.x, transform.position.y - GroundedOffset, transform.position.z);
Grounded = Physics.CheckSphere(spherePosition, GroundedRadius, GroundLayers, QueryTriggerInteraction.Ignore);
}

private void CameraRotation()
{
// if there is an input
if (_input.look.sqrMagnitude >= _threshold)
{
//Don’t multiply mouse input by Time.deltaTime
float deltaTimeMultiplier = IsCurrentDeviceMouse ? 1.0f : Time.deltaTime;

_cinemachineTargetPitch += _input.look.y * RotationSpeed * deltaTimeMultiplier;
_rotationVelocity = _input.look.x * RotationSpeed * deltaTimeMultiplier;

// clamp our pitch rotation
_cinemachineTargetPitch = ClampAngle(_cinemachineTargetPitch, BottomClamp, TopClamp);

// Update Cinemachine camera target pitch
CinemachineCameraTarget.transform.localRotation = Quaternion.Euler(_cinemachineTargetPitch, 0.0f, 0.0f);

// rotate the player left and right
transform.Rotate(Vector3.up * _rotationVelocity);
}
}

private void Move()
{
// set target speed based on move speed, sprint speed and if sprint is pressed
float targetSpeed = _input.sprint ? SprintSpeed : MoveSpeed;

// a simplistic acceleration and deceleration designed to be easy to remove, replace, or iterate upon

// note: Vector2’s == operator uses approximation so is not floating point error prone, and is cheaper than magnitude
// if there is no input, set the target speed to 0
if (_input.move == Vector2.zero) targetSpeed = 0.0f;

// a reference to the players current horizontal velocity
float currentHorizontalSpeed = new Vector3(_controller.velocity.x, 0.0f, _controller.velocity.z).magnitude;

float speedOffset = 0.1f;
float inputMagnitude = _input.analogMovement ? _input.move.magnitude : 1f;

// accelerate or decelerate to target speed
if (currentHorizontalSpeed < targetSpeed - speedOffset || currentHorizontalSpeed > targetSpeed + speedOffset)
{
// creates curved result rather than a linear one giving a more organic speed change
// note T in Lerp is clamped, so we don’t need to clamp our speed
_speed = Mathf.Lerp(currentHorizontalSpeed, targetSpeed * inputMagnitude, Time.deltaTime * SpeedChangeRate);

// round speed to 3 decimal places
_speed = Mathf.Round(_speed * 1000f) / 1000f;
}
else
{
_speed = targetSpeed;
}

// normalise input direction
Vector3 inputDirection = new Vector3(_input.move.x, 0.0f, _input.move.y).normalized;

// note: Vector2’s != operator uses approximation so is not floating point error prone, and is cheaper than magnitude
// if there is a move input rotate player when the player is moving
if (_input.move != Vector2.zero)
{
// move
inputDirection = transform.right * _input.move.x + transform.forward * _input.move.y;
}

// move the player
_controller.Move(inputDirection.normalized * (_speed * Time.deltaTime) + new Vector3(0.0f, _verticalVelocity, 0.0f) * Time.deltaTime);
}

private void JumpAndGravity()
{
if (Grounded)
{
// reset the fall timeout timer
_fallTimeoutDelta = FallTimeout;

// stop our velocity dropping infinitely when grounded
if (_verticalVelocity < 0.0f)
{
_verticalVelocity = -2f;
}

// Jump
if (_input.jump && _jumpTimeoutDelta <= 0.0f)
{
// the square root of H * -2 * G = how much velocity needed to reach desired height
_verticalVelocity = Mathf.Sqrt(JumpHeight * -2f * Gravity);
}

// jump timeout
if (_jumpTimeoutDelta >= 0.0f)
{
_jumpTimeoutDelta -= Time.deltaTime;
}
}
else
{
// reset the jump timeout timer
_jumpTimeoutDelta = JumpTimeout;

// fall timeout
if (_fallTimeoutDelta >= 0.0f)
{
_fallTimeoutDelta -= Time.deltaTime;
}

// if we are not grounded, do not jump
_input.jump = false;
}

// apply gravity over time if under terminal (multiply by delta time twice to linearly speed up over time)
if (_verticalVelocity < _terminalVelocity)
{
_verticalVelocity += Gravity * Time.deltaTime;
}
}

private static float ClampAngle(float lfAngle, float lfMin, float lfMax)
{
if (lfAngle < -360f) lfAngle += 360f;
if (lfAngle > 360f) lfAngle -= 360f;
return Mathf.Clamp(lfAngle, lfMin, lfMax);
}

private void OnDrawGizmosSelected()
{
Color transparentGreen = new Color(0.0f, 1.0f, 0.0f, 0.35f);
Color transparentRed = new Color(1.0f, 0.0f, 0.0f, 0.35f);

if (Grounded) Gizmos.color = transparentGreen;
else Gizmos.color = transparentRed;

// when selected, draw a gizmo in the position of, and matching radius of, the grounded collider
Gizmos.DrawSphere(new Vector3(transform.position.x, transform.position.y - GroundedOffset, transform.position.z), GroundedRadius);
}
}
}

I have also reported a bug related to this issue. Bug IN-90305. I’ve done some research into this and it appears to be due to the isGrounded flag not getting set during a call to Move. As it notes in the documentation, the move won’t happen if the move distance is less than the minMoveDistance amount. The default minMoveDistance in a newly created CharacterController component is 0.001.

If the player isn’t otherwise moving, the example code gives it a downward movement equal to down velocity * delta time. Where the velocity is incremented by gravity * delta time each frame. This means that a Move will be made depending on the framerate of the playback. In the editor, the playback typically runs as fast as it can and so the frame rate is likely over 100 FPS.

At 60 FPS, the delta time should be 0.016 so frame by frame it reaches the threshold easily in one frame

frame velocity movement
1 0.16 0.0025

But at 100 FPS it would reach the threshold only if the frame time was slightly over the target 0.01 seconds. In practice, sometimes the grounded flag will be set, and sometimes not:

frame velocity movement
1 0.098 0.000981
2 0.20 0.0020

At 120 FPS it will fairly regularly be set on every other frame. You have a 50% chance of triggering a jump at this frame rate.

frame velocity movement
1 0.082 0.00068
2 0.16 0.0014

The higher the frame rate, the less likely it is the grounded flag will be set and the less likely you will be able to trigger the jump.

This can be fixed by setting the minMoveDistance to 0. It is actually noted in the documentation that 0 is probably the best value to use. However, when a new component of this type is created, it always defaults to 0.001.

(These figures are as in version 6000.0.24)

Looks like a problem where you just should put movement in:
FixedUpdate() and input still in Update()

Though it potentially can give a little bit of inertia, cause moving will be fixed(to physics?).

I’m not an expert, but seen many things related to movement etc. were put to FixedUpdate() to remove the frame binding.
Don’t do that to Lerps though, if you’re holding an object and it follows the target position, for example.

Putting the Move calls in FixedUpdate should work, so long as the physics update rate is slow enough. That is, as long as fixedDeltaTime > \sqrt{\frac{minMoveDistance}{gravity}}. With the default minMoveDistance of 0.001, gravity of 9.81, and fixedDeltaTime of 0.02 it will work. But with fixedDeltaTime values of 0.01 or lower you will have the same problem.

Setting minMoveDistance to 0 (as is recommended in multiple places in the documentation) fixes the issue whether you are using Update or FixedUpdate and regardless of deltaTime.

However, I wouldn’t recommend putting Move calls into FixedUpdate as that is reserved for updating forces in rigid bodies. That is, FixedUpdate is for applying forces to attached Rigidbody components. Although the CharacterController does perform collision tests during Move calls, it doesn’t use forces and has it’s own separate callback to allow it to push rigid bodies around.

It looks like this is going to be fixed in the documentation :crossed_fingers:

developers are already aware of the unexpected behaviour of the isGrounded property and have plans to update the ‘CharacterController.Move’ documentation. It is known that setting the minMoveDistance = 0 fixes the issue (as you mentioned)