Hoovering vehicule with thrusters

Hello everyone,

I’m currently playing with unity physics and i’m requesting your help because i’m trying to make a floating vehicule with thrusters and i’m pretty sure that my approach is so naive that I must lack the knowledge and understanding of the principles of basic physics.

So here’s my approach :

  • I have my main vehicule as a gameobject with a Rigidbody and a collider
  • I have four children that act as thrusters positions : I use their relative position to apply a force on a specific point of my vehicule
  • Each tick, I loop inside my thrusters gameobjects and fire a Ray : if they are below the max height I can apply a force
  • Based on the distance between each thruster and their hit.point, I multiply the force of my thruster by the normalized distance value. For instance, if the max distance is 10 and the distance between the hit.point and the thruster is 5, I multiply my thruster strength by that normalized value (which in this case is 0.5). My thruster is at 50% of it’s maximum strength.

Here is the complete code :

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

[RequireComponent(typeof(Rigidbody))]
public class Hoovering : MonoBehaviour
{
    const float _GRAV = 9.81f;

    //Children object that are placed at thrusters location
    public GameObject[] m_hooverPoints;

    //Components
    private Rigidbody rb;

    //Metrics
    [SerializeField] private float m_hooverMaxHeight, m_hooverMinHeight;
    [SerializeField] private float m_hooverThrust;
    [SerializeField] private float m_hooverDamp;

    //Testing purposes
    [SerializeField] private Color col_minThruster, col_maxThruster;

    private void Start()
    {
        rb = gameObject.GetComponent<Rigidbody>();
        if (m_hooverPoints == null)
        {
            Debug.LogError("No hoover points detected");
            return;
        }
    }

    private void Update()
    {
        TestInputs();
    }

    private void FixedUpdate()
    {
        //Check if the model is facing uspide down
        if (Vector3.Dot(transform.up, Vector3.down) < 0 )
        {
            for (int i = 0; i < m_hooverPoints.Length; i++)
            {
                //Shoot the raycast at the hooverpoint position
                Vector3 anchorPointPos = m_hooverPoints[i].GetComponent<Transform>().position;
                Ray downRay = new Ray(anchorPointPos, -Vector3.up);
                RaycastHit hit;

                if (Physics.Raycast(downRay, out hit, m_hooverMaxHeight))
                {
                    //float m_normalizedOffset = m_hooverHeight - hit.distance / m_hooverHeight - hooverOffset;
                    float m_normalizedThruster = (m_hooverMaxHeight - hit.distance) / (m_hooverMaxHeight - m_hooverMinHeight);
                    Debug.Log("Thruster " + i + " = " + m_normalizedThruster * 100 + " % of power");
                    Debug.DrawLine(anchorPointPos, hit.point, Color.Lerp(col_minThruster, col_maxThruster, m_normalizedThruster));
                    float upwardSpeed = m_hooverPoints[i].GetComponent<Rigidbody>().velocity.y;
                    float m_lift = (m_hooverThrust * m_normalizedThruster);

                    rb.AddForceAtPosition(m_lift * Vector3.up, anchorPointPos, ForceMode.Force);
                }
            }
        }
    }

    private void TestInputs()
    {
        if (Input.GetKey(KeyCode.UpArrow))
        {
            rb.AddForce(-Vector3.forward * 100f, ForceMode.Force);
        }
        else if (Input.GetKey(KeyCode.DownArrow))
        {
            rb.AddForce(Vector3.forward * 100f, ForceMode.Force);
        }
    }
}

greenvastcornsnake

But, as you can see in this gif, it’s not really properly working. Right now, it can’t stop bouncing. I guess, i could have a “stable” thruster force (which is like 50% of the max strength) but I don’t really know how to calculate the optimum strength for a stable vehicule. I can’t really handle slopes too. I don’t know if lowering the strength based on the distance is a good idea. Again, my guess would be to apply a normalized but relative value of each thruster. Instead of comparing the distance between the ground and the thruster, find the lowest thruster on the vehicule, relative to the highest thruster.

Oh and I was wondering, how can I retranscript the strength of a thruster based on the mass of my rigidbody ? Does “1” of mass equal “1” of strength applied in an AddForce ?

I’ve already searched for solution on the net, but i’m pretty sure that I don’t really understand how it works (maybe it’s a bad idea to proceed like that), so I was wondering if you could help me to find the best approach.

Thank you for your lecture, and have a nice day,
Klondique

Just use simplifications of physics. You probably don’t need all that complexity.

Assume fixed mass of vehicle.
Consider always up, with maybe some wobble.
Use center of mas CoM, to hold stable position above the ground. You can use lerp as damping. Is kind of cheating physics, but most games are smoke and mirrors.

However, if you start using mass and want adjustable thrusters, you would need go into process control and automation route. Using PI, PID, or alternative ML for even more clever control.

Complexity increases with multiple thrusters, special if they are not always look down. Your hovercraft will start swing sideways, and may start spinning, on uneven ground. You could use some further simplification at this stage. You certainly want to avoid all that complexity, if you not familiar with controls. Otherwise you will really struggle to hold your hovercraft stable on such platforms, without additional directional and position controllers.

See my hovercraft WIP project, if curious a bit more on the challenges.

Hey there, you’re on the right track! Add a damping force to your thrusters:

right now you implemented something that is like a mass and a spring. They will bounce indefinitely. Add a per thruster force proportional to the velocity of the thrusters projected onto the raycast direction.

Look for spring, mass, damper differential equations, there’s very good material about that out there. Let me know if you need a more in-depth explanation!

@tjmaul I was just writing a post suggesting exactly that :slight_smile:

I’ll then contribute with some pseudocode. Right now the thruster force looks like this:

force = hooverTrust * height

Applying a damper would look like this:

velocity = (height - previousHeight) / Time.deltaTime
force = hooverTrust * height - velocity * hooverTrustDamper

After fine tuning the values a bit you should get a stable hoovering vehicle.

Edit: additionally, you may want to verify that the force is greater than zero before applying it. If the height change rate is large enough it may result in negative forces. This would make the vehicle to keep “glued” to the ground in situations where it should just jump. You may want this behavior or not based on the expected gameplay.

Hi everyone,
Thanks for you kind answers.
@Antypodish
Thank you for clearing that up. An approach using a PID is very interesting. Nevertheless I have a small question, in the context of a hoovering vehicle, the PID would be used to regulate the power of the thrusters by using it as a value to correct the height of the vehicle?

@Edy @tjmaul
Thank you for your answers and formulas. Dampering is therefore essential to nullify the rebound of the vehicle and stabilize it?
I tried to implement your solution but even when tweaking the angular drag and the mass of the vehicle, it became very unstable. Maybe it’s because I apply dampering on the height and velocity of each thruster?

Here’s a gif and the code.

hauntingdelectableegret

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

[RequireComponent(typeof(Rigidbody))]
public class SmartHoovering : MonoBehaviour
{
    const float _GRAV = 9.81f;
    public PID pid;

    [Header("Components")]
    //Children object that are placed at thrusters location
    public GameObject[] m_hooverPoints;

    //Components
    private Rigidbody rb;
    private Vector3 cOM;

    [Header("Parameters")]
    [SerializeField] private float m_hooverMaxHeight;
    [SerializeField] private float m_hooverMinHeight;
    [SerializeField] private float m_stableHeight;
    [SerializeField] private float m_hooverThrust;
    [SerializeField] private float m_hooverDamp;

    private float previousHeight;

    [Header("In Editor")]
    [SerializeField] private Color col_minThruster;
    [SerializeField] private Color col_maxThruster;

    private void Start()
    {
        rb = gameObject.GetComponent<Rigidbody>();
        cOM = rb.centerOfMass;

        m_stableHeight = (m_hooverMaxHeight + m_hooverMinHeight)/2;

        if (m_hooverPoints == null)
        {
            Debug.LogError("No hoover points detected");
            return;
        }
    }

    private void Update()
    {
        TestInputs();
    }

