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.
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.
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()
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
}
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:
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
}
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
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;
private void Awake()
{
// get a reference to our main camera
if (_mainCamera == null)
{
_mainCamera = GameObject.FindGameObjectWithTag(“MainCamera”);
}
}
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;
// 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;
// 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);
// 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);
}
// 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 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.
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
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)