Simulating turret rotation

Hello,

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 :frowning:

This is test scene:

The red sphere object should be a spaceship in the final game. It has these settings:

The turret object is a child of the red sphere and it has these settings:

I also checked this Unity “Rotation Constrain” component, but I couldn’t manage to make it work. Maybe it doesn’t do what I think it does.

I hope that someone can save my day :frowning:

100000001218704--1121384--clear.png

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.

https://www.gamedev.net/articles/programming/math-and-physics/do-we-really-need-quaternions-r1199/

You may wish to consider alternative methods of rotation.

Hi, no the game is 3D and that’s why it feels so difficult for me to handle these rotations. Sorry for not being specific.

Would you mind sharing an example so that I can get a glimpse of this works?

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. :eyes:

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.

Only suffers gimbal lock IF

eulerangles = eulerangles + vector
eulerangles = eulerangles - vector

these two ^^ do not =
eulerangles = vector

Make sure to reset to zero at 360 degrees

It basically how the inspector does it so it’s really useful in that respect.

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?? :frowning:

It also doesn’t work when baseRotation = transform.rotation;

Turret aiming/rotating in 3D, like a tank turret or pillbox anti-aircraft turret:

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

I tried to adapt your code to mine and I got this:

public class TurretBrainAndTargeter : MonoBehaviour
    {

        [SerializeField] private ShipModel shipModel;

        [SerializeField] Transform traverse;
        [SerializeField] Transform elevate;

        float TraverseSnappiness = 2.0f;
        float ElevationSnappiness = 2.0f;

        float DesiredTraverse;
        float DesiredElevation;

        Vector3 DeltaToTarget;

        void Update()
        {
            UpdateTargetTracking();
            Slew();
        }

        public void UpdateTargetTracking()
        {
            if (shipModel.target != null)
            {
                DeltaToTarget = shipModel.target.position - traverse.position;
                DesiredTraverse = Mathf.Atan2(DeltaToTarget.x, DeltaToTarget.z) * Mathf.Rad2Deg;
                float range = Mathf.Sqrt(DeltaToTarget.x * DeltaToTarget.x + DeltaToTarget.z * DeltaToTarget.z);
                DesiredElevation = Mathf.Atan2(-DeltaToTarget.y, range) * Mathf.Rad2Deg;
            }
        }

        void Slew()
        {

            Quaternion trav = Quaternion.Euler(0, DesiredTraverse, 0);
            Quaternion elev = Quaternion.Euler(DesiredElevation, 0, 0);

            traverse.localRotation = Quaternion.Lerp(traverse.localRotation, trav, TraverseSnappiness * Time.deltaTime);
            elevate.localRotation = Quaternion.Lerp(elevate.localRotation, elev, ElevationSnappiness * Time.deltaTime);

            if (shipModel.target != null)
            {
                float angle = Vector3.Angle(DeltaToTarget, elevate.forward);
            }
        }

    }

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:

        [SerializeField] Transform traverse;
        [SerializeField] Transform elevate;

I don’t quite get why tho. I have only one object and my instict tells me that doing what you is solving the same problem in a different way.

Also, you account for gravity in your code, which I don’t need to. In fact I removed it.

Also I can’t make sense of your math, but I’m quite ignorante there :sweat_smile: But if I don’t get a glimpse of how it works I can’t fix it.

I’m very surprised that this problem is so difficult to solve code-wise…

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.