    private void FixedUpdate()
    {
        float correctedHeight = pid.Update(m_stableHeight, currentHeight(cOM), Time.deltaTime);
        Debug.Log(correctedHeight);

        //Check if the model is facing uspide down
        if (Vector3.Dot(transform.up, Vector3.down) < 0 )
        {
            for (int i = 0; i < m_hooverPoints.Length; i++)
            {
                //Shoot the raycast at the hooverpoint position
                Vector3 anchorPointPos = m_hooverPoints[i].GetComponent<Transform>().position;
                Ray downRay = new Ray(anchorPointPos, -Vector3.up);
                RaycastHit hit;
                  
                if (Physics.Raycast(downRay, out hit, m_hooverMaxHeight))
                {
                    //float m_normalizedOffset = m_hooverHeight - hit.distance / m_hooverHeight - hooverOffset;
                    float m_normalizedThruster = (m_hooverMaxHeight - hit.distance) / (m_hooverMaxHeight - m_hooverMinHeight);
                    //Debug.Log("Thruster " + i + " = " + m_normalizedThruster * 100 + " % of power");

                    Debug.DrawLine(anchorPointPos, hit.point, Color.Lerp(col_minThruster, col_maxThruster, m_normalizedThruster));
                    //float upwardSpeed = m_hooverPoints[i].GetComponent<Rigidbody>().velocity.y;
                    float velocity = (currentHeight(anchorPointPos) - previousHeight) / Time.deltaTime;
                    Debug.Log(velocity);

                    float m_lift = (m_hooverThrust * currentHeight(anchorPointPos)) - velocity * m_hooverDamp;

                    rb.AddForceAtPosition(m_lift * Vector3.up, anchorPointPos, ForceMode.Acceleration);

                    previousHeight = hit.distance;
                }
            }
        }
    }

    private void TestInputs()
    {
        if (Input.GetKey(KeyCode.UpArrow))
        {
            rb.AddRelativeForce(-Vector3.forward * 50f, ForceMode.Force);
        }
        if (Input.GetKey(KeyCode.DownArrow))
        {
            rb.AddRelativeForce(Vector3.forward * 50f, ForceMode.Force);
        }
        if (Input.GetKey(KeyCode.RightArrow))
        {
            rb.AddRelativeTorque(new Vector3(0, 1, 0) * m_hooverThrust, ForceMode.Force);
        }
        if (Input.GetKey(KeyCode.LeftArrow))
        {
            rb.AddRelativeTorque(new Vector3(0, -1, 0) * m_hooverThrust, ForceMode.Force);
        }

        //Small AddForce
        rb.AddRelativeForce(-Vector3.forward * 15f, ForceMode.Force);

    }

    private float currentHeight(Vector3 position)
    {
        Ray downRay = new Ray(position, -Vector3.up);
        RaycastHit hit;
        if (Physics.Raycast(downRay, out hit, Mathf.Infinity))
        {
            return hit.distance;
        }
        else
            return 0;
    }

    private void OnDrawGizmos()
    {
        Gizmos.color = new Color(1, 0, 0, 0.5f);
        Gizmos.DrawCube(transform.position, Vector3.one);
        float correctedHeight = pid.Update(m_stableHeight, currentHeight(cOM), Time.deltaTime);
        Gizmos.color = new Color(0, 1, 1, 1f);
        Gizmos.DrawCube(new Vector3(transform.position.x, correctedHeight, transform.position.z), Vector3.one);
    }
}

I tried to tweak all the formulas: the applied force, the power of the dampering, the calculation of the height between the thruster and the floor, but nothing works, I can’t get a stable vehicle, even on a flat floor.
I’m trying to understand but I confess I’m having a little trouble, if you could help me understand what’s wrong, I’d be very grateful.

Thank you and have a nice day,
Klondique

float m_lift = ((m_hooverThrust * currentHeight(anchorPointPos)) - velocity * m_hooverDamp) * Time.deltaTime;

Edit : I just realized I forgot to multiply by Time.deltaTIme… It’s way more stable now. But i’m still having the bounce effect, so I guess nothing is solved :smile:

distortedposhcoati

It’s a force, you shouldn’t multiply it by Time.deltaTime. If you do so, then you’d get different results based on the actual timestep used.

First, I’d ensure that setting m_hooverDamp to 0 leaves you with the exact result as you had previously. Then I’d try these workarounds:

  • Use small m_hooverDamp values. Vehicles in those videos seem to be using too much damping so the stable state is “overshoot”.
  • Increase the rigidbody’s inertia tensor (Rigidbody.inertiaTensor). Just read its value on start, multiply by some factor, then re-apply it. The higher factor, the more difficult will be to the rigidbody to rotate due to the off-center forces.

@Klondique that is correct. PID will adjust output base on input. So if you provide height as input, your output will be thrust threshold.

But PID may be tricky to tune. Also it may be vulnerable to dramatic changes. For example changing significant mass, or reconfiguration of thrusters position. Then PID may require to retune.

On side note, Neural Network can be capable to handle such variables. Or mix of NN and PID. By that not only options here of course.

If you know, that setup of vehicle is stable and rather constant, PID can be definitely worth a shot.

Using physics damping, can help mitigate unwanted oscillations.