Converting a look-vector to a rotation value

I am programming a turret that spins around its Y-axis. Once the turret sees a player in front, it will run a sub-routine in which the turret points itself at the player every frame for as long as the player is within line-of-sight. The turret exists out of a static base (blue in the image below) and a MeshPart (in red) that spins around.

image

The hierarchy of the turret is as follows:

image

Spinning the turret in its default state is done by increasing a rotation variable every frame, then rotating the attachment in the blue base, and then CFraming the red MeshPart such that its relative offset from the attachment remains consistent:

-- rotate the turret around
local dt = game:GetService("RunService").Heartbeat:Wait()
rotation = rotation + dt * ROTATION_SPEED
TurretAtt.Orientation = Vector3.new(0, math.deg(rotation), 0) -- rotate the attachment
MeshPart.CFrame = TurretAtt.WorldCFrame * turretOffset -- CFrame the red MeshPart accordingly

One oversight with this approach is that when a player is detected, the sub-routine is called which also rotates the turret around (to follow the player). When the sub-routine is finished and the code above is called again, the rotation value will cause the code to rotate the turret back to the orientation it was at when a player was first detected. This causes a sudden change in the CFrame of the turret which looks unnatural.

Instead, what should happen is that after the sub-routine finishes running, the rotation variable is set to some value corresponding to the current CFrame of the MeshPart at that moment. I was thinking that this could be done by reading the look-vector of the MeshPartā€™s CFrame. However, I am having trouble figuring out how I can use this vector to calculate what value I should assign to the rotation variable.

You could probably back out the rotation value from math.rad(TurretAtt.Orientation.Y) , it seems? Does the routine which tracks a player assign a value to TurretAtt.Orientation? Or does it use some other method?

Edit: Hereā€™s one take on this where the ā€œtrackingā€ routine uses the same Rotation variable as the idle spin routine.

Source Code

image

--!strict

local Rotation = 0;
local SpinSpeed = math.pi * 2 / 10; --Full rotation every 10 seconds.

local TurretBase = script.Parent.Base;
local TurretOrient = TurretBase.TurretOrient;
local TurretBody = script.Parent.TurretBody;

local function FindNearbyEnemy()
	local position = TurretBody.Position;
	local target: BasePart? = nil;
	local closestDistance = 20;
	for i, player in game:GetService("Players"):GetPlayers() do
		if player.Character then
			local rootPart = player.Character:FindFirstChild("HumanoidRootPart");
			if rootPart and rootPart:IsA("BasePart") then
				local distance = (position - rootPart.Position).Magnitude;
				if distance < closestDistance then
					target = rootPart;
					closestDistance = distance;
				end
			end
		end
	end
	return target;
end

game:GetService("RunService").Heartbeat:Connect(function(dt: number)
	local enemy = FindNearbyEnemy();
	if enemy then
		local positionRelative = TurretBase.CFrame:PointToObjectSpace(enemy.Position);
		Rotation = math.atan2(positionRelative.X, positionRelative.Z) - math.pi/2;
	else
		Rotation += dt * SpinSpeed;
	end
	TurretOrient.Orientation = Vector3.new(0, math.deg(Rotation), 0);
	TurretBody.CFrame = TurretOrient.WorldCFrame;
end);

I should have probably clarified the exact method I use to make the turret point towards the player. :sweat_smile:
The following line of code is basically how I make the turret look at the player, using the CFrame.new(v1, v2) constructor:

MeshPart.CFrame = CFrame.new(selfCFrame.Position, playerCFrame.Position) * turretOffset

Where selfCFrameā€™s Position property is equal to TurretAtt.WorldPosition and turretOffset is a very small (negligible) CFrame to properly align the visuals of the model. This means that the attachment ā€˜TurretAttā€™ (which is that green dot in the image) is not actually connecting the MeshPart through any constraints. When the sub-routine which changes the CFrame of the MeshPart is running, the attachment is not touched at all, so its orientation remains consistent.

The code in your edit looks very helpful. I hadnā€™t considered using PointToObjectSpace and math.atan2(). It seems that the PointToObjectSpace also ensures that this works when the turret is rotated. I will see if I can make my code work using your example!

Ah, OK, well getting rotation from the CFrame of the turret would be very similar to getting the rotation from the Position of the target. Put the turretā€™s CFrame in the object space of the Basis part, then use atan2.

local relativeVector = TurretAtt.Parent.CFrame:VectorToObjectSpace(MeshPart.CFrame.LookVector);
rotation = math.atan2(relativeVector.X, relativeVector.Z); --if the rotation is backwards, flip x and y. If the rotation is out of phase, add math.pi/2 until it's in-phase.
1 Like