Restrict rotation of a gameObject that rotates using torque of ForceMode.VelocityChange

I have an airplane whose roll, pitch and yaw are controlled by gamepad input, which provides a torque:

private void UpdateSteering(float dt)
{
    var speed = Mathf.Max(0, localVelocity.z);
    var steeringPower = steeringCurve.Evaluate(speed);
    var targetAngularVelocity = Vector3.Scale(controlInput, turnSpeed * steeringPower);
    var angularVelocity = localAngularVelocity * Mathf.Rad2Deg;

    var correction = new Vector3(                                                                                  //resultant torque from user input
        CalculateSteering(dt, angularVelocity.x, targetAngularVelocity.x, turnAcceleration.x * steeringPower),
        CalculateSteering(dt, angularVelocity.y, targetAngularVelocity.y, turnAcceleration.y * steeringPower),
        CalculateSteering(dt, angularVelocity.z, targetAngularVelocity.z, turnAcceleration.z * steeringPower)
        );

    var yawRoll = new Vector3(0f, 0f, yawRollCurve.Evaluate(-inputYaw));                     //roll produced due to yaw input
    var rollYaw = new Vector3(0f, rollYawCurve.Evaluate(Mathf.Asin(drone.transform.right.y) * Mathf.Rad2Deg), 0f);       //yaw produced due to roll input

    correction += yawRoll + rollYaw;

    rb.AddRelativeTorque(correction * Mathf.Deg2Rad, ForceMode.VelocityChange);   //ignore rigidbody mass
}
private float CalculateSteering(float dt, float angularVelocity, float targetAngularVelocity, float acceleration)
{
    var error = targetAngularVelocity - angularVelocity;
    var accel = acceleration * dt;
    return Mathf.Clamp(error, -accel, accel);
}

Where controlInput is a Vector3 created using inputs from 3 axes of the gamepad. This works completely fine for my needs. However I am working on an “easy mode” that can be toggled on and off on the fly. Easy mode has two functions:

  1. Restrict the roll and pitch to certain values (such as +/- 30 deg roll and +/- 20 deg pitch).
  2. When input roll or pitch is zero, gradually reset the airplane to neutral on the corresponding axis.
    This is the method I am using when it is toggled on:
private void EasyMode()
{
    float maxEasyModePitch = Mathf.Sin(this.maxEasyModePitch * Mathf.Deg2Rad); //this.maxEasyModePitch = 20
    float maxEasyModeRoll = Mathf.Sin(this.maxEasyModeRoll * Mathf.Deg2Rad); //this.maxEasyModeRoll = 30

    if (transform.right.y >= maxEasyModeRoll || transform.right.y <= -maxEasyModeRoll)
    {
        rollInput = 0; // remove input roll when max roll reached
        Debug.Log("Reached roll limit");
    }

    if (transform.forward.y >= maxEasyModePitch || transform.forward.y <= -maxEasyModePitch)
    {
        pitchInput = 0; // remove input pitch when max pitch reached
        Debug.Log("Reached pitch limit");
    }

    if (Mathf.Abs(changedRoll) <= 0.1) //joystick either released or max roll reached
        transform.Rotate(transform.forward, -transform.right.y * rollCorrectionSpeed * Time.fixedDeltaTime, Space.Self);

    if (Mathf.Abs(changedPitch) <= 0.1) //joystick either released or max pitch reached
        transform.Rotate(transform.right, transform.forward.y * pitchCorrectionSpeed * Time.fixedDeltaTime, Space.Self);
}

The code works perfectly as long as the aircraft’s heading (i.e, rotation around y-axis) is zero. As the heading changes, it behaves more and more unexpectedly. At 180 degrees of heading, the reset rotation for both pitch and roll are in the complete opposite direction of what they should be.

From my understanding, transform.forward and transform.right should be reliable for specifying the direction of rotation for my use case, but they are clearly not. However the Debug.Log statements for reaching roll and pitch limits have shown me that transform.forward.y and transform right.y (which happen to be equal to the sines of airplane’s current pitch and roll angles respectively) are reliable for checking when the maximum pitch/roll are reached.

If anybody could explain why this is the case and/or provide an alternate solution, I would greatly appreciate it.

I have also tried this function that I found here which directly modifies the transform.rotation quaternion, as a different way of restricting roll and pitch. The input for the function is q = transform.rotation, bounds = new Vector3 (this.maxEasyModePitch, 180, this.maxEasyModeRoll).

private Quaternion ClampRotation(Quaternion q, Vector3 bounds)
{
    q.x /= q.w;
    q.y /= q.w;
    q.z /= q.w;
    q.w = 1.0f;

    float angleX = 2.0f * Mathf.Rad2Deg * Mathf.Atan(q.x);
    angleX = Mathf.Clamp(angleX, -bounds.x, bounds.x);
    q.x = Mathf.Tan(0.5f * Mathf.Deg2Rad * angleX);

    float angleY = 2.0f * Mathf.Rad2Deg * Mathf.Atan(q.y);
    angleY = Mathf.Clamp(angleY, -bounds.y, bounds.y);
    q.y = Mathf.Tan(0.5f * Mathf.Deg2Rad * angleY);

    float angleZ = 2.0f * Mathf.Rad2Deg * Mathf.Atan(q.z);
    angleZ = Mathf.Clamp(angleZ, -bounds.z, bounds.z);
    q.z = Mathf.Tan(0.5f * Mathf.Deg2Rad * angleZ);

    return q;
}

This function correctly restricts roll and pitch, but once again only at zero heading. As the heading changes, the restricted roll and pitch get closer to zero. They both reach actual zero at 180 degrees of heading.

NOTE: All the above functions were called in FixedUpdate.

You need to use Space.World instead of Space.Self in your Rotate calls in EasyMode() function.

Also, I don’t see in your EasyMode function where your actually clamping the rotation, you’re just trying to gradually reset the rotation.

1 Like

Changing it to Space.World solves everything! Thanks so much.

I also edited the code to clarify how I am restricting the roll and pitch - I am reducing input of the corresponding axis to zero when the max angle is reached.

If you want to continue adding angular forces to rotate in easy mode while also limiting the rotation, you can do this:

        Quaternion rot = Quaternion.FromToRotation(transform.up, Vector3.up);
        rb.AddTorque((new Vector3(rot.x, 0, rot.z)-rb.angularVelocity*0.2f)*2f); // constantly apply a force to straighten out the rigidbody
        rb.AddRelativeTorque(Input.GetAxis("Vertical"),0,-Input.GetAxisRaw("Horizontal")); // apply the input force to pitch and roll
2 Likes