I am currently trying to create a character controller that will allow the player to slide down slopes while maintaining control of their direction - in other words, a fairly simple snowboarding game. (I'm actually going to be using it for penguins sliding on their stomach but this has the same requirements). Note that ramps and cliffs should work as expected, with the player becoming airborne until they hit the ground.
I posted a thread on Twitter detailing my struggles here, but here is the best-functioning code I've managed so far:
// First, calculate our old trajectory
Vector3 oldTrajectory = transform.position - oldPosition;
// Objects in motion tend to stay in motion, so we start with that same motion first
Vector3 newTrajectory = oldTrajectory;
// If we're grounded, we'll take that into account for sliding purposes
if (cc.isGrounded)
{
// First get the slope underneath us
RaycastHit hit;
if (Physics.Raycast(transform.position, -oldNormal, out hit, 2f) || Physics.Raycast(transform.position, Vector3.down, out hit, 2f))
{
oldNormal = hit.normal;
Vector3 downSlopeRight = Vector3.Cross(hit.normal, GRAVITY_DIRECTION);
Vector3 downSlopeDirection = Vector3.Cross(downSlopeRight, hit.normal);
Vector3 movementForce = downSlopeDirection *
(gravity * Time.fixedDeltaTime * Vector3.Dot(downSlopeDirection, GRAVITY_DIRECTION)) *
(1 + (newTrajectory.magnitude * Vector3.Dot(downSlopeDirection, newTrajectory.normalized)));
newTrajectory += movementForce * slopeImpact * Time.fixedDeltaTime;
}
}
// Otherwise, normally account for gravity
{
newTrajectory += gravity * GRAVITY_DIRECTION * Time.fixedDeltaTime * Time.fixedDeltaTime;
}
oldPosition = transform.position;
cc.Move(newTrajectory)
Essentially: Take our current momentum, add gravity directed by the slope, and accelerate even more if the slope matches our trajectory (I can't even remember why I put that part in). This kind of works but especially when rocking back and forth in a divot, you notice that the momentum isn't really being handled properly (see here).
I attempted a second approach, where the player simply moves in the direction they're facing, and they gain speed while moving down a slope or lose it while going up. I couldn't figure out a way to get this to work at all:
// First, take the real distance we moved, and convert it to fairy numbers (time-independant measurements)
Vector3 newMomentum = (transform.position - oldPosition) / Time.fixedDeltaTime;
// If we're grounded, we'll take that into account for sliding purposes
if (cc.isGrounded)
{
// First get the slope underneath us
RaycastHit hit;
if (Physics.Raycast(transform.position, -oldNormal, out hit, 2f) || Physics.Raycast(transform.position, Vector3.down, out hit, 2f))
{
oldNormal = hit.normal;
Vector3 downSlopeRight = Vector3.Cross(hit.normal, GRAVITY_DIRECTION);
Vector3 downSlopeDirection = Vector3.Cross(downSlopeRight, hit.normal);
Vector3 facingDirectionOnSlope = Vector3.Cross(transform.right, hit.normal);
// All we want to do is move the player faster if they're facing down the slope, or slower if facing up
// (will also slide them backwards eventually which is funny)
float speed = newMomentum.magnitude;
speed += gravity * slopeImpact * Vector3.Dot(downSlopeDirection, newMomentum.normalized);
newMomentum = facingDirectionOnSlope * speed;
}
}
else
{
// WE ADD GRAVITY BECAUSE THE GRAVITY DIRECTION IS DOWN
newMomentum += gravity * Time.fixedDeltaTime * GRAVITY_DIRECTION;
}
// Update oldPosition after everything else (just in case we use it) but BEFORE CHANGING IT
oldPosition = transform.position;
cc.Move(newMomentum * Time.fixedDeltaTime);
I tried a few different variations on this but I couldn't get it to work.
This approach is ideal, where the player can choose their direction and slopes give them speed (later I'd gently rotate them towards facing down the slope), as opposed to the other one relying on momentum and directing gravity. I just can't seem to figure out how to get it to work.
CharacterController
is going to be your best bet. You'll have a much easier time with simulation if you use aRigidBody
so that you can work with the forces instead of callingMove
(and then you just need to make sure to lock rotation but do so naturally using the normal of the surface). Regardless, you should probably normalizedownSlopeDirection
in your first example to remove any magnitude problems that arise from the cross product calculation. Also, it looks like newTrajectory contributes twice (once with .magnitude, then you add to newTrajectory) so fix that \$\endgroup\$RigidBody
as I'd like better control over things like the amount and direction of gravity, friction, etc. I also find the default physics prefer to tumble than slide, even with low friction and bounciness. It just seems much simpler to define the exact movement I want than to try and wrangle aRigidBody
. ACharacterController
deals with collisions and checking if I'm grounded, which is all I want from it. \$\endgroup\$