Limiting Part's Turn Rate

Hi, so I want to make a part that looks at a player and tracks their movements, but I do not know how to limit part’s turn rate.

As you can see, the part tracks the player flawlessly and is always catching up to him, which is what I don’t want. I still want the part to track, but I want it so it can’t catch up to players that are moving at higher speeds. I am also trying to avoid Roblox’s physics and Tweens, only CFrames.

Any help is greatly appreciated.

2 Likes

You can do it by adding a debounce

local turnRate = 3 --Every 3 seconds the part rotates to face the character
local lastTurn = tick() --The last time the part rotated

Then just add an if statement before your rotate code that checks if the last time the part rotated is greater than or equal to the turnRate

if tick() - lastTurn >= turnRate then
    lastTurn = tick() --Update the last time the part rotated
    --Your rotate code here
end

Adjust turnRate to your liking, hope this helps

1 Like

The reply above me does not seem to have understood what you really want.

For this, you have to use a heartbeat + lerp combination.

RunService.Heartbeat:Connect(function(DT)

	local X, Y, Z = OriginalLookVectorCFrame:ToOrientation()
	local TargetRotation = Vector3.new(X, Y, Z)
	
	local CurrentRotation = Part.Orientation
	
	Part.Orientation = CurrentRotation:Lerp(CurrentRotation, DT)

end)

Should be something like this, let me know if I missed any important details.

Thanks for you quick reply.
Could you tell me what OriginalLookVectorCFrame variable should be assigned to? I cannot get it through my head.
Thank you.

1 Like

It should be the part CFrame on the video you sent.

I still can’t understand, could you please point out whats wrong?

local turnPart = script.Parent
local OriginalLookVectorCFrame = turnPart.CFrame
local target = workspace.VectorMan_Real.HumanoidRootPart

local RunService = game:GetService("RunService")

RunService.Heartbeat:Connect(function(DT)

	local X, Y, Z = OriginalLookVectorCFrame:ToOrientation()
	local TargetRotation = Vector3.new(X, Y, Z)
	
	local CurrentRotation = turnPart.Orientation

	turnPart.Orientation = CurrentRotation:Lerp(CurrentRotation, DT)

end)

Thank you again

2 Likes

Sorry for any confusion, should not have left it so unexplained. Let me know if there are any issues : D

local turnPart = script.Parent
local target = workspace.VectorMan_Real.HumanoidRootPart

local RunService = game:GetService("RunService")

RunService.Heartbeat:Connect(function(DT)

	local OriginalLookAtCFrame = CFrame.lookAt(turnPart.CFrame.Position, target.Position)

	local X, Y, Z = OriginalLookVectorCFrame:ToOrientation()
	local TargetRotation = Vector3.new(X, Y, Z)
	
	local CurrentRotation = turnPart.Orientation

	turnPart.Orientation = CurrentRotation:Lerp(CurrentRotation, DT)

end)

Well the output is clean, but the part doesn’t move.

REALLY SORRY i made a simple mistake while lerping :frowning:

local turnPart = script.Parent
local target = workspace.VectorMan_Real.HumanoidRootPart

local RunService = game:GetService("RunService")

RunService.Heartbeat:Connect(function(DT)

	local OriginalLookAtCFrame = CFrame.lookAt(turnPart.CFrame.Position, target.Position)

	local X, Y, Z = OriginalLookVectorCFrame:ToOrientation()
	local TargetRotation = Vector3.new(X, Y, Z)
	
	local CurrentRotation = turnPart.Orientation

	turnPart.Orientation = CurrentRotation:Lerp(TargetRotation, DT)

end)

should work now hopefully :smiley:

runService.Heartbeat:Connect(function(dt: number)
      part.CFrame = part.CFrame:Lerp(CFrame.new(part.Position, rootPart.Position), .2 * dt * 60)
end)

This script lerps part CFrame to Goal by 20% each 1 frame
Goal is the part looking to the root part CFrame translation
Lerp percentage is .2 * frame time * 60, so it should be 0.2 each 1 frame (0.0166 secs), as it would be in a 60 fps client (different framerates would still make it turn in the same time it would take for a 60 fps client), you can change .2 to a higher number to make it turn faster or to a lower number to make it turn slower

2 Likes

I’m not sure that’s what the OP is asking for. To me “limiting” the turning speed implies clamping to a maximum turn rate. He states that he does not want the part to keep up with “players that are moving at higher speeds”, but the solution you give will result in the part not keeping up with players at any non-zero speed, since the part is always asymptotically approaching a goal orientation. It will lag behind in following any player by an amount proportional to their speed.

A second possible problem is that CFrame:Lerp is a Slerp. It’s going to interpolate both the yaw and pitch of that part simultaneously. It may be that he wants only the yaw turn rate limited, and for the pitch to track the vertical (jumping) without limitation, it’s not clear from the description.

