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:
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;
}
}
}