I’m working on something like a spaceship game and I’m struggling a lot with turret rotation.
I wrote some code that follows a target and works to some degree:
public class TurretRotateTowardComponent : MonoBehaviour
{
[SerializeField] private TurretModel model;
void Update()
{
// Determine which direction to rotate towards
Vector3 targetDirection = model.targetTransform.position - transform.position;
// The step size is equal to speed times frame time.
float singleStep = model.speed * Time.deltaTime;
// Rotate the forward vector towards the targetTransform direction by one step
Vector3 newDirection = Vector3.RotateTowards(transform.forward, targetDirection, singleStep, 0.0f);
// Calculate a rotation a step closer to the targetTransform and applies rotation to this object
transform.rotation = Quaternion.LookRotation(newDirection);
// Constrain turret rotation
transform.rotation = Quaternion.Euler(transform.eulerAngles.x, Math.Clamp(transform.eulerAngles.y, model.yMin, model.yMax), 0);
}
}
But as the spaceship rotates, the turret rotation breaks. I tried to understand this quaternion thingy but I’m struggling with that too.
I did great stuff so far, but this is the only thing I couldn’t manage to do it myself
It looks like your gameplay is limited to 2D? Then doing the calculations in 2D, and only at the end converting to a 3D rotation around the axis you need, will make things much simpler. In 2D you only have a single angle you need to concern yourself with, in 3D rotations are much more troublesome.
One issue is that euler angles are neither unambiguous (there are multiple angle values for each distinct rotation) nor easily composable (manipulating a single axis can lead to very different results depending on the situation). I suspect this is what’s causing your issues, rotating around y might not be the rotation you expect in this specific situation.
One relatively simple way to limit rotations with quaternions (albeit only on all axes at once), is to use Quaternion.Angle to get the angle from the base to your current rotation and, if the angle is above your limit, use Quaternion.Slerp with your base / current rotations and maxAngle / currentAngle as t value to get your clamped rotation.
Something like this? Haven’t tested it but used a similar method before.
/// <summary>
/// Clamp a rotation relative to a reference base rotation
/// </summary>
/// <param name="baseRotation">The base rotation to clamp relvative to</param>
/// <param name="currentRotation">The current rotation to limit</param>
/// <param name="maxAngle">The maximum angle to clamp to in degrees (0 < maxAngle < 180°)</param>
/// <returns>The clamped rotation</returns>
Quaternion ClampRotation(Quaternion baseRotation, Quaternion currentRotation, float maxAngle)
{
// Check if the current angle exceeds the limit
var angle = Quaternion.Angle(baseRotation, currentRotation);
if (angle <= maxAngle) {
// Inside limit, return rotation unchanged
return currentRotation;
}
// Clamp rotation by slerping from the base rotation to the limit,
// keeping the direction of the current rotation.
// Since slerp is uniform, the angle between the result and baseRotation will be (t * angle).
return Quaternion.Slerp(baseRotation, currentRotation, maxAngle / angle);
}
You may wish to contribute constructively, e.g. by providing examples of methods to clamp rotations in 3D without using quaternions. And a link that doesn’t point to a post that has been retracted.
i am a wishing man
To apply a transform Euler angle create a private or public vector 3. Modify this vector on the desired axis and make your Euler angles = this vector.
This suffers from gimbal lock and is why - alongside in general - you shouldn’t use euler angles for rotations. They really only really serve as a human readable manner to read rotations.
The Unity API has pretty much everything you need to handle rotations without even knowing what goes under the hood with quaternions. For example if you want to rotate on a particular axis you use Quaternion.AngleAxis.
Believe it or not, I’m still struggling with this problem. The final code looks like this:
public class LookAtTarget : MonoBehaviour
{
[SerializeField] private ShipModel shipModel;
[SerializeField] private float rotationSpeed = 1f;
[SerializeField] private float maxAngle = 90;
private Quaternion baseRotation;
void Start()
{
baseRotation = transform.localRotation;
}
void Update()
{
if (shipModel.target != null)
{
Vector3 direction = shipModel.target.position - transform.position;
Quaternion targetRotation = Quaternion.LookRotation(direction);
// Smoothly rotate towards the limited target rotation
transform.rotation = ClampRotation(baseRotation, targetRotation, maxAngle);
}
}
Quaternion ClampRotation(Quaternion baseRotation, Quaternion currentRotation, float maxAngle)
{
// Check if the current angle exceeds the limit
var angle = Quaternion.Angle(baseRotation, currentRotation);
if (angle <= maxAngle)
{
// Inside limit, return rotation unchanged
return currentRotation;
}
return Quaternion.Slerp(baseRotation, currentRotation, maxAngle / angle);
}
void OnDrawGizmos()
{
// Mostra solo i gizmos quando l'oggetto è selezionato
//if (!this.gameObject.activeInHierarchy || !Selection.Contains(this.gameObject)) return;
// Disegna i limiti di yaw e pitch come linee gialle
Gizmos.color = Color.yellow;
Vector3 front = Quaternion.AngleAxis(-maxAngle, transform.right) * transform.forward;
Vector3 back = Quaternion.AngleAxis(maxAngle, transform.right) * transform.forward;
Vector3 left = Quaternion.AngleAxis(-maxAngle, transform.up) * transform.forward;
Vector3 right = Quaternion.AngleAxis(maxAngle, transform.up) * transform.forward;
Gizmos.DrawLine(transform.position, transform.position + front);
Gizmos.DrawLine(transform.position, transform.position + back);
Gizmos.DrawLine(transform.position, transform.position + left);
Gizmos.DrawLine(transform.position, transform.position + right);
}
}
This code works perfectly when my spaceship has rotation {0, 0, 0} but as soon as I rotate the spaceship to {0, 0, 90} it breaks again and the turret looks / points inside the spaceship… Ideas??
Thanks for sharin. I’ll test it out. Do you have a dummy project where you show how this works or can you upload one? That would make everything easier
Funny enough, the behavior that I’ve got with my older code is the same as yours: when z < 0 and the ship has no rotation, the turret stops rotating. What’s different instead is the behavior when my ship is z=-90°: I get an odd behavior of the turret.
I find interesting that you have two different object to rotate the turret:
Yeah, you cannot mix local and global rotations. If you use global positions to calculate the look rotation, you also need to use transform.rotation as the base rotation.
Also, LookRotation takes a second up parameter that defaults to Vector3.up. If you have something upright like a human, that is correct, but if you have something that can tilt like a spaceship, you want to set the second parameter to your up axis (e.g. save transform.up together with your baseRotation or have a base reference transform that gives you the base rotation and up axis and which you can then rotate dynamically together with the turret).
Rotations can be surprisingly tricky. Limiting a rotation is conceptually simple, we have an intuitive understanding of how it should work, but the way rotations can wrap around and nest makes it difficult to express in code and it’s not at all simple to generalize the problem.
Kurt’s approach breaks it down into two 2D-rotations (traverse & elevate), which makes it a lot more manageable. I’d encourage you to try to learn what the math is doing, the approach can be used for many different problems and it would also help you understand what is not working in your setup.