If the goal is to track the player’s horizontal position, perfectly up to some maximum turn rate of the Part, then the way to go about that is to get the angle between the direction to the player this frame and last frame, clamp it to the desired max turn rate (max yaw change of the part per frame) and then construct clamped rotation CFrame using CFrame.fromAxisAngle( Vector3.yAxis, clampedAngle). The up-down (pitch change) of the part can be left unclamped, or clamped to an entirely separate max turn rate. Just do the rotation math for the yaw and pitch rotations separately and then multiply them together at the end.

The last things to consider are what happens when the character goes past the 180-degrees singularity, either by running fast or by crossing through the pole (if possible). Using just CFrame math, like Lerp, the part will reverse direction, always trying to take the shortest path to pointing at the character. Is this OK, or do you want the part to keep turning the same direction the player ran around it? If so, you need to manually track the winding angle, so tht it can count past 180 degrees. If there is nothing stopping the player from running directly under the part, you have to consider that singularity too, i.e. does that count always as running around 180 degrees clockwise, counter-clockwise, or does it use the last non-singular direction of the player?

What I came up with uses two scripts, so sorry if you don’t want to have to use two.

In one script, I placed under ServerScriptService, used to create the follower object:

local trackingSpeed = 15 --how many studs you want it to move per second

game.Players.PlayerAdded:Connect(function(player)
	player.CharacterAdded:Connect(function(character)
		local follower = Instance.new("Part", character)
		follower.Name = "Follower"
		follower.CanCollide = false
		follower.Transparency = 1
		follower.Anchored = true
		follower.Size = Vector3.new(1,1,1)
		
		game:GetService("RunService").Stepped:Connect(function(_, dt)
			local direction = character.PrimaryPart.Position - follower.Position
			if direction.Magnitude > 1 then --to prevent jittery behavior
				direction = direction.Unit
			end
			follower.CFrame = follower.CFrame + (direction * trackingSpeed * dt)
		end)
	end)
end)

The first variable, trackingSpeed is just as the comment says, how many studs per second it will move.
You can change that in the loop if you don’t want the player to be able to get too far away from the follower. Something like this probably

game:GetService("RunService").Stepped:Connect(function(_, dt)
	local direction = character.PrimaryPart.Position - follower.Position
	local amountToMove = trackingSpeed + direction.Magnitude / 5 -- 100 studs increases the studs per second by 20, 25 increases it by 5, etc.
	if direction.Magnitude > 1 then --to prevent jittery behavior
		direction = direction.Unit
	end
	follower.CFrame = follower.CFrame + (direction * trackingSpeed * dt)
end)

As for the second script, that controls the rotation of the part:

local function getNearestPlayer(position)
	local Players = game:GetService("Players")
	local nearestPlayer = nil
	local shortestDistance = math.huge

	for _, player in Players:GetPlayers() do
		if player.Character and player.Character.PrimaryPart then
			local distance = (player.Character.PrimaryPart.Position - position).Magnitude
			if distance < shortestDistance then
				shortestDistance = distance
				nearestPlayer = player
			end
		end
	end

	return nearestPlayer
end

while task.wait() do
	local player = getNearestPlayer(script.Parent.Position)
	if not player then continue end
	if player.Character then
		if player.Character.Follower then
			script.Parent.CFrame = CFrame.lookAt(script.Parent.Position, player.Character.Follower.Position)
		end
	end
end

All that the second script does is make the part rotate to face the follower object instead of the player, so all you would have to do is replace the target of the part to be the follower.

The follower itself, also has a target you can change.

local direction = character.PrimaryPart.Position - follower.Position
                            ^^^^^^^^^^^

These two scripts don’t actually apply rotation limits, but have something that mimicks them.
The farther away you get, the less it acts like a limit on rotation.

Tell me if you have any problems.

I want it to be limited equally vertically and horizontally, however it would be nice if you could customize these.

I am planning it to be able to find the shortest path to it’s target.

I have another thing you can try out.

local function limitTurn(cframe, target, maxturnrate, dt)
	local look = cframe.LookVector
	local targetlook = (target - cframe.Position).Unit
	local angledif = math.acos(math.clamp(look:Dot(targetlook), -1, 1))
	local turnrate = math.rad(maxturnrate) * dt
	local newlook
	
	if angledif <= turnrate then
		newlook = targetlook
	else
		local axis = look:Cross(targetlook).Unit
		if axis.Magnitude == 0 then
			axis = Vector3.new(0, 1, 0)
		end
		newlook = CFrame.fromAxisAngle(axis, turnrate) * look
	end

	return CFrame.lookAt(cframe.Position, cframe.Position + newlook)
end

game:GetService("RunService").Stepped:Connect(function(_,dt)
	local lookat = limitTurn(script.Parent.CFrame, game.Players:GetPlayers()[1].Character.PrimaryPart.Position, 50, dt)
	script.Parent.CFrame = lookat
end)

Compared to my other code, this one should be able to properly limit the vertical and horizontal rotation regardless of the distance between the part and the player. It uses the shortest path to the target.

You are a genius, thank you so much. Your code works exacly how I wanted.

1 Like

