Search code examples
c#unity-game-enginevectorquaternions

Unity align vehicle to road WITH rotation


Goal: to create a vehicle with properties similar to that of Mario Kart 8's anti gravity mode, or f- zero; the ability to ride on extreme non horizontal surfaces.

Desired behavior: the vehicle should not turn unless the thumbstick or arrow keys are pressed/moved; it must keep a straight line of motion with the exception of roll and vertical curvature relative to the camera's view.

Actual behavior: The vehicle will slowly(sometimes quickly) fall out of line and keep curving until the track stops bending. if placed in an inward facing cylinder and driven around radially, the vehicle will begin to curve towards either global +z or global +y.

(no error messages)

What I've tried: -setting transform.up to the surface normal then rotating around the normal as an axis -using quaternion.euler(0, [desired angle], 0) then fromToRotation

The alignment and rotation code:

transform.rotation = Quaternion.Euler(0, rotation, 0);
Quaternion tilt = Quaternion.FromToRotation(Vector3.up, localUp);
transform.rotation = tilt * transform.rotation;
transform.position += velocity * 1.1f;

The entire script:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
using PhysicsExtensions;
using UnityEngine.Rendering.PostProcessing;

public class Cart : MonoBehaviour
{
Kartphysics inputActions;
public new Transform camera, camTarget, camTargetDrift, Visual;
public ShipType shipType;
public AudioSource Vroom;
public Vector3 localUp = Vector3.up;
Vector3 velocity, camUp, followPos;
public AnimationCurve SteeringControl;
public float steerAmount;
float rotation, rollTarget, roll, fovDifference, vroomPitch = 0, flameLength = 0;
public float normalFov, speedFov, Velocity, rollAmount, speedFactor, forcedAcceleration;
public GameObject[] ships;
public FlamingTrail[] flames;
public PostProcessProfile ppp;
Vector2 JoystickVal;
ChromaticAberration ca;
LensDistortion ld;
Vector3 LastForward;

private void Start()
{
    switch (shipType)
    {
        case ShipType.Carrier:
            {
                ships[0].SetActive(true);
                break;
            }
        case ShipType.Ram:
            {
                ships[1].SetActive(true);
                break;
            }
    }
    ca = ppp.GetSetting<ChromaticAberration>();
    ld = ppp.GetSetting<LensDistortion>();
}
private void Update()
{
    UpdateVisuals();
    UpdateCamera();
    Velocity = velocity.magnitude;
}
private void FixedUpdate()
{
    UpdateKart();
}
void SetFlames(float length)
{
    for(int i = 0; i < flames.Length; i++)
    {
        flames[i].length = length;
    }
}
void UpdateVisuals()
{
    ca.intensity.value = Mathf.Clamp01(forcedAcceleration) * 2;
    ld.intensity.value = Mathf.Lerp(0, -70f, Mathf.Clamp(forcedAcceleration, 0, 1));
    SetFlames(flameLength);
    Vroom.pitch = Mathf.Lerp(Vroom.pitch, vroomPitch, (speedFactor * 0.01f) * 10);
    Visual.position = Vector3.Lerp(Visual.position, transform.position, (speedFactor * 0.01f) * 30);
    Visual.rotation = Quaternion.Lerp(Visual.rotation, transform.rotation, (speedFactor * 0.01f) * 15);
    
}
void UpdateCamera()
{
    fovDifference = speedFov - normalFov;
    Camera.main.fieldOfView = speedFov - (fovDifference * (1 / Mathf.Clamp(velocity.magnitude + 1, 0, Mathf.Infinity)));
    camUp = Vector3.Lerp(camUp, localUp.normalized, (speedFactor * 0.01f) * (Vector3.Distance(camera.position, Vector3.Lerp(camTarget.position, camTargetDrift.position, transform.InverseTransformDirection(velocity).x)) + 3));
    camera.rotation = Quaternion.Slerp(camera.rotation, Quaternion.LookRotation((transform.position - (transform.right * transform.InverseTransformDirection(velocity).x * 5) + transform.up) - camera.position, camUp), (speedFactor * 0.01f) * 13);
    camera.position = Vector3.Lerp(camera.position, Vector3.Lerp(camTarget.position, camTargetDrift.position, transform.InverseTransformDirection(velocity).x), (speedFactor * 0.01f) * Vector3.Distance(camera.position, camTarget.position) * 20);
}
void UpdateKart()
{
    JoystickVal = new Vector2(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical"));
    if (Input.GetAxis("Submit") > 0.5)
        JoystickVal = new Vector2(JoystickVal.x, 1);
    if (Input.GetAxis("Cancel") > 0.5)
        JoystickVal = new Vector2(JoystickVal.x, -1);
    if (JoystickVal.magnitude > 1)
    {
        JoystickVal.Normalize();
    }
    JoystickVal *= (speedFactor * 0.01f) * 0.2f;
    JoystickVal /= Mathf.Clamp(velocity.magnitude, 0.7f, Mathf.Infinity);
    velocity += ((transform.forward * JoystickVal.y) / Mathf.Clamp(Mathf.Abs(transform.InverseTransformDirection(velocity).x), 0.7f, Mathf.Infinity));
    
    rollTarget = Mathf.Clamp01(SteeringControl.Evaluate(velocity.magnitude)) * JoystickVal.x * rollAmount;
    roll = Mathf.MoveTowards(roll, rollTarget, (speedFactor * 0.01f) * 4);
    velocity -= localUp * (speedFactor * 0.01f) * 0.7f;
    velocity /= 1 + ((speedFactor * 0.01f) / 8);
    RaycastHit hit;
    CircleCastHit circleHit;
    if (Physics.Raycast(transform.position + transform.up, -transform.up + (velocity / 1), out hit))
    {

        if (hit.distance < 4)
        {
            transform.position -= hit.normal.normalized * (speedFactor * 0.01f);
            localUp = Vector3.MoveTowards(localUp, hit.normal, (speedFactor * 0.01f) * 9);
            if (hit.distance < 1.2f)
            {
                flameLength = Velocity * 2;
                if (hit.collider.tag == "SpeedPanel")
                    forcedAcceleration = 3f;
                rotation += SteeringControl.Evaluate(velocity.magnitude * 0.7f) * JoystickVal.x * (speedFactor * 0.01f) * 100 * steerAmount;
                transform.position += hit.normal.normalized * (1 - hit.distance);
                vroomPitch = velocity.magnitude * 1.5f;
                velocity += ((transform.forward * ((JoystickVal.y * 1.3f) + (forcedAcceleration / 100))) / Mathf.Clamp(Mathf.Abs(transform.InverseTransformDirection(velocity).x), 0.7f, Mathf.Infinity));
                rotation += SteeringControl.Evaluate((speedFactor * 0.01f) * velocity.magnitude * 50) * JoystickVal.x * 0.3f;
                velocity /= 1 + ((speedFactor * 0.01f));
                velocity -= transform.right * transform.InverseTransformDirection(velocity).x * 0.2f;
                Vector3 force = (hit.normal * -transform.InverseTransformDirection(velocity).y / Mathf.Clamp(hit.distance - 0.1f, 0.5f, 2)) * 1.1f;
                if (force.magnitude > 1)
                    force = force.normalized * 1;
                force /= 8;
                velocity += force;

            }
            else
            {
                vroomPitch = 0;
                flameLength = Mathf.MoveTowards(flameLength, 0, 0.03f);
            }

        }
        else
        {
            localUp = Vector3.MoveTowards(localUp, Vector3.up, (speedFactor * 0.01f) * 1.2f);
            vroomPitch = 0;
            transform.forward = velocity.normalized;
            flameLength = Mathf.MoveTowards(flameLength, 0, 0.03f);
        }
    }
    else
    {
        localUp = Vector3.MoveTowards(localUp, Vector3.up, (speedFactor * 0.01f) * 2);
        vroomPitch = 0;
        flameLength = Mathf.MoveTowards(flameLength, 0, 0.03f);
    }
    if (PhysicsII.CircleCast(transform.position + (transform.up * 0.5f), localUp, 0.7f, 8, out circleHit))
    {
        Debug.DrawRay(circleHit.nearestHit().point, circleHit.nearestHit().normal, Color.red, 0.1f);
        Debug.Log("HIT");
        velocity += (transform.position + (transform.up * 0.5f) - circleHit.nearestHit().point) / 3;
        if (circleHit.nearestHit().distance < 0.4f)
            velocity += (transform.position + (transform.up * 0.5f) - circleHit.nearestHit().point) / 7;
        if (circleHit.nearestHit().distance < 0.14f)
            velocity += (transform.position + (transform.up * 0.5f) - circleHit.nearestHit().point) / 7;

    }
    if(Physics.Raycast(transform.position + (transform.up * 0.8f) - velocity, velocity , out hit))
    {
        if(hit.distance < Velocity * 2)
            velocity /= 1 + ((speedFactor * 0.01f) * 2f);
        if (hit.distance < Velocity * 1.2f)
            velocity = Vector3.Reflect(velocity, hit.normal);
    }
    forcedAcceleration = Mathf.MoveTowards(forcedAcceleration, 0, 0.1f);

    transform.rotation = Quaternion.Euler(0, rotation, 0);
    Quaternion tilt = Quaternion.FromToRotation(Vector3.up, localUp);
    transform.rotation = tilt * transform.rotation;
    transform.position += velocity * 1.1f;

}
public enum ShipType
{
    Carrier = 0,
    Ram = 1
}
}

Solution

