Project Zero Machine: Hover Car

Project Zero Machine: Hover Car

In the Project Zero Machine I made hover cars called machines that smoothly get back to a stable position over the ground no matter the ground slope.

The way it works that we have hover points equally spaced around our machine. These hover points raycasts downwards to check if the point is close to the ground. If a hover point is close to the ground it adds a relative upward force to the machine inversly proportional to the distance from the ground. The more hover points the more stable the machine will be on hazardous ground. In our case the ground is mostly flat with small slopes of max 30 degrees so four hover points is fine. For more flexibility I made it so that the hoverpoints can be set as transfroms (empty game objects) and we can add and remove and many as we want. You should not use less than three otherwise the object cannot stay stable:

stable-machines

One thing we need to take into account is the machines center of gravity. The hover points must be equally spaced from the center of gravity of our machine. For simplicity we set our center of gravity to zero vector in code. Do note that the default center of gravity and inertiaTensor is computed by Unity using the shape of the colliders in the rigidbody. Other than that we only need to play with the gravity and force settings and we get a hovering car!

But you’ll see that the machine is not stable enough if that is all we do. For example if we flip our machine on its back then it floats upside down. To deal with that we add a torque force if the upward angle of the machine is facing down.

Here is the full code for a hovering object. You can easily create a HoverCar script, which inherits from HoverObject instead of MonoBehaviour.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(Rigidbody))]
public class HoverObject : MonoBehaviour {

    protected enum State
    {
        Grounded,
        Flying,
        Respawning,
        Destroyed
    }

    protected State state;
    protected Rigidbody rb;
    [SerializeField] float hoverForce = 5000;
    [SerializeField] protected float hoverHeight = 2.0f;
    [SerializeField] float hoverStability = 5f;
    [SerializeField] float hoverStabilitySpeed = 15f;
    [SerializeField] float angleToDownThreshold = 20;
    //[SerializeField] float hoverStabilityForce = 4.5f;

    [Header("An array of transforms is created using its children")]
    [SerializeField] GameObject hoverPointsParent;
    List<Transform> hoverPoints;
    int layerMask;

    [SerializeField] protected GameObject meshAndCollider;

    float YThreshold = -5;

    float halfTurnPerSecondWhenUpsideDown = 10;

    FloorModifier floorModifier;

    public FloorModifier.Modifiers getFloorModifier { get { return floorModifier!=null ? floorModifier.floor : FloorModifier.normalFloor; } }
    public bool isActive { get { return !(state == State.Destroyed || state == State.Respawning); } }


    public virtual void Awake()
    {
        rb = GetComponent<Rigidbody>();
        state = State.Flying;

        //set center of mass to center of transform instead of computed by collider
        rb.centerOfMass = Vector3.zero;
        //set inertia to inertia of a sphere of radius 1, instead of computed by collider
        //http://hyperphysics.phy-astr.gsu.edu/hbase/isph.html
        float sphereInertia = (2f / 5f) * rb.mass;
        rb.inertiaTensor = new Vector3(sphereInertia, sphereInertia, sphereInertia);

        InitializeHoverPoints();

    }

    void Start()
    {
        layerMask = (1 << LayerMask.NameToLayer("Track")) + (1 << LayerMask.NameToLayer("Obstacle")) + (1 << LayerMask.NameToLayer("OutOfBounds"));
        //layerMask = ~layerMask;
    }

    void InitializeHoverPoints()
    {
        hoverPoints = new List<Transform>();
        if (hoverPointsParent)
        {
            hoverPoints.AddRange(hoverPointsParent.GetComponentsInChildren<Transform>());
            hoverPoints.RemoveAt(0);    //remove parents transform
        }

    }

    private void Update()
    {
        if (transform.position.y < YThreshold && isActive)
        {
            StartCoroutine(OffBounds());
        }
    }

    IEnumerator OffBounds()
    {
        state = State.Respawning;
        meshAndCollider.SetActive(false);
        yield return new WaitForSeconds(0.5f);
        state = State.Flying;
        Respawn();
    }

    protected virtual void Respawn()
    {
        state = State.Flying;
        meshAndCollider.SetActive(true);
        transform.localRotation = Quaternion.identity;
        transform.localPosition = Vector3.zero;
        rb.velocity = Vector3.zero;
        rb.angularVelocity = Vector3.zero;
    }

