0
\$\begingroup\$

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.

\$\endgroup\$
  • \$\begingroup\$ Not sure if a CharacterController is going to be your best bet. You'll have a much easier time with simulation if you use a RigidBody so that you can work with the forces instead of calling Move (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 normalize downSlopeDirection 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\$ – Coburn Feb 20 '17 at 0:32
  • \$\begingroup\$ Also, you never actually posted a question. I understand you're having trouble but you should have at least some concrete questions that we can write answers for instead of "My code just isn't working, how do I do this right?" because that in itself isn't concrete. \$\endgroup\$ – Coburn Feb 20 '17 at 0:34
  • \$\begingroup\$ @Coburn The question was essentially "How could I program these physics", but I understand that might be a bit vague (I'm not really sure what else to ask). I'd rather avoid 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 a RigidBody. A CharacterController deals with collisions and checking if I'm grounded, which is all I want from it. \$\endgroup\$ – Magrias Feb 21 '17 at 20:32
  • \$\begingroup\$ Nah that's not really that vague. It at least tells me you're looking for a solution to this larger problem than trying to figure out a specific error in your code so I'll target my answer toward that. \$\endgroup\$ – Coburn Feb 21 '17 at 22:02
1
\$\begingroup\$

You're going to need to stop using a CharacterController and start using a RigidBody. The CharacterController is a class directly in the physics engine (PhysX). Because of this, Unity doesn't really allow for much customization on it (and I'm not really sure if they could) and so you lose a lot of control that you get with a straight up RigidBody. What you gain in the CharacterController in simplicity you lose in configurability. Continueing to use CharacterController means constantly fighting the defaults they have put in place to try to do things that are outside of the scope of that class.

When you switch to a RigidBody, consider the following things:

  • Use AddForce (or a similar function that suits your use case) for gravity and it will automatically work with collisions
  • Preventing the character from tumbling instead of sliding is going to require that you properly set the mass of the character as well as a bit of clever rotation management. This includes locking the rotation to the terrain you're currently on or applying rotational acceleration so the character doesn't flip over. A more complex solution will result in more realism/less seemingly like you're glued to the ground.
  • Checking if your grounded is probably the hardest part because there's no one solution to this (and in itself it's kind of vague, like, are you grounded if you're on the ground but upside down?). You'll just have to use some clever raycasting and sweeping to determine this one or maybe a geometric solution.
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.