CharacterController.IsGrounded returning unreliable state

Hi!

After invalid normals and intersection points returned by standing collisions threw a spanner into my handmade swept character controller implementation, I tried to make do with the one provided by Unity.

However, I want the character controller to influence my character’s velocity as well, so I built my movement logic like this:

protected void FixedUpdate() {
  this.Velocity += Physics.gravity * Time.fixedDeltaTime;
  Vector3 translation = this.Velocity * Time.fixedDeltaTime;
  this.characterController.Move(translation);

  // The 'velocity' property is already descaled via fixedDeltaTime O_o
  this.Velocity = this.characterController.velocity;
  this.IsGrounded = this.characterController.isGrounded;
}

This works nicely when moving on flat ground, but on slopes it will a) move smoothly downward yet report IsGrounded as false, b) cause sporadic bounces upwards and report IsGrounded as false.

Here’s an example:

3216183--246346--inconsistent-isgrounded.png

There’s a slope, high to the left and low to the right. The character is moving left (X = -9, Y = 2.6), its movement vector points slightly into the slope. The CharacterController then reports the velocity as (X = -9, Y = 3.1), clearly showing that it had to adjust the translation due to hitting the slope. Yet IsGrounded is false.

If I increase gravity (making the translation vector point further into the ground) the CharacterController starts to work as expected.

Why is this?

I have one workaround, which is to separate the horizontal and vertical parts of the movement, but this requires me to call Move() two times:

/// <summary>Applies the force of gravity to the actor</summary>
public void ApplyGravity(Vector3 gravity) {
  // Times mass b/c w/o friction, a feather falls as fast as a lead weight!
  QueueForce(gravity * this.Mass * this.GravityScale);
}

/// <summary>Called once per physics frame</summary>
protected virtual void FixedUpdate() {
  float delta = Time.fixedDeltaTime;

  if(this.IsAffectedByGravity) {
    ApplyGravity(Gravity);
  }
 
  // Calculate the movement of the game object in this physics frame
  Vector3 translation;
  {

    // Apply force scaled by time
    Vector3 acceleration = this.queuedForces / this.Mass;
    this.Velocity += acceleration * delta;

    // Impulses go directly into velocity
    this.Velocity += this.queuedImpulses / this.Mass;

    // Velocity is scaled by time, but queued movements are added unscaled
    translation = this.Velocity * delta;
    translation += this.queuedMovement;

  }

  // Here's the annoying part: in Unity 5, the CharacterController does not reliably
  // report being grounded even if it is driven downward relative to the ground angle.
  //
  // For example, moving /into/ a slope (velocity = -9, 2.61) while then reporting
  // being deflected by the slope (velocity = -8.78, 3.19) it still claims to not
  // be grounded
  //
  // We solve this by doing the movement in two parts which appears robust so far.
  Vector3 reportedVelocity;
  moveActor(translation, out this.IsGrounded, out reportedVelocity);

  // Update the velocity. This is filtered so that small errors will not accumulate,
  // like when moving at a speed of 5.0 up a slope and the movement logic says that
  // the character only moved 4.99 units, getting slower every cycle.
  updateVelocity(reportedVelocity, updateHorizontalVelocity: true);

  // Reset the queues
  this.queuedForces = Vector3.zero;
  this.queuedImpulses = Vector3.zero;
  this.queuedMovement = Vector3.zero;
}

/// <summary>
///   Moves the actor by the specified amount (unless blocked by colliders) and
///   provides the new grounded state and actual velocity
/// </summary>
/// <param name="translation">Amount by which the actor will be moved</param>
/// <param name="isGrounded">Whether the actor is now grounded</param>
/// <param name="actualVelocity">Actual velocity the actor is moving with</param>
/// <remarks>
///   If the actor hits a wall, the reported velocity will change.
/// </remarks>
private void moveActor(Vector3 translation, out bool isGrounded, out Vector3 actualVelocity) {

  // We first separate the horizontal and vertical movement
  Vector3 horizontalTranslation = new Vector3(translation.x, 0.0f, translation.z);
  Vector3 verticalTranslation = new Vector3(0.0f, translation.y, 0.0f);

  // Then, for positive Y velocities, we first move up and then sideways so that
  // the move can not be interpreted as "hovering slightly above the ground" and
  // for negative Y velocities, we first move sideways and then down so the final
  // bit of movement will be "into the ground" with a definite collision
  //
  // If the ground is touched at any of the two movements, we consider the actor
  // to be grounded. This is
  if(translation.y >= 0.0f) {
    this.characterController.Move(verticalTranslation);
    actualVelocity = new Vector3(0.0f, this.characterController.velocity.y, 0.0f);
    isGrounded = this.characterController.isGrounded;

    this.characterController.Move(horizontalTranslation);
    actualVelocity += this.characterController.velocity;
    isGrounded |= this.characterController.isGrounded;
  } else {
    this.characterController.Move(horizontalTranslation);
    actualVelocity = this.characterController.velocity;
    isGrounded = this.characterController.isGrounded;

    this.characterController.Move(verticalTranslation);
    actualVelocity.y += this.characterController.velocity.y;
    isGrounded |= this.characterController.isGrounded;
  }

}

The CharacterController’s IsGrounded state is robust this way, but I’m not happy wrapping the CharacterController in layers and layers of code to fix and sanitize its outputs.

It sounds to me like the isGrounded check is using a raycast of length relating to gravity

Are you wanting to know more about the behind-the-scenes work, or do you want a workaround? Can you use collision detection routines to set your own grounded flag?

I’m both interested in knowing how the character controller works internally (i.e. how can IsGrounded be false after a call to Move() that clearly had to adjust the position b/c of ground collision) and in hearing how others avoided the problem.

Normally I’d just look at the source via ILspy, but the CharacterController is just a thin wrapper around a native implementation of its functionality.

I can use my own raycasts with a bit of extra tolerance, but I’m still wondering why Unity’s implementation of the CharacterController shows such unreliability. I found many reports of similar issues (sometimes caused by user error, but leaving enough cases were the CharacterController is actually at fault):

I could understand if the CharacterController simply checked if the last call to Move() resulted in ground contact and moving exactly parallel to the ground (like, {1, 0, 0} along a flat surface) would result in random flip-flopping of the isGrounded property since, depending on rounding errors, the character could as well be hovering at a tiny distance above the ground (and indeed, aforementioned user error generally comes from not applying gravity when grounded), but in my case, I apply always gravity, yet all bets are off on slopes - even if the movement vector is pointing slightly into the slope:

3217342--246470--not-grounded.png

If I do the same movement in two steps (first up, then horizontal) to increase the angle at which the CharacterController is driven into the ground, it will report IsGrounded as true:

3217342--246471--is-grounded.png

The Unity example projects seem to suffer the same issues, so likely the CharacterController’s implementation is not robust here.

Are there any bug reports concerning this? That may get better traction with those who can actually do something about it.

I’ve never had the inclination to use the CharacterController, but I do see the appeal. It’s a shame there are so many problems surrounding it that have gone unaddressed :face_with_spiral_eyes:

I’m not aware of any bug reports. It does look like something was changed between Unity 4 and Unity 5, though, as many problem threads state that the grounding issues appeared when upgrading their project from Unity 4.

If I may ask, what are you using instead?

Before this, I built a prototype on Unity’s Rigidbody physics, but ran into non-trivial problems keeping consistent speed up- & downhills as well as preventing slipping on slopes.

I’ve also taken a look at third-party options (i.e. Super Character Controller which uses stacked spheres instead of a capsule or Corgi Engine which uses multiple RayCasts along the character’s height) but haven’t found one I like so far :slight_smile:

I’ve not started a project in Unity that would need to make use of it. All of my toying around with the physics engine for 2D platformer-esque mechanics used simple shapes (a box collider that the visual character would align with). For ground detection, a raycast.

Current project is in VR, using the SteamVR rig as a base and tons and tons of custom code behind it.

Normally I will roll my own character controller, but that’s because I’m use to working without a game engine like Unity. :hushed: