I have a turret that rotates to track (point towards) a target transform in global space along the y axis, that code works fine, but I now want to constrain the turret’s rotation between two angle values (e.g. -90, 90).
I know I can use Mathf.Clamp(angle, -90, 90) to clamp the turret angle on the y axis, but that only works globally (clamping to 0 keeps the turret at the global 0 angle not relative to the parent). I want the turret to still track the target properly but also be clamped between the min/max angle limits locally. Also something to note is that the turret’s scene rotation makes it rotate locally on the x axis but on the y axis globally to move the same way.
Here’s my current turret movement code:
void RotateTurret()
{
Vector3 targetDirection = target.position - turret.transform.position;
float turretAngle;
if (reverseTurret)
{
turretAngle = Mathf.Atan2(targetDirection.x, targetDirection.z) * Mathf.Rad2Deg;
}
else
{
turretAngle = Mathf.Atan2(targetDirection.x, targetDirection.z) * Mathf.Rad2Deg + 180f;
}
float turretRotationAmount = turretSpeed * Time.deltaTime;
Quaternion currentRotation = turret.transform.rotation;
Quaternion targetTurretRotation = Quaternion.Euler(0f, turretAngle, 0f);
turret.transform.rotation = Quaternion.RotateTowards(currentRotation, targetTurretRotation, turretRotationAmount);
}
3 Answers
3
Last time I answered a similar question I ended up proposing code like one below. It solves these issues of angular limits in local spaces; you may find it useful.
public Transform target;
[SerializeField] Vector2
degreesPerSecond = new Vector2( 5 , 10 ) ,
gimbalMin = new Vector2( -40 , -80 ) ,
gimbalMax = new Vector2( 30 , 80 );
void FixedUpdate ()
{
var parent = transform.parent;
float deltaTime = Time.fixedDeltaTime;
Vector3 srcLocalDegrees = transform.localEulerAngles;
Quaternion targetRotation = Quaternion.LookRotation( ( target.position - transform.position ).normalized , parent.up );
Vector3 targetLocalDegrees = ( Quaternion.Inverse(parent.rotation) * targetRotation ).eulerAngles;
Vector3 dstLocalDegrees = new Vector3(
targetLocalDegrees.x>180
? Mathf.Max( targetLocalDegrees.x , 360f+gimbalMin.x )
: Mathf.Min( targetLocalDegrees.x , gimbalMax.x ) ,
targetLocalDegrees.y>180
? Mathf.Max( targetLocalDegrees.y , 360f+gimbalMin.y )
: Mathf.Min( targetLocalDegrees.y , gimbalMax.y ) ,
targetLocalDegrees.z
);
transform.localEulerAngles = new Vector3(
Mathf.MoveTowardsAngle( srcLocalDegrees.x , dstLocalDegrees.x , degreesPerSecond.x * deltaTime ) ,
Mathf.MoveTowardsAngle( srcLocalDegrees.y , dstLocalDegrees.y , degreesPerSecond.y * deltaTime ) ,
srcLocalDegrees.z
);
}
FULL SOURCE CODE: universal turret component · GitHub

So I tried modifying some of your code to work with my turret, but there are some weird things I can’t quite figure out. First of all it appears that it thinks the target is slightly right of where it actually is (since i’m shooting into a goal, this makes it just barely miss). Also if i add Debug.DrawLine(turret.transform.position, transform.InverseTransformPoint(targetLocalDegrees), Color.red); to the function then the line points to some random point in my scene, definitely NOT my desired target. Note my code is on my highest level parent in the hierachy, so i adjusted things accordingly. Also the angle limiting seems to behave weirdly, like not allowing the turret to move even if it’s within the correctly range. Note that my turret’s starting rotation is (90, 90, 90) to be in the orientation i want.
How I changed the code:
private void RotateTurret()
{
Transform parent = turret.transform.parent;
float deltaTime = Time.fixedDeltaTime;
Vector3 srcLocalDegrees = turret.transform.localEulerAngles;
Quaternion targetRotation = Quaternion.LookRotation((target.position - turret.transform.position).normalized , parent.up);
Vector3 targetLocalDegrees = (Quaternion.Inverse(parent.rotation) * targetRotation).eulerAngles;
Debug.DrawLine(turret.transform.position, transform.InverseTransformPoint(targetLocalDegrees), Color.red);
Vector3 dstLocalDegrees = new Vector3(
targetLocalDegrees.x > 180
? Mathf.Max(targetLocalDegrees.x, 360f + turretLimits.x)
: Mathf.Min(targetLocalDegrees.x, turretLimits.y),
targetLocalDegrees.y,
targetLocalDegrees.z
);
turret.transform.localEulerAngles = new Vector3(
Mathf.MoveTowardsAngle( srcLocalDegrees.x , dstLocalDegrees.x , turretSpeed * deltaTime ),
90f,
90f
);
}
This is what my line draw presented, the red cross is where the target actually is

Here’s something that worked for me, I haven’t done any refactoring on it so it’s pretty raw.
private void FixedUpdate()
{
if (player == null)
{
player = GameObject.FindWithTag("Player");
if (player == null)
{
Debug.LogError("Player Not Found!");
return;
}
}
{
aimPoint = player.GetComponentInChildren<AimPoint>();
if(aimPoint == null)
{
Debug.LogError("Player Aim Point Not Found.");
return;
}
}
float distance = Vector3.Distance(rb.position, player.transform.position);
if (distance + Mathf.Epsilon <= aggroRange + Mathf.Epsilon)
{
AimAtTarget();
}
//other non-related stuff.
private void AimAtTarget()
{
//aimPoint is an empty object within the target that can be passed to the script
//or pulled out of the targets game object
targetDirection = aimPoint.transform.position - baseTransform.position;
//Create a Quaternion to find where the relative position of the target is in the x
// and z planes.
Quaternion horizontalRotationToTarget = Quaternion.LookRotation(targetDirection, Vector3.up);
//The base is the part of the weapon that rotates around the y axis
baseTransform.rotation = Quaternion.Slerp(baseTransform.rotation, horizontalRotationToTarget, turnSpeed * Time.fixedDeltaTime);
Vector3 horizontalRotation = new Vector3(targetDirection.x, 0.0f, targetDirection.z);
float angle = Vector3.Angle(horizontalRotation, targetDirection);
Quaternion pitchRotation = Quaternion.AngleAxis(angle, Vector3.right);
pivotPoint.localRotation = Quaternion.Slerp(pivotPoint.localRotation,
pitchRotation, (turnSpeed * 2) * Time.fixedDeltaTime);
// this part could get ugly depending on how many barrels your weapon has.
// if it only has one you don't need the loop
// I'm fudging a little because this is something I'm using for a four-barreled
// minigun. the launchPoints refer to the transform of an empty object I've
// placed just outside the muzzle of each barrel.
// I've just found that this makes it more accurate.
for (int i = 0; i < launchPoints.Count; i++)
{
Vector3 barrelDirection = aimPoint.transform.position -
launchPoints[i].position;
Quaternion barrelAim = Quaternion.LookRotation(barrelDirection, Vector3.up);
launchPoints[i].rotation = Quaternion.Slerp(launchPoints[i].rotation,
barrelAim, turnSpeed * Time.fixedDeltaTime);
}
It looks like you already have an answer to your question, but I felt like throwing in my $.02 and half hoping that someone has constructive criticism. And like I said, I haven’t run a profiler on this so I don’t know how performant it is, especially with that loop in there.