Creating a motion control for a space ship and rockets using the physics and forces / torgues

Hello,

I’m still quite new to Unity. But thanks to the forums and various YouTube tutorials, I have made good progress.

Currently, I am building a space game. Since it is set in space, I am using the physics engine but without gravity - otherwise, everything would fall down.

My current plan (I hope I won’t run into a dead end) is to base movements (and rotations) on forces from thrusters (i.e., main engines and side thrusters for roll, pitch, and yaw).

It’s already working quite well for the player. But application of this for a enemy or rockets is currently impossible.

For the ship control, I use the following:

Here is just the essential excerpt:

public float maxThrustInkN = 10f; // Maximum thrust of the ship in kN
public float currentThrottle = 0f; // Current value
public float afterburnerThrustInkN = 300f; // Maximum thrust of the ship's afterburner in kN
public float massInTons = 1f; // Mass of the ship in metric tons
public float drag = 0.1f; // Air resistance of the ship
public float maxThrustGier = 5f; // Maximum yaw rate of the ship
public float maxThrustNick = 5f; // Maximum pitch rate of the ship
public float maxThrustRoll = 5f; // Maximum roll rate of the ship
public float initialGierThrustPercent = 0.1f; // Initial percentage of maximum yaw thrust
public float gierThrustIncrement = 0.1f; // Increase in yaw thrust per second
public float nickThrustIncrement = 0.1f; // Increase in pitch thrust per second
public float rollThrustIncrement = 0.1f; // Increase in roll thrust per second
public TextMeshProUGUI currentSpeedText; // Reference to the TextMeshPro text element to display the speed
public float crossSectionalArea = 10f; // Cross-sectional area of the ship in square meters
public float airDensity = 1.225f; // Air density in kg/m^3
public float angularDrag = 1f; // Angular drag of the ship

void Awake()
{
    rb = GetComponent<Rigidbody>();
    rb.mass = massInTons * 1000f; // Converts tons to kilograms
    rb.drag = drag; // Sets the air resistance of the rigidbody
    rb.angularDrag = angularDrag; // Sets the angular drag of the rigidbody
}

void Start()
{
    // Connect the controls to the ship
    ....
    InvokeRepeating(nameof(CheckStatus), 0f, 0.1f); // Calls the CheckStatus method every 0.1 seconds
    InvokeRepeating(nameof(IncreaseGierThrust), 0f, 0.1f); // Calls the IncreaseGierThrust method every 0.1 seconds
    InvokeRepeating(nameof(IncreaseNickThrust), 0f, 0.1f); // Calls the IncreaseNickThrust method every 0.1 seconds
    InvokeRepeating(nameof(IncreaseRollThrust), 0f, 0.1f); // Calls the IncreaseRollThrust method every 0.1 seconds
}

void Update()
{
    // Read controls and apply
    HandleThrustInput();
    HandleGierInput();
    HandleNickInput();
    HandleRollInput();
}

void FixedUpdate()
{
    // Apply forces
    ApplyThrust();
    ApplyGier();
    ApplyNick();
    ApplyRoll();
}

void HandleThrustInput()
{
    if (Input.GetKeyDown(KeyCode.KeypadMultiply)) // Full thrust
    {
        currentThrust = maxThrustInkN;
    }
    if (Input.GetKeyDown(KeyCode.KeypadDivide)) // Stop thrust
    {
        Debug.Log("Stop");
        currentThrust = 0;
    }
}

// Just as example
void HandleGierInput()
{
    if (Input.GetKey(KeyCode.A)) // A key for turning left
    {
        Debug.Log("Left");
        isGierLeft = true;
        isGierRight = false;
    }
    else if (Input.GetKey(KeyCode.D)) // D key for turning right
    {
        Debug.Log("Right");
        isGierLeft = false;
        isGierRight = true;
    }
    else
    {
        isGierLeft = false;
        isGierRight = false;
    }
}

// Application of thrust force
void ApplyThrust()
{
    thrustForce = transform.right * currentThrust * 1000f; // Converts kN to N and directs the force in the ship's X direction
    rb.AddForce(thrustForce);

    // Adjusts the velocity vector to the ship's orientation
    if (currentThrust > 0f)
    {
        rb.velocity = transform.right * rb.velocity.magnitude;
    }
}

void ApplyGier()
{
    Vector3 gierTorque = transform.up * currentGierThrust * 1000f; // Converts kN to N and directs the torque in the ship's Y direction
    rb.AddTorque(gierTorque);
}

void ApplyNick()
{
    Vector3 nickTorque = transform.forward * currentNickThrust * 1000f; // Converts kN to N and directs the torque in the ship's Z direction
    rb.AddTorque(nickTorque);
}

void ApplyRoll()
{
    Vector3 rollTorque = transform.right * currentRollThrust * 1000f; // Converts kN to N and directs the torque in the ship's X direction
    rb.AddTorque(rollTorque);
}

t’s working quite well.

The issue, however, is that the rotation starts slowly and lingers a bit. For a player, this is not a problem, but for the AI, or initially for something like a rocket flying towards a target, it’s a big problem.

Attached is the rocket script.

To try it out:

Create 3 bodies (rocket, launcher, target) and 1 camera.

The rocket needs a Rigidbody. The script goes on the rocket.

When the rocket starts, it moves along the Y-direction of the launcher. Then it should fly towards the target - for this, it needs to use the forces to rotate.

I started a script to calculate the direction.

Flight axis rocket: Y+ Launch direction launcher: Y+

using UnityEngine;
using UnityEngine;

public class RocketController : MonoBehaviour
{
    public float throttleInKilonewtons = 100f; // Throttle of the rocket in kN
    public float steeringThrust = 10f; // Steering thrust in kN for yaw and pitch
    public float rocketMass = 1f; // Mass of the rocket in tons
    public float drag = 0.1f; // Air resistance of the rocket
    public float angularDrag = 0.1f; // Roll resistance / inertia
    public float startVelocity = 80f; // Start speed in m/s
    public float currentSpeed = 0f;
    private Rigidbody rb;
    public Transform target; // The target as a parameter
    public Transform launchPoint; // The launch point as a parameter

    public float proportionalGain = 1.0f;
    public float derivativeGain = 1.0f;
    private float previousError = 0.0f;
    private float maxControlSignal = 30;

    private void Start()
    {
        // Setup Rigidbody for rocket
        rb = GetComponent<Rigidbody>();
        rb.mass = rocketMass * 1000f; // Set physics for the weight of the rocket
        rb.drag = drag; // Sets the air resistance of the rigidbody
        rb.angularDrag = angularDrag; // Sets the angular drag of the rigidbody
        rb.useGravity = false; // Ensure gravity is off - else rockets do not work!

        if (launchPoint != null)
        {
            // Set the initial position and direction of the rocket to point from the launch point
            transform.position = launchPoint.position;
            Vector3 launchDirection = (launchPoint.forward).normalized;
            transform.rotation = Quaternion.LookRotation(launchDirection, Vector3.forward); // Correctly aligned!

            // Set start speed in the launch direction of the launcher's Y+ axis
            rb.velocity = transform.up * startVelocity;
        }
        else
        {
            // Default start speed if no launch point is provided
            rb.velocity = transform.up * startVelocity;
        }
    }

    private void FixedUpdate()
    {
        // Calculate acceleration through throttle / thrust
        Vector3 forwardForce = transform.up * throttleInKilonewtons * 1000f; // Convert kN to N - Y direction
        rb.AddForce(forwardForce);

        if (target != null)
        {
            // Logic for steering ...see below

            Debug.DrawLine(transform.position, target.position, Color.red);
        }
    }
}

Variant 1: Berechung

// Direction to the target in world coordinates
Vector3 targetDirection = target.position - transform.position;
Debug.Log("Targetdirection:" + targetDirection.ToString());

// Transform the direction into the local space of the rocket
Vector3 localTargetDirection = transform.InverseTransformDirection(targetDirection);
Debug.Log("local Targetdirection:" + localTargetDirection.ToString());
// Determine the local basis vectors of the rocket
Vector3 localForward = transform.up;
Vector3 localUp = transform.forward;
Vector3 localRight = transform.right;

// Calculate the torque around the local Y+ axis to rotate the rocket towards the target
Vector3 torque = Vector3.Cross(localTargetDirection.normalized, localUp) * steeringThrust * 1000f;
Debug.Log("Torque:" + torque.ToString());

// Apply the torques
rb.AddTorque(torque);

[/code}

Variant 2: P-D Regler
[code]
// Calculate the error (deviation from the target)
Vector3 targetDirection = target.position - transform.position;
float error = Vector3.Angle(transform.forward, targetDirection);

// Calculate the rate of change of the error
float errorDerivative = (error - previousError) / Time.fixedDeltaTime;

// Calculate the control signal with the PD controller
float controlSignal = proportionalGain * error + derivativeGain * errorDerivative;

// Limit the control signal to avoid excessive reactions
controlSignal = Mathf.Clamp(controlSignal, -maxControlSignal, maxControlSignal);

// Apply the control signal to the rocket
rb.AddTorque(transform.up * controlSignal);

// Save the current error for the next calculation
previousError = error;

Both do strange things. The rocket does start correctly from the launcher, but the rotation is very wild—it almost looks like tumbling. If the target and the launcher are aligned, it’s minimal, but if the rocket has to fly in an arc, it goes completely out of control. Somehow, I have a mental block here. If any of you have an idea, suggestions would be very welcome.

Otherwise … have fun with your creative work in Unity!

2 Likes

Hello Mathric,

the concept of applying real physics and a real PD controller sounds very interesting.

The main issue that I see with the control strategy is applying always the control torque on a constant direction relative to your of your rocket (localUp in Variant 1 and transform.up in Variant). The deviation of your rocket to the target should have 2 degrees of freedom if your simulation runs in 3 dimensions and therefore you would need to apply torque around Up and Right to control this 2 degrees of freedom. The resulting torque would in a direction not be necessarily aligned to the local up of local right (it should be mostly a composition).
To calculate the vector to apply the torque I would calculate the normal Vector of the plane containing your Forward direction and the target direction (you want your forward Vector to move in that plane until it aligns with targetDirection). And the normal of the plane containing both vectors is the result of the cross product. (I think with a drawing would be easier to explain :smile:)
Would write on line 39 something like:

rb.AddTorque(Vector3.Cross(transform.forward, targetDirection). normalized * controlSignal);

This should give you the torque in the necessary direction.

However I think probably you might have also an issue with the use of Vector3.Angle. This function will give you always a positive angle back. This is probably ok for the proportional part of your controller but should be an issue for the derivative part (2 different deviations in consecutive frames can give you the same angle, but and your derivative controller would think the angle didn’t change).

2 Likes

Hey, yeah. I got your point. First I tried with only one direction. I made some progress … but still not finished. Still testing with 1 rotation at a time. If this works, the 2 angles needed can easily be calculated.

I’m at the point that I understand the amount of force and weight influence the rotation a lot.

9856350--1419501--upload_2024-5-26_22-16-8.png

Code is attached. Pythonscript for visu too.

9856350–1419486–rocketSimulator.cs (9.29 KB)
9856350–1419495–showdata2.zip (3.08 KB)

and as I’ve found out… the strange things are “just” cause too much force makes it spin like a tornado :-).