Search Unity

Sliding down slopes system with the Character Controller

Discussion in 'Scripting' started by bbQsauce, Dec 30, 2017.

  1. bbQsauce

    bbQsauce

    Joined:
    Jun 29, 2014
    Posts:
    53
    Hello, i'm trying to create a sliding system similar to Rayman 2 but in first person, in which you can slide down slopes, move left and right and fly off ramps.

    I managed to do all of that except the left and right movement with acceleration while sliding since i get some weird behaviour when the character is on the intersection of 2 planes, it flickers back and forth like it doesn't know where to move. If i turn off the acceleration it works fine, but it's not exactly realistic sliding.

    Here's the script:
    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. [RequireComponent(typeof(CharacterController))]
    6. public class SlidingController : MonoBehaviour
    7. {
    8.     public float groundSpeed = 20;  // Ground and left/right movement speed while sliding
    9.     public float airAcceleration = 20;
    10.     public float jumpForce = 15;
    11.     public float gravityPower = 10;
    12.     public float airDrag = 0.2f;
    13.     public float slideMovementAcceleration = 40; // How fast do we accelerate left and right while sliding
    14.     public float slideMovementDrag = 0.5f;
    15.     public LayerMask slideMask; // Assign this layermask to objects you want the player to slide on
    16.     public string rampTag = "Finish"; // Put this tag on objects you want the player to slide on upwards (ramps)
    17.  
    18.     public bool slidingStrafeAccelEnabled = true; // If has to apply strafe acceleration while sliding
    19.  
    20.     float ccHeight;
    21.     float ccWidth;
    22.     float slideForce;
    23.     float strafeMagnitude;
    24.  
    25.  
    26.     bool grounded;
    27.     bool sliding;
    28.  
    29.     bool lastFrameFlying;
    30.     bool lastFrameGrounded;
    31.     bool lastFrameSliding;
    32.  
    33.  
    34.     Vector3 gravity = Vector3.zero;
    35.     Vector3 movement = Vector3.zero;
    36.     Vector3 slideDirection = Vector3.zero;
    37.     Vector3 airForce = Vector3.zero;
    38.  
    39.     CharacterController cc;
    40.     Vector3 hitNormal;
    41.  
    42.     // Use this for initialization
    43.     void Start()
    44.     {
    45.         cc = GetComponent<CharacterController>();
    46.         ccHeight = GetComponent<CharacterController>().bounds.extents.y;
    47.         ccWidth = GetComponent<CharacterController>().bounds.extents.x;
    48.     }
    49.  
    50.     // Update is called once per frame
    51.     void Update()
    52.     {
    53.         CheckGroundCondition(); // Check if i'm sliding, grounded or flying
    54.  
    55.         Vector3 input = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));
    56.         input.Normalize();
    57.         input = transform.TransformDirection(input); // Transform the input to be relative to the player's rotation
    58.  
    59.  
    60.         if (sliding)
    61.         {
    62.             if (lastFrameFlying) // If i was flying the last frame
    63.             {
    64.                 slideForce = gravity.y; // Set my slide speed based on the player's falling speed
    65.                 gravity = Vector3.zero;
    66.                 movement = Vector3.zero;
    67.                 airForce = Vector3.zero;
    68.             }
    69.  
    70.             slideForce -= gravityPower * Time.deltaTime; // Add slide speed over time
    71.             slideDirection *= slideForce;
    72.  
    73.             Quaternion rot = new Quaternion();
    74.             rot = Quaternion.FromToRotation(Vector3.up, hitNormal); // Create a rotation from the input direction and the normal of the surface
    75.             input = rot * input; // Apply the rotation to the input so it matches the surface normal we're standing on
    76.  
    77.             Vector3 slidePerpendicular = Vector3.Cross(slideDirection.normalized, hitNormal).normalized; // Get the plane perpendicular for the left/right movement while sliding
    78.             float strafeDot = Vector3.Dot(slidePerpendicular, input); // Determine if we should move left or right and how much
    79.  
    80.             if(slidingStrafeAccelEnabled)
    81.             {
    82.                 strafeMagnitude += slideMovementAcceleration * strafeDot * Time.deltaTime; // Accelerate our movement magnitude
    83.                 strafeMagnitude /= 1 + slideMovementDrag * Time.deltaTime; // Apply strafe drag
    84.                 movement = slidePerpendicular * strafeMagnitude;
    85.             }
    86.             else
    87.             {
    88.                 movement = slidePerpendicular * slideMovementAcceleration * strafeDot;
    89.             }
    90.  
    91.          
    92.         }
    93.         else if (grounded)
    94.         {
    95.             if(lastFrameFlying)
    96.             {
    97.                 gravity = Vector3.zero;
    98.                 airForce = Vector3.zero;
    99.             }
    100.  
    101.             movement = input * groundSpeed;
    102.  
    103.             if (Input.GetButtonDown("Jump"))
    104.             {
    105.                 gravity.y = jumpForce;
    106.             }
    107.         }
    108.         else
    109.         {
    110.          
    111.             if (lastFrameSliding)
    112.             {
    113.                 airForce = slideDirection; // Set the air force to the slide direction so we keep our speed after jumping off ramps
    114.                 slideDirection = Vector3.zero;
    115.             }
    116.  
    117.             if(lastFrameGrounded)
    118.             {
    119.                 airForce = movement;
    120.                 movement = Vector3.zero;
    121.             }
    122.          
    123.             airForce += input * airAcceleration * Time.deltaTime; // Accelerate air speed over time
    124.             airForce /= 1 + airDrag * Time.deltaTime; // Apply air drag
    125.  
    126.  
    127.             gravity.y -= gravityPower * Time.deltaTime; // Apply gravity
    128.         }
    129.  
    130.         cc.Move((movement + gravity + slideDirection + airForce) * Time.deltaTime); // Finally move the CC
    131.  
    132.  
    133.         lastFrameGrounded = grounded;
    134.         lastFrameSliding = sliding;
    135.         lastFrameFlying = !grounded && !sliding;
    136.     }
    137.  
    138.  
    139.  
    140.     void CheckGroundCondition()
    141.     {
    142.         RaycastHit check;
    143.  
    144.         bool upwards = false;
    145.         RaycastHit[] hits = Physics.SphereCastAll(transform.position, ccWidth / 4, -Vector3.up, ccHeight + 0.5f, slideMask);
    146.  
    147.         if (hits.Length > 0) // If the spherecast hit a least one object with the slideMask
    148.         {
    149.             Vector3 norm = Vector3.zero;
    150.             Vector3 upNorm = Vector3.zero;
    151.  
    152.             for (int i = 0; i < hits.Length; i++)
    153.             {
    154.                 if (hits[i].collider.tag == rampTag && hits[i].collider.gameObject != this.gameObject) // Determine if we should slide downwards or upwards
    155.                 {
    156.                     upwards = true;
    157.                     upNorm += hits[i].normal; // blend upwards normals
    158.                 }
    159.                 else if (hits[i].collider.gameObject != this.gameObject)
    160.                 {
    161.                     norm += hits[i].normal; // blend downwards normals
    162.                 }
    163.             }
    164.  
    165.             norm.Normalize();
    166.             upNorm.Normalize();
    167.  
    168.             if (!upwards) {            
    169.                 Vector3 cross = Vector3.Cross(Vector3.up, norm); // Get the slide direction perpendicular
    170.                 slideDirection = Vector3.Cross(norm, cross).normalized; // Get the slide direction
    171.                 hitNormal = norm;
    172.             }
    173.             else {            
    174.                 Vector3 cross = Vector3.Cross(Vector3.up, upNorm);
    175.                 slideDirection = -Vector3.Cross(upNorm, cross).normalized;
    176.                 hitNormal = upNorm;
    177.             }
    178.  
    179.             grounded = false;
    180.             sliding = true;
    181.  
    182.             // Debug rays
    183.             Vector3 slidePerpendicular = Vector3.Cross(Vector3.up, hitNormal);
    184.             Debug.DrawRay(hits[0].point, slidePerpendicular * 30, Color.blue);
    185.             Debug.DrawRay(hits[0].point, -slidePerpendicular * 30, Color.blue);
    186.             Debug.DrawRay(hits[0].point, -slideDirection.normalized * 30, Color.red);          
    187.         }
    188.         else if (Physics.Raycast(transform.position, -Vector3.up, out check,ccHeight + 0.1f) && check.collider.gameObject != this.gameObject) // Raycast downwards to see if there's an object to stand on
    189.         {
    190.             grounded = true;
    191.             sliding = false;
    192.         }
    193.         else // If i'm flying
    194.         {
    195.             grounded = false;
    196.             sliding = false;
    197.         }
    198.     }
    199. }

    Did anybody have had any experience with similar stuff? Is the right approach to get the slide direction through a double cross product between Vector3.up and the normal and then between the acquired vector and the normal again? Is there a way to have more control over the slide direction?
    Because sometimes this happens(red ray is the slide direction which is inverted because it's a ramp, blue rays are strafe movement) and what's supposed to be a ramp that launches you forward it launches you sideways because the plane is tilted on both the Z and the X axis.
    How do skiing games approach this for example?

    Thanks in advance.
     
    Last edited: Dec 30, 2017
    Ermiq likes this.
  2. lordconstant

    lordconstant

    Joined:
    Jul 4, 2013
    Posts:
    389
    Ive done something similar & had the exact same problems. I ended doing two raycasts, on from the character that you are currently doing & one ahead of the character.

    Use the front hit take the back to get the slide direction.

    Your current issue on flat ground gets solved by this as they go the way the started.

    Also add a blend to your new forward, that will stop the jittering when crossing intersecting planes.

    If the game is linear I would recommened going with a two spline system to define the edges of a slide that way you can path around much more easily also its less of a headache in general. Obviously this isnt always a possible option, but its easier to maintain.
     
    bbQsauce likes this.
  3. bbQsauce

    bbQsauce

    Joined:
    Jun 29, 2014
    Posts:
    53
    If i understand correctly you get the direction from the first hit point to the second to get the slide direction right? How do i get the forward position from which i would need to do the second raycast? Because my character can rotate and the slopes can be in any position or rotation they want. Should i get the initial sliding direction on the first hit of the slope and then keep it until i stop sliding?

    What do you mean with add a blend to your new forward?
     
  4. lordconstant

    lordconstant

    Joined:
    Jul 4, 2013
    Posts:
    389
    The raycasts would be continous while sliding.

    Fire one down in front of you, grab the hit spot. To get where to cast a ray down do something like:
    Vector3 forwardPos = transform.position + (transform.forward * forwardRayLength);
    Expose forwardRayLength as you may want to tweak this for better results.
    If walls start to give you problems you may want to shorten the length when your too close to one.

    Fire a second down from your current position, grab the hit spot.

    Do first hit spot - second hit spot & normalize the result, store this as a desired heading.

    Use https://docs.unity3d.com/ScriptReference/Vector3.RotateTowards.html to rotate from the current heading to the new heading gradually.
     
    bbQsauce likes this.