How to make a turret rotate with limit in rotation

I want to make a turret that receive command to aim at a target, then try its best to rotate towards that direction.
It has a limit in rotation, so that it won’t rotate past the limit.


The cyan lines are debug ray to show where the limit should be.
Currently, my code was able to limit it at the correct angle. However, when I change target from A1 to A2 (in picture), it rotates by picking the shortest way, which is wrong since it will make it rotate past the limit to go there. It should rotate the other way.

My code, this run in FixedUpdate():

public void AimAt(Vector3 targetPosition)
        {
            Vector3 directionFromHorizontalPivot = targetPosition - horizontalPivot.position;
            Vector3 horizontalDirection = new Vector3(directionFromHorizontalPivot.x, 0f, directionFromHorizontalPivot.z).normalized;

            if (horizontalPivot != null)
            {
                float targetHorizontalAngle = Mathf.Atan2(horizontalDirection.x, horizontalDirection.z) * Mathf.Rad2Deg;

                float currentHorizontalAngle = Mathf.DeltaAngle(_startingHorizontalEuler.y, targetHorizontalAngle);

                float clampedHorizontalAngle = currentHorizontalAngle;

                if (currentHorizontalAngle < horizontalLimit.x || currentHorizontalAngle > horizontalLimit.y)
                {
                    float distanceToMinLimit = Mathf.Abs(Mathf.DeltaAngle(targetHorizontalAngle, horizontalLimit.x));
                    float distanceToMaxLimit = Mathf.Abs(Mathf.DeltaAngle(targetHorizontalAngle, horizontalLimit.y));

                    if (distanceToMinLimit < distanceToMaxLimit)
                    {
                        clampedHorizontalAngle = horizontalLimit.x;
                        rotationDirection = currentHorizontalAngle > clampedHorizontalAngle ? -1 : 1;
                    }
                    else
                    {
                        clampedHorizontalAngle = horizontalLimit.y;
                        rotationDirection = currentHorizontalAngle > clampedHorizontalAngle ? 1 : -1;
                    }
                }

                float finalHorizontalAngle = _startingHorizontalEuler.y + clampedHorizontalAngle;

                Quaternion targetHorizontalRotation = Quaternion.Euler(0f, finalHorizontalAngle, 0f);
                Quaternion nextRotation = Quaternion.RotateTowards(
                    horizontalPivot.rotation, targetHorizontalRotation, rotationDirection * horizontalSpeed * Time.deltaTime
                );

                float nextHorizontalAngle = Mathf.DeltaAngle(_startingHorizontalEuler.y, nextRotation.eulerAngles.y);

                if (nextHorizontalAngle < horizontalLimit.x || nextHorizontalAngle > horizontalLimit.y)
                {
                    rotationDirection *= -1;
                    nextRotation = Quaternion.RotateTowards(
                        horizontalPivot.rotation, targetHorizontalRotation, rotationDirection * horizontalSpeed * Time.deltaTime
                    );
                }

                horizontalPivot.rotation = nextRotation;

                Debug.Log(
                    $"Target: {targetHorizontalAngle}, Current: {currentHorizontalAngle}, Clamped: {clampedHorizontalAngle}, Direction: {rotationDirection}");
            }

            if (verticalPivot != null)
            {
                Vector3 directionFromVerticalPivot = targetPosition - verticalPivot.position;

                float targetVerticalAngle = Vector3.SignedAngle(horizontalDirection, directionFromVerticalPivot, verticalPivot.right);

                float clampedVerticalAngle = Mathf.Clamp(targetVerticalAngle, -verticalLimit, verticalLimit);

                Quaternion targetVerticalRotation = Quaternion.Euler(clampedVerticalAngle, 0f, 0f);
                verticalPivot.localRotation = Quaternion.RotateTowards(
                    verticalPivot.localRotation, targetVerticalRotation, verticalSpeed * Time.deltaTime
                );

                Debug.DrawRay(verticalPivot.position, verticalPivot.forward * 50f, Color.magenta, 2f);
            }
        }

How to make it respect the limit and rotate in the correct direction? I want this system to be able to work with changeable horizontalLimit, which can be anything like (0, 270), (-90, 160), or (150, 270)…

I found a solution.

I store the current target as a command, it contains target position and rotate direction of the turret.

When receive fire target position, check for correct rotate direction by starting from the target direction, increment angle by small step multiple times on both rotate direction (clockwise and counter-clockwise), check each angle for valid angle (that is within defined angle limit).

When it found the first valid angle, pick that as rotate direction and save it to current command.

Then in FixedUpdate, the turret can rotate to try to aim at the target with the correct rotate direction.

i came across this and thought it was unresolved, there is an option to mark it as resolved, just saying, ty

I’m still finding a way to code this turret. What I posted above made the turret pick the correct limit if the target is outside of the limit, but if the target is inside the limit, it still tries to go the shorter way instead of finding the correct rotation direction. And also setting the limit to negative value break it.

I found out that the Euler angle is normalized to range (0, 360), so if the limit I set is (-180, 40), 220 degree of rotation, normalizing it to (0, 360) range would make -180 angle become 180, which make the limit become (40, 180), 140 degree of rotation in the wrong direction.

I’m thinking of calculating delta angle to a fixed transform Anchor (that is same position as the horizontal pivot but does not rotate) to keep track of the delta angle, and calculate angle in range (-180, 180) instead of (0, 360) in world rotation. All the angle won’t use world rotation any more but will be calculated by delta angle to Anchor.forward vector.

I still need more ideas, other solutions I found online does not address the issue of picking the correct direction to rotate to make the turret respect the limit and not cross the limit and rotate the shorter way to face the target.

finally, the code is

		private void RotateHorizontalPivot()
        {
            if (_currentCommand.FireCommand == null || _currentCommand.HorizontalRotateDone) return; // No target to rotate towards

            // Calculate the target horizontal angle
            Vector3 directionFromHorizontalPivot = _targetPosition - horizontalPivot.position;
            Vector3 horizontalDirection = new Vector3(directionFromHorizontalPivot.x, 0f, directionFromHorizontalPivot.z).normalized;
            Vector3 anchorHorizontalDir = new Vector3(anchorForward.forward.x, 0f, anchorForward.forward.z).normalized;
            _horizontalDirection = horizontalDirection;

            float anchorToTarget = Vector3.SignedAngle(anchorHorizontalDir, horizontalDirection, Vector3.up);
            float anchorToCurrent = Vector3.SignedAngle(anchorHorizontalDir, horizontalPivot.forward, Vector3.up);
            float currentToTarget = Vector3.SignedAngle(horizontalPivot.forward, horizontalDirection, Vector3.up);

            //check anchor to target is in limit
            if (anchorToTarget < horizontalLimit.x || anchorToTarget > horizontalLimit.y)
            {
                //outside limit, find the closest limit to rotate to
                Vector3 lowerLimitDirection = Quaternion.Euler(0f, horizontalLimit.x, 0f) * anchorForward.forward;
                Vector3 upperLimitDirection = Quaternion.Euler(0f, horizontalLimit.y, 0f) * anchorForward.forward;
                if (Vector3.Angle(horizontalDirection, lowerLimitDirection) < Vector3.Angle(horizontalDirection, upperLimitDirection))
                {
                    //lower limit is closer
                    horizontalDirection = lowerLimitDirection;
                }
                else
                {
                    horizontalDirection = upperLimitDirection;
                }

                anchorToTarget = Vector3.SignedAngle(anchorHorizontalDir, horizontalDirection, Vector3.up);
                currentToTarget = Vector3.SignedAngle(horizontalPivot.forward, horizontalDirection, Vector3.up);
            }

            //check sign delta angle to find correct rotate direction that have to across the anchor forward and not across the limit
            if (Mathf.Sign(anchorToTarget) == Mathf.Sign(anchorToCurrent))
            {
                if (anchorToCurrent < anchorToTarget)
                {
                    _currentCommand.RotateDirection = 1; //clockwise
                }
                else
                {
                    _currentCommand.RotateDirection = -1; //counter
                }
            }
            else
            {
                if (Mathf.Sign(anchorToCurrent) > 0)
                {
                    _currentCommand.RotateDirection = -1;
                }
                else
                {
                    _currentCommand.RotateDirection = 1; //counter
                }
            }

            Debug.Log(
                $"[*] anchorToTarget: {anchorToTarget}, anchorToCurrent:{anchorToCurrent}, currentToTarget:{currentToTarget}, rotate dir: {_currentCommand.RotateDirection}");

            // Calculate rotation step
            float rotationStep = horizontalSpeed * Time.fixedDeltaTime * _currentCommand.RotateDirection;
            float nextAngle = anchorToCurrent + rotationStep;

            // Check if the next angle will reach or overshoot the target
            if (HasReachedTarget(anchorToCurrent, nextAngle, anchorToTarget, rotationDirection))
            {
                CompleteRotation();
                return;
            }

            horizontalPivot.localRotation = Quaternion.Euler(0f, nextAngle, 0f);
        }
		
		private bool HasReachedTarget(float currentAngle, float nextAngle, float targetAngle, int direction)
        {
            if (direction == 1) // Clockwise
            {
                return currentAngle <= targetAngle && nextAngle >= targetAngle;
            }
            else if (direction == -1) // Counterclockwise
            {
                return currentAngle >= targetAngle && nextAngle <= targetAngle;
            }

            return false; // Invalid direction
        }