How to avoid CharacterControllers stepOffset launching my character into space?

I’m writing a FPS controller script using CharacterController. I’m trying to keep the momentum of the character and allow some acceleration. To achieve this, I use the ‘velocity’ parameter every frame (see code below)

My problem:

When CharacterController performs an stepOffset elevation, the character is launched into the air.

The problem source:

When the CharacterController performs a stepOffset, its ‘velocity.y’ is set really high. I’m guessing equivalent to the height of the elevation divided by time spent getting there.

Also, stepOffset triggering seems to be hidden well within the CharacterController.

Question:

Is there a way to either subtract the vertical stepOffset velocity from the velocity, or otherwise check if the stepOffset did -something- this frame?

Possible fix #1: I tried Increasing the gravity to decrease the launch height, making it look like the character is simply “jumping onto the ledge”. This is acceptable, as long as the “jumping onto the ledge” is an acceptable behaviour. In my case, I’d like to avoid it.

Possible fix #2: More speed allows the rounded bottom of the charactercontrollers capsule collider to simply glide up from pure geometry alone. This is less than ideal, since most situations in my game will not involve a speedy character.

Possible fix #3: Set stepOffset to zero and write own collision raycast stepOffset functionality. I’m investigating this further tomorrow, unless any bright ideas pop up.

This is the code that runs every frame with Move():

// CharacterController charCon;
// Move towards horisontal input speed
Vector3 v3BodySpeed = charCon.velocity; // PROBLEM: Triggering a stepOffset sets a high y

// Only movetowards horizontal speed
float fSpeedY = v3BodySpeed.y; // save this while temporarily zeroing out vertical speed
v3BodySpeed.y = 0F; // temp
v3BodySpeed = Vector3.MoveTowards(v3BodySpeed, v3InputBodySpeed, fAccelerationSpeed);
v3BodySpeed.y = fSpeedY; // Reinstated vertical speed

// Jumping is instant instead of "MoveTowards"
if(isStartingAJump){
	v3BodySpeed.y += fJumpSpeed;
	isStartingAJump = false;
}

// Apply gravity
v3BodySpeed += Physics.gravity * Time.deltaTime;

// Make it move
charCon.Move(v3BodySpeed * Time.deltaTime);

After testing various solutions (all with bigger issues), I have finally found an adequate solution to the stepOffset vertical velocity issue. One downside is that it removes the ability to suddenly propel the character upwards from external forces (I.e. an explosion).

Since the CharacterController does not expose its stepOffset in any way, shape or form (other than the setting), it feels like I’m creating a solution to a problem that should never have existed in the first place.

The solution:

Each frame, detect the stepOffset and subtract its speed from the momentum.

###Detecting the stepOffset

In my case, there are three reasons a charactercontroller gains a positive vertical velocity:

1 - Walking uphill

2 - Jumping

3 - stepOffset

(4 - other external forces, e.g. an explosion. Ignored, for now)

Walking uphill (1) will usually produce a low vertical velocity.

Jumping (2) can be tracked (isJumped == true , until isGrounded again)

stepOffset (3) produces a very high vertical velocity.

So the solution is to check the change in characterController.velocity from frame to frame, looking for a suddenly high vertical velocity.

Detection of a stepOffset should trigger If the character is not jumping, but the vertical velocity suddenly increased by a ridiculous amount. In my case, a single-frame vertical speed increase of atleast 3 meters per second seemed good enough.

Remember, even small edges may produce a high vertical velocity, because the sudden movement happens in 16-33 milliseconds (1 frame).

###Subtracting the stepOffset speed

Short version is:

newSpeed.y -= detectedStepOffsetSpeed.y

The result is a character that can walk up steps/stairs using stepOffset, and mostly NOT fly up into space. Not perfect, but definitely not bad. Here is the new code.

float stepOffsetThreshold = 3.0F;
private void UpdateBodyPosition()
{
	// Current speed
	Vector3 v3BodySpeed = charCon.velocity;

	// Save a lot of performance when there are many character controllers in the scene
	if(isGrounded == true && isStartingAJump == false && v3BodySpeed.magnitude == 0F && v3InputBodySpeed.magnitude == 0F)
		return; // No movement, and is standing still on the ground.

	// Only movetowards horizontal speed, since jumping is such a delicate creature
	float fSpeedY = v3BodySpeed.y; // save this while temporarily zeroing out vertical speed
	v3BodySpeed.y = 0F; // temp
	v3BodySpeed = Vector3.MoveTowards(v3BodySpeed, v3InputBodySpeed, fAccelerationSpeed);
	v3BodySpeed.y = fSpeedY; // Reinstated vertical speed

	// Try to detect stepOffset vertical speed
	float fDeltaY = v3BodySpeed.y - v3LastBodySpeed.y; // Speed change since last frame
	v3LastBodySpeed = v3BodySpeed;
	if(isJumped == false && fDeltaY > stepOffsetThreshold){
		// IS grounded, and high vertical speed => Probably stepOffset
		// Remove the vertical speed from the momentum persistence
		v3BodySpeed.y -= fDeltaY;
		Debug.Log("StepOffset detected, removed "+fDeltaY+" from vert.speed.");
	}

	// Jumping is instant instead of "MoveTowards"
	if(isStartingAJump == true)
	{
		v3BodySpeed.y += fJumpSpeed;
		isStartingAJump = false;
		isJumped = true;
	}

	// Apply gravity
	if(isGrounded == false)
		v3BodySpeed += Physics.gravity * Time.deltaTime * 2;

	// Make it move
	charCon.Move(v3BodySpeed * Time.deltaTime);

	// Reset input for the next frame
	v3InputBodySpeed = Vector3.zero;
}