    void OnDrawGizmos()
    {
        if (!isActive) { return; }

        //Initialize hoverPoints
        if (hoverPoints == null)
        {
            InitializeHoverPoints();
        }
        if (hoverPoints.Count != hoverPointsParent.GetComponentsInChildren<Transform>().Length - 1)
        {
            InitializeHoverPoints();
        }

        //  Hover Force
        RaycastHit hit;
        for (int i = 0; i < hoverPoints.Count; i++)
        {
            var hoverPoint = hoverPoints[i];
            if (Physics.Raycast(hoverPoint.position,
                                -Vector3.up, out hit,
                                hoverHeight,
                                layerMask))
            {
                Gizmos.color = Color.blue;
                Gizmos.DrawLine(hoverPoint.position, hit.point);
                Gizmos.DrawSphere(hit.point, 0.5f);
            }
            else
            {
                Gizmos.color = Color.red;
                Gizmos.DrawLine(hoverPoint.position, hoverPoint.position + Vector3.down * hoverHeight);
            }
        }
    }

    

    void FixedUpdate () {

        if(!isActive) { return; }

        Vector3 upVector = Vector3.up; //TODO: should it be normal to floor?


        //Stabilise object
        Quaternion angleAxis = Quaternion.AngleAxis
            (
                rb.angularVelocity.magnitude * Mathf.Rad2Deg * hoverStability / hoverStabilitySpeed,
                rb.angularVelocity
            );
        Vector3 predictedUp = angleAxis * transform.up; //rotate up by angle
        Vector3 torqueVector = Vector3.Cross(predictedUp, upVector); //torque vector needed to achieve predictedUp

        //the bigger the angle the bigger the force
        rb.AddTorque(torqueVector * hoverStabilitySpeed * hoverStabilitySpeed);

        //if object is upside down
        float angleToDown = Vector3.Angle(transform.up, -upVector);
        if (Mathf.Abs(angleToDown) < angleToDownThreshold)
        {
            //turn on the z axis 
            float angVelZ = halfTurnPerSecondWhenUpsideDown * Mathf.PI; //turns radians per second 
            float angAccZ = angVelZ / Time.fixedDeltaTime;
            float inertiaZ = rb.inertiaTensor.z;
            rb.AddRelativeTorque(transform.forward * inertiaZ * angAccZ);
        }

        //  Hover Force
        RaycastHit hit;
        bool hitFloorModifier = false;
        bool hitSomething = false;
        bool outOfBounds = false;
        for (int i = 0; i < hoverPoints.Count; i++)
        {
            Transform hoverPoint = hoverPoints[i];

            //Hover object above ground
            Ray ray = new Ray(hoverPoint.position, Vector3.down);
            if (Physics.Raycast(ray, out hit, hoverHeight, layerMask))
            {
                hitSomething = true;
                outOfBounds = outOfBounds || hit.collider.gameObject.layer == LayerMask.NameToLayer("OutOfBounds");
                //Debug.Log(hit.collider.name);
                float normalizedDist = (1.0f - (hit.distance / hoverHeight));
                //the square of the distance
                Vector3 force = Vector3.up * hoverForce * normalizedDist * normalizedDist;
                rb.AddForceAtPosition(force, hoverPoint.position);

                FloorModifier tempfloorModifier = hit.collider.GetComponent<FloorModifier>();
                if(tempfloorModifier != null) { hitFloorModifier = true; floorModifier = tempfloorModifier; }
            }
        }
        if (outOfBounds)
        {
            StartCoroutine(OffBounds());
        }
        if (!hitFloorModifier)
        {
            floorModifier = null;
        }
        else
        {
            floorModifier.handler(rb);
        }
        if (hitSomething && state == State.Flying)
        {
            state = State.Grounded;
        }
        else if(!hitSomething && state == State.Grounded)
        {
            state = State.Flying;
        }
    }
}
Author face

Santiago Rubio (Sangemdoko)

A electronics and information engineer who works on game development in his free time. He created Sleeping Penguinz to publish the games he makes with his friends and familly.

Recent post