  • Here's a partial answer because I can't test it on my end currently to see if it works. It also appears like "roll" isn't yet used for anything (is it meant to alter the local up of the transform somehow?) so I'm not sure about that.

    First, instead of keeping a float rotation to keep track of how the vehicle is turned, you can just use transform.forward or transform.right for those purposes, and measure the modifications to that on a per-frame basis:

    void UpdateKart()
    {
    
        Vector3 newForward = transform.forward;  
        float turnAmount = 0f;
    
        // ...
                if (hit.distance < 1.2f)
                {
                    flameLength = Velocity * 2;
                    if (hit.collider.tag == "SpeedPanel")
                        forcedAcceleration = 3f;
                    turnAmount += SteeringControl.Evaluate(velocity.magnitude * 0.7f) 
                            * JoystickVal.x * (speedFactor * 0.01f) * 100 * steerAmount;
                    transform.position += hit.normal.normalized * (1 - hit.distance);
                    vroomPitch = velocity.magnitude * 1.5f;
                    velocity += /* too long to bother formatting */
                    turnAmount += SteeringControl.Evaluate((speedFactor * 0.01f) 
                            * velocity.magnitude * 50) * JoystickVal.x * 0.3f;
    
        // ...
    
    

    Then when you actually adjust the rotation, apply the turn amount around the local up axis to the current local forward direction. And finally, set the transform's rotation so that its new local up is localUp and it keeps its local forward as constant a direction as possible (cross products followed by Quaternion.LookRotation can be used for this):

        forcedAcceleration = Mathf.MoveTowards(forcedAcceleration, 0, 0.1f);
    
        Vector3 turnedForward = Quaternion.AngleAxis(turnAmount - 180, localUp) * 
                transform.forward; 
    
        Vector3 newRight = Vector3.Cross(turnedForward, localUp);
        if (newRight == Vector3.zero)
        {
           /* Ambiguous situation - maybe kart landed with its nose directly in the 
              direction of localUp or opposite direction. Possible solution: use 
              velocity as previous forward direction and recalculate, using a random 
              direction if that doesn't work 
           */
    
           newRight = Vector3.Cross(velocity, localUp); 
    
           if (newRight == Vector3.zero) 
           {    
               newRight = Vector3.ProjectOnPlane(Random.insideUnitSphere, 
                       localUp).normalized;
           }
        }
    
        Vector3 newForward = Vector3.Cross(newRight, localUp);
    
        transform.rotation = Quaternion.LookRotation(newForward, localUp);
        transform.position += velocity * 1.1f;
    

    The reason you're seeing the results you are is that FromToRotation will give you the "smallest" rotation that will move one vector to the other. But you're more concerned with a rotation that will keep the local forward close to what they are before the adjustment (it's difficult to explain why this isn't the same thing). Hence the Cross stuff.

    As I said, this is only intended to be a partial solution to get you closer. But, it may be all you need. Let me know what you think in the comments.