I’m currently working on an RTS, and I’m trying to implement a unit turnrate such that units have a maximum angle that they can turn per frame. For example, perhaps I want the maximum turnrate of a unit to be up to 5 degrees per frame.

The unit (or at least, a part which represents the direction the unit is facing) should essentially turn towards a target over an interval of time instead of instantly turning as they currently do.

A proper implementation would have to execute these partial turns in the most efficient direction, rather than, for example, always turning in one direction only (this is the point I’ve been able to reach). I feel like this should be an easy problem, but I’m having trouble figuring it out. Any help would be appreciated. Thanks!

you could try using lerp. Although im not sure how to limit it by degrees per frame you can specify how many seconds you want it to take to reach the goal and It will smoothly tween the turret to the end cframe you want.

local turret = --wherever the turret is
local startTurretCFrame = --Whatever the current cframe is
local goalTurretCFrame = --The target cframe you want to turret to move towards
local turretTurnDuration = --The number of seconds you want the turn to last
local turnStartTime = tick() --The time that you started turning
local steppedEvent
steppedEvent = game:GetService("RunService").Stepped:Connect(function()
local timeElapsed = tick() - turnStartTime
local alpha = timeElapsed * (1/turretTurnDuration)
local newTurretCFrame = startTurretCFrame:lerp(goalTurretCFrame, alpha)
turret.CFrame = newTurretCFrame
--This part is optional if your dont want the stepped event running all the time
if timeElapsed >= turretTurnDuration then
steppedEvent:Disconnect()
end
end)

I think the simplest way to go about this is slerping while using the dot product as a ratio:

-- in a loop:
local currentCFrame = Unit.CFrame
local targetCFrame = -- target CFrame
-- gives an angle in radians
local angle = math.acos(currentCFrame.LookVector:Dot(targetCFrame.LookVector))
local maxTurnRate = math.rad(5) -- 5 degrees
local lerpTurn = math.min(maxTurnRate/angle, 1) -- take a ratio
Unit.CFrame = currentCFrame:Lerp(targetCFrame, lerpTurn)

If you want to understand it, slerping is basically lerping but for angles, so if you take the angle using the dot product between their look vectors, you can divide it into an interval of 5 degrees by taking a ratio of the increment over the total and lerping by that. The math.min is just there to make sure the current CFrame doesn’t overshoot the target.

You can also use a Rodrigues’ Rotation, explained by @EgoMoose. This looks like a mountain to climb, so look at it when you have tons and tons of free time.

The other, probably harder way of doing this, if your unit can’t look up or down, is by storing an angle between the X and Z axes and adding/subtracting 5 degrees from the angle itself:

local unitAngle = 0
-- in a loop:
local targetAngle = -- target angle
local deltaAngle = targetAngle - unitAngle
-- clamp between [-180, 180] for most efficient turn
if math.abs(deltaAngle) > 180 then
deltaAngle = deltaAngle - math.sign(deltaAngle)*360
end
local maxTurnRate = 5
unitAngle = unitAngle + math.sign(deltaAngle)*math.min(maxTurnRate, math.abs(deltaAngle))

You can then apply this angle using CFrame.Angles: