Moving and Rotating Rigidbody in a responsive way

I am creating a space game and want to move my ship in a very responsive way with only little smoothing. Since I want collision and physics, I use rigidbody and collider.
I will explain my steps first:
The translation is working fine so far with this code:

float x, y, z = 0;

private void HandleMovement() 
{
    //Thrust
    if (Math.Abs(thrustKey) > 0.01f)
    {
        x += thrustKey * thrustAcceleration;
        x = Mathf.Clamp(x, thrustMaxBackwardSpeed, thrustMaxForwardSpeed);
    }
    else 
    {
        x *= thrustGlideReduction;
    }

   //Strafe
    if (Math.Abs(strafeKey) > 0.01f)
    {
        y += strafeKey * strafeAcceleration;
        y = Mathf.Clamp(y, -maxStrafeSpeed, maxStrafeSpeed);
    }
    else 
    {
        y *= strafeGlideReduction;
    }

    //Up Down
    if (Math.Abs(upDownKey) > 0.01f)
    {
        z += upDownKey * upDownAcceleration;
        z = Mathf.Clamp(z, -maxUpDownSpeed, maxUpDownSpeed);
    }
    else
    {
        z *= upDownGlideReduction;
    }

    Vector3 newVelocity = new Vector3(y, z, x);
    rigidbody.velocity = newVelocity;
}

HandleMovement() is called in FixedUpdate. This is working fine so far, except when applying the velocity it is world based. When I am looking to the left and strafing forward, I am basically moving right.

The most complicated thing for me is the rotation. I took the same code as the translation and applied it to the rotation.

   private void HandleRotation()
   {
        //Pitch Yaw
        if (Math.Abs(pitchYawKey.sqrMagnitude) > 0.1f)
        {
            angularY += pitchYawKey.x * pitchYawAcceleration;
            angularY = Mathf.Clamp(angularY, -pitchYawMaxSpeed, pitchYawMaxSpeed);

            angularZ += pitchYawKey.y * pitchYawAcceleration;
            angularZ = Mathf.Clamp(angularZ, -pitchYawMaxSpeed, pitchYawMaxSpeed);                
        }
        else
        {
            angularY *= pitchYawReduction;
            angularZ *= pitchYawReduction;
        }

        Quaternion deltaPitch = Quaternion.Euler(transform.forward * -angularZ);
        Quaternion deltaYaw = Quaternion.Euler(transform.up * angularY);
        Quaternion targetRotation = rigidbody.transform.rotation * deltaYaw * deltaPitch;

        rigidbody.MoveRotation(targetRotation);
   }

I also want to add roll, but I haven’t come so far yet.
From this I have the following questions:

  1. What is the recommended way to move the rigidbody? I experienced some shaking sometimes. I have the feeling, that setting the velocity directly is not a good idea. Rigidbody.AddRelativeForce() results in floaty behaviour.
  2. What is the recommended way to rotate the rigidbody? I tried Rigidbody.AddRelativeTorque() but this results in very floaty and not precise control.
  3. I tried to compensate the sharp control with really high drag and angular drag values in the rigidbody. It didn’t work and I had a bad gut feeling about this. Would this be a viable option if I were to use other methods?
  4. If I’d use Rigidbody.AddRelativeForce and Rigidbody.AddRelativeTorque(), how can I modify the acceleration and the max speed independently? Do I need to clamp the velocity in every FixedUpdate?

If you can lead me in the right direction and tell me what is good and bad rigidbody practice, I’d be very thankful :slight_smile:

MoveRotation() and AddForce() is fine to use with a rigidbody, it lets the physics run on it’s own. I use MoveRotation() and not AddTorque() because it suits my game to control rotation directly (collisions will not alter the rotation), but if you want the rotation to also be simulated, do the other.
Why you feel that the ship is floaty must mean you have some of the parameters wrong, it is tricky to get right, but basically it is Engine Power, Mass, Drag and Gravity, Top speed is a function of those and not something you set. Just be sure about the scale of your world and objects before adjusting the parameters because distances will alter the numbers you use. It is not trivial to change later and keep the same behavior.

//By QueenInHeels
using System;
using UnityEngine;
using UnityEngine.InputSystem;

namespace Player 
{
[RequireComponent(typeof(Rigidbody))] //Gravity off; Interpolation to interpolate, Mass = 1, Drag = 0, AngularDrag = 0
[RequireComponent(typeof(Collider))]
[RequireComponent(typeof(PlayerInput))]  //Behaviour: Invoke Unity Events
public class SpaceshipController : MonoBehaviour
{
    [Header("Ship Movement Settings")]
    [Space]
    [Space]

    [Header("Thrusting")]
    [SerializeField]
    private float thrust = 1;
    [SerializeField]
    private float thrustImpulsStrength = 20f;
    [SerializeField]
    private float thrustBreaking = 10f;
    [SerializeField]
    private float maxThrustVelocity = 50f;
    [SerializeField]
    private float thrustImpulsThreshold = 5f;
    [Space]

    [Header("Strafing")]
    [SerializeField]
    private float strafe = 1;
    [SerializeField]
    private float strafeImpulsStrength = 20f;
    [SerializeField]
    private float strafeBreaking = 10f;
    [SerializeField]
    private float maxStrafeVelocity = 30f;
    [SerializeField]
    private float strafeImpulsThreshold = 5f;
    [Space]

    [Header("Up Down")]
    [SerializeField]
    private float upDown = 1;
    [SerializeField]
    private float upDownImpulsStrength = 20f;
    [SerializeField]
    private float upDownBreaking = 10f;
    [SerializeField]
    private float maxUpDownVelocity = 30f;
    [SerializeField]
    private float upDownImpulsThreshold = 5f;
    [Space]

    [Header("Roll")]
    [SerializeField]
    private float roll = 0.5f;
    [SerializeField]
    private float rollImpulsStrength = 0.5f;
    [SerializeField]
    private float rollBraking = 10f;
    [SerializeField]
    private float maxRollVelocity = 2f;
    [SerializeField]
    private float rollImpulsThreshold = .6f;
    [Space]

    [Header("Pitch")]
    [SerializeField]
    private float pitch = 0.4f;
    [SerializeField]
    private float pitchImpulsStrength = 0.4f;
    [SerializeField]
    private float pitchBraking = 10f;
    [SerializeField]
    private float maxPitchVelocity = 10f;
    [SerializeField]
    private float pitchImpulsThreshold = .1f;
    [Space]

    [Header("Yaw")]
    [SerializeField]
    private float yaw = 0.4f;
    [SerializeField]
    private float yawImpulsStrength = 0.4f;
    [SerializeField]
    private float yawBraking = 10f;
    [SerializeField]
    private float maxYawVelocity = 10f;
    [SerializeField]
    private float yawImpulsThreshold = .1f;

    private float thrustInput;
    private float strafeInput;
    private float upDownInput;
    private float rollInput;
    private Vector2 pitchYawInput;

    private Vector3 localVelocity;
    private Vector3 localAngularVelocity;

    private new Rigidbody rigidbody;

    private void Awake() 
    {
        rigidbody = GetComponent<Rigidbody>();
    }
    

    private void FixedUpdate() 
    {
        HandleMovement();
    }

    private void HandleMovement() 
    {
        localVelocity = transform.InverseTransformDirection(rigidbody.velocity);
        Console.WriteLine("Local velocity: " + localVelocity.ToString());

        localAngularVelocity = transform.InverseTransformDirection(rigidbody.angularVelocity);
        Console.WriteLine("Local Angular velocity: " + localAngularVelocity.ToString());

        HandleThrust();
        HandleUpDown();
        HandleStrafe();

        HandleRoll();
        HandlePitch();
        HandleYaw();
    }

    private void HandleThrust() 
    {
        if (thrustInput > 0.1f || thrustInput < -0.1f)
        {
            if (Math.Abs(localVelocity.z) < thrustImpulsThreshold || (Math.Sign(localVelocity.z) != Math.Sign(thrustInput)))
            {
                rigidbody.AddRelativeForce(Vector3.forward * thrustInput * thrustImpulsStrength, ForceMode.VelocityChange);
            }
            else if(Math.Abs(localVelocity.z) < maxThrustVelocity)
            {
                rigidbody.AddRelativeForce(Vector3.forward * thrustInput * thrust, ForceMode.Force);
            }
        }
        else
        {
            Vector3 brakingVector = Vector3.forward * -localVelocity.z * thrustBreaking;
            rigidbody.AddRelativeForce(brakingVector);
        }
    }

    private void HandleUpDown() 
    {
        if (upDownInput > 0.1f || upDownInput < -0.1f)
        {
            if (Math.Abs(localVelocity.y) < upDownImpulsThreshold || (Math.Sign(localVelocity.y) != Math.Sign(upDownInput)))
            {
                rigidbody.AddRelativeForce(Vector3.up * upDownInput * upDownImpulsStrength, ForceMode.VelocityChange);
            }
            else if(Math.Abs(localVelocity.y) < maxUpDownVelocity)
            {
                rigidbody.AddRelativeForce(Vector3.up * upDownInput * upDown, ForceMode.Force);
            }
        }
        else
        {
            Vector3 brakingVector = Vector3.up * -localVelocity.y * upDownBreaking;
            rigidbody.AddRelativeForce(brakingVector);
        }
    }

