The way I like to solve this is to use a dynamic Rigidbody
, and give my character control script a notion of "traction" - the maximum force they can exert against the ground to alter their movement.
When traction is high, we can control the movement very precisely, like using a CharacterContoller
. Imagine the character is wearing sturdy cleats with a planted stance so they can resist attempts to push them around.
External influences can still be used to modify this traction where we want to: for instance, we can check the material of the collider under the controller and use it to lower the current traction if, say, we're standing on slippery ice.
Or if the character is hit, we can put them into a stumbling state where they have reduced traction for a period of time, or until they drop below a certain speed, allowing strong hits to send them sliding/flying like a regular dynamic object.
The heart of this is a helper method something like this:
void PushToward(Vector3 targetVelocity, float maxStartingForce, float maxStoppingForce) {
Vector3 deltaV = targetVelocity - rigidbody.velocity;
Vector3 force = rigidbody.mass * deltaV / Time.deltaTime;
// TODO: You may want to remove the component of force perpendicular to the
// terrain, so that we can only push ourselves parallel to the ground.
float forceLimit = Vector3.Dot(deltaV, rigidbody.velocity) < 0f ?
maxStoppingForce : maxStartingForce;
force = Vector3.ClampMagnitude(force, forceLimit);
rigidbody.AddForce(force);
}
Here we pass the velocity our character "wants" to have based on the player's input, but modulate that want according to a limiting force value (you could also parametrize these as accelerations instead of forces, to take the mass out of the equation). That way our control script respects some physical limits of how much "torque" our character's engine has, how much grip they have on the ground, how hard they can push against obstacles/etc. In contrast to a CharacterContoller
that will always go exactly to the destination position, stopping only for collisions.
Character control generally feels best if you have a small window of acceleration when starting to move, but a very hard brake when releasing the controls or changing direction. That's why I like to use separate limits for "starting" and "stopping" forces - the latter can be much higher to keep the controls feeling tight and responsive, without making the character look too jerky.
These two force parameters become our avenue for managing our character's traction. When standing on ice, or when the character is in a stumbling/ragdoll state, (which we can trigger based on relative velocity/impulse of a collision inside OnCollisionEnter
), we can scale both these force limits down to a small multiple, so the character exerts less and less control over their movement. Maybe even 0 to send them skating like a hockey puck. Then we can bring that multiplier back up to 1 when they're on solid ground or when they regain their footing after a knock back.
This gives us a controllable blend between pure rigid body dynamics-driven behaviour, and the tight control of a character controller, and we can dial the behaviour anywhere along that spectrum as the situation demands.