But I have one question, how can I separate vertical and horizontal axis?
This is so I can have different maximum speeds for each. And also right now both axis get to the target at the same time, however it would be much better if, for example, the vertical axis could get there faster if it can instead of syncing with the horizontal (or the opposite). It’s quite difficult to explain and even more difficult to understand. Sorry for all of these requests, you are the only person so far that could implement this.

I thought of some stuff and figured out how to do it.
First of all, limiting horizontal movement is pretty easy, you just substitute the y value in the target position with the rotating parts y position. It allows for the individual change of the horizontal axis.

local lookat = limitTurn(part.CFrame, Vector3.new(root.Position.X, part.Position.Y, root.Position.Z), 10, dt)
part.CFrame = lookat

In this case, “part” is the rotating part, “root” is just the humanoidrootpart.

For vertical rotation, it was a lot harder for me to find something that worked.
What I came up with for doing it was,

getting the distance between the part and the root part,

then multiplying the unit of the rotating part’s lookvector, by the distance.

I would replace the Y in the vector3 obtained from that with the root part’s Y position.

I would then add on the X and Z positions of the rotating part to that vector3, so it is properly offset.

In code, this is what that looks like.

local dist = (Vector3.new(root.Position.X, root.Position.Y, root.Position.Z) - part.Position).Magnitude
local lookVector = part.CFrame.LookVector.Unit * dist
local XYlookvector = Vector3.new(lookVector.X, root.Position.Y, lookVector.Z)
local lookat = limitTurn(part.CFrame, Vector3.new(part.Position.X, 0, part.Position.Z) + XYlookvector, 50, dt)
part.CFrame = lookat

So, if I wanted to do both simultaneously, it would look like this.

game:GetService("RunService").Stepped:Connect(function(_,dt)
	local player = game.Players:GetPlayers()[1]
	local character = player.Character
	local root = character.HumanoidRootPart
	
	local dist = (Vector3.new(root.Position.X, root.Position.Y, root.Position.Z) - part.Position).Magnitude
	local lookVector = part.CFrame.LookVector.Unit * dist
	local XYlookvector = Vector3.new(lookVector.X, root.Position.Y, lookVector.Z)
	
    local lookat = limitTurn(part.CFrame, Vector3.new(part.Position.X, 0, part.Position.Z) + XYlookvector, 50, dt)
	part.CFrame = lookat

	lookat = limitTurn(part.CFrame, Vector3.new(root.Position.X, part.Position.Y, root.Position.Z), 10, dt)
	part.CFrame = lookat
end)

Since the function remains unchanged, this is all you would have to do.
If you have problems with it, just let me know.

Edit: Apparently the order of horizontal and vertical rotation changes mattered. If horizontal went first, it could only rotate horizontally when it vertically rotated. Once I swapped the order, to being
vertical->horizontal It seemed to work fine.

It works well, however there are a few bugs. If you set horizontal max turn rate more than vertical, the vertical will go back to the original rotation after its done finding the player. And also specifically horizontal axis are now smooth, but I want it to be linear. Still, thank you.

Yeah, I had a feeling it probably wouldn’t work too well given the order of the changes mattered.
The problem with It was that if the horizontal was faster at turning then the vertical, the y target of the horizontal would override the y target of the vertical. Because of that, I swapped the method to clamping the turns.

local function angleDiff(current, target)
	local diff = target - current
	
	if diff > math.pi then
		diff = diff - 2 * math.pi
	elseif diff < -math.pi then
		diff = diff + 2 * math.pi
	end
	
	return diff
end

local function limitTurn(cframe, target, maxH, maxV, dt)
	local currentYaw, currentPitch, _ = cframe:ToEulerAnglesYXZ()
	local targetYaw, targetPitch, _ = target:ToEulerAnglesYXZ()

	local yawDiff = angleDiff(currentYaw, targetYaw)
	local pitchDiff = angleDiff(currentPitch, targetPitch)

	local maxYaw = math.rad(maxH) * dt
	local maxPitch = math.rad(maxV) * dt

	local clampedYaw = math.clamp(yawDiff, -maxYaw, maxYaw)
	local clampedPitch = math.clamp(pitchDiff, -maxPitch, maxPitch)

	local newYaw = currentYaw + clampedYaw
	local newPitch = currentPitch + clampedPitch

	return CFrame.new(cframe.Position) * CFrame.fromEulerAnglesYXZ(newYaw, newPitch, 0)
end

game:GetService("RunService").Stepped:Connect(function(_, dt)
	local player = game.Players:GetPlayers()[1]
	local character = player.Character
	local root = character.HumanoidRootPart

	local lookat = limitTurn(part.CFrame, CFrame.lookAt(part.Position, root.Position), 10, 50, dt)
	part.CFrame = lookat
end)

The angle difference function is because if you moved past a certain point, it would take a longer way of turning towards you. It does cause a slight change of the z axis in its orientation, but its in the thousandths of a degree so it doesn’t really change anything, and it always stays around zero, so its not like its increasing.

Thanks a lot! I needed all of this for a sentry gun I’m making. Now I just need to figure out how to apply this script to my actual sentry gun, but I don’t think this will be very problematic for me. Thank you very much.