    private void HandleStrafe()
    {
        if (strafeInput > 0.1f || strafeInput < -0.1f)
        {
            if (Math.Abs(localVelocity.x) < strafeImpulsThreshold || (Math.Sign(localVelocity.x) != Math.Sign(strafeInput)))
            {
                rigidbody.AddRelativeForce(Vector3.right * strafeInput * strafeImpulsStrength, ForceMode.VelocityChange);
            }
            else if(Math.Abs(localVelocity.x) < maxStrafeVelocity)
            {
                rigidbody.AddRelativeForce(Vector3.right * strafeInput * strafe, ForceMode.Force);
            }
        }
        else
        {
            Vector3 brakingVector = Vector3.right * -localVelocity.x * strafeBreaking;
            rigidbody.AddRelativeForce(brakingVector);
        }
    }

    private void HandleRoll() 
    {
        if (rollInput > 0.1f || rollInput < -0.1f)
        {
            if (Math.Abs(localAngularVelocity.z) < rollImpulsThreshold || Math.Sign(localAngularVelocity.z) == Math.Sign(rollInput))
            {
                rigidbody.AddRelativeTorque(Vector3.back * rollInput * rollImpulsStrength, ForceMode.VelocityChange);
            }
            else if(Math.Abs(localAngularVelocity.z) < maxRollVelocity)
            {
                rigidbody.AddRelativeTorque(Vector3.back * rollInput * roll);
            }
        }
        else
        {
            Vector3 brakingVector = Vector3.back * localAngularVelocity.z * rollBraking;
            rigidbody.AddRelativeTorque(brakingVector);
        }            
    }

    private void HandlePitch() 
    {
        if (pitchYawInput.y > 0.1f || pitchYawInput.y < -0.1f)
        {
            if (Math.Abs(localAngularVelocity.x) < pitchImpulsThreshold || Math.Sign(-localAngularVelocity.x) != Math.Sign(pitchYawInput.y))
            {
                rigidbody.AddRelativeTorque(Vector3.right * Mathf.Clamp(-pitchYawInput.y, -1f, 1f) * pitchImpulsStrength, ForceMode.VelocityChange);
            }
            else if(Math.Abs(localAngularVelocity.x) < maxPitchVelocity)
            {
                rigidbody.AddRelativeTorque(Vector3.right * -pitchYawInput.y * pitch);
            }
        }
        else
        {
            Vector3 brakingVector = Vector3.right * -localAngularVelocity.x * pitchBraking;
            rigidbody.AddRelativeTorque(brakingVector);
        }
    }

    private void HandleYaw() 
    {
        if (pitchYawInput.x > 0.1f || pitchYawInput.x < -0.1f)
        {
            if (Math.Abs(localAngularVelocity.y) < yawImpulsThreshold || Math.Sign(localAngularVelocity.y) != Math.Sign(pitchYawInput.x))
            {
                rigidbody.AddRelativeTorque(Vector3.up * Mathf.Clamp(pitchYawInput.x, -1f, 1f) * yawImpulsStrength, ForceMode.VelocityChange);
            }
            else if(Math.Abs(localAngularVelocity.y) < maxYawVelocity)
            {
                rigidbody.AddRelativeTorque(Vector3.up * pitchYawInput.x * yaw);
            }
        }
        else
        {
            Vector3 brakingVector = Vector3.up * -localAngularVelocity.y * yawBraking;
            rigidbody.AddRelativeTorque(brakingVector);
        }
    }

    #region InputMethods

    public void OnThrust(InputAction.CallbackContext context) 
    {
        thrustInput = context.ReadValue<float>();
    }

    public void OnStrafe(InputAction.CallbackContext context)
    {
        strafeInput = context.ReadValue<float>();
    }

    public void OnUpDown(InputAction.CallbackContext context)
    {
        upDownInput = context.ReadValue<float>();
    }

    public void OnRoll(InputAction.CallbackContext context)
    {
        rollInput = context.ReadValue<float>();
    }

    public void OnPitchYaw(InputAction.CallbackContext context)
    {
        pitchYawInput = context.ReadValue<Vector2>();
    }

    #endregion

}
}

The braking is done by applying force in the opposite direction. Acceleration is done by using Rigidbody.AddForce and ForceMode set to VelocityChange. I am also using the new InputSystem. The region at the bottom is for this Input system :slight_smile: