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:
- Restrict the roll and pitch to certain values (such as +/- 30 deg roll and +/- 20 deg pitch).
- 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.