How can I make my character's head, point towards another player's head?

I’ve been trying to do this for around 30 minutes. This is my code so far, but it’s not working correctly. Sometimes the head points correctly, the other time it points to the opposite side it should be pointing at. Also, it doesn’t point on the Y axis but only on the X axis (basically horizontally only):

local targetHeadCF = lastTarget.Head.CFrame;
local targetHeadLook = targetHeadCF.LookVector;
local ToObjectSpace = CharacterPivot:ToObjectSpace(targetHeadCF).LookVector;
local cfTarget = CFrame.new(0, NeckC0Origin.Y, 0) * CFrame.Angles(0, ToObjectSpace.X, 0) * CFrame.Angles(-ToObjectSpace.Y, 0, 0);
Neck.C0 = Neck.C0:Lerp(cfTarget, 0.1);

Can anyone help me? I’ve been banging my head over this for a while now.

Edit:
Gonna specify:

  • CharacterPivot = My character’s pivot
  • targetHeadCF = The CFrame of the head of the other player (the one my head should point to)
  • NeckC0Origin = The original C0 of my head’s neck
5 Likes

I have developed a function to make world space CFrames, like CFrame.lookAt apply when using Motor6D or welds C0 property. This function should work with all rigs as it takes into account of the C1 as explained and derived through here.

I prefer using world space CFrames as it’s easier to visualize rather than thinking in object space relative to the Part0.

local neckJoint = script.Parent

local target = Workspace.Target

local Head = script.Parent.Parent
local upperTorso = neckJoint.Part0

local function worldCFrameToC0ObjectSpace(motor6DJoint,worldCFrame)
	local part1CF = motor6DJoint.Part1.CFrame
	local c1Store = motor6DJoint.C1
	local c0Store = motor6DJoint.C0
	local relativeToPart1 =c0Store*c1Store:Inverse()*part1CF:Inverse()*worldCFrame*c1Store
	relativeToPart1 -= relativeToPart1.Position
	
	local goalC0CFrame = relativeToPart1+c0Store.Position--New orientation but keep old C0 joint position
	return goalC0CFrame
end


while true do
	task.wait()
	local goalCF = CFrame.lookAt(Head.Position, target.Position, upperTorso.CFrame.UpVector)
	neckJoint.C0 = worldCFrameToC0ObjectSpace(neckJoint,goalCF)
end

Test file:

WeldToWorldSpaceTest.rbxl (44.3 KB)

If you wish to restrict it then that’s another story, but you make the goalCF relative to the torso via ToObjectSpace, Convert to orientation using :ToOrientation, then clamp the angles, then reconstruct using CFrame.fromOrientation. I have done this within my turret module.

29 Likes

Thank you so much! This is great. I’m sure it will help other people too. Thanks again.

2 Likes

Is there a way to add limits to this?

1 Like

You are a life saver; I’ve been trying to figure it out for days now!

1 Like

Update since this function got popular.

Turns out this weird derivation is the same as the one in my article I had to reverse engineer it to figure it out

c0Store * c1Store:Inverse() * part1CF:Inverse() 
--Try to derive it from the original weld equation
part1.CFrame * C1 == Part0.CFrame * C0
--Inverse the c1 both sides
part1.CFrame  == Part0.CFrame * C0 *C1:Inverse()
--Inverse the part0 both sides
Part0.CFrame:Inverse()*part1.CFrame  ==  C0 *C1:Inverse()

--The formmula I somehow got from this post
	local relativeToPart1 = c0Store * c1Store:Inverse() * part1CF:Inverse() * goalWorldCFrame * c1Store

--subsitute the  C0 *C1:Inverse()
	local relativeToPart1 = Part0.CFrame:Inverse()*part1.CFrame * part1CF:Inverse() * goalWorldCFrame * c1Store

--Resolve
	local relativeToPart1 = Part0.CFrame:Inverse()* goalWorldCFrame * c1Store

Somehow it became the same as the one in my article, so the one in my post was the overcomplicated version but its the same thing haha :coefficients:

1 Like

Late response, but I have found a method of doing so that seems to work well; and I’d like to share it for any other developers that have, or will come across this problem in the future.

NOTE: This code was made for the intention of use with R6, to make it compatible with R15 just replace Torso with UpperTorso.

First of all, we take those two lines of code in that while loop and initialise a variable for the transformed CFrame relative to C0.

local GoalCF = CFrame.lookAt(BodyPart.Position, LookTarget.Position, Torso.CFrame.UpVector)
local RelativeGoalCF = worldCFrameToC0ObjectSpace(Joint, GoalCF)

Then, retrieve the X, Y, and Z components of the new orientation of C0 using :ToOrientation. Keep in mind that the values returned are in radians.

local rx, ry, rz = RelativeGoalCF:ToOrientation()

I’m going to focus on ry for this, as this is the component that lets the head move sideways (hence the component we want to add limits to so NPCs don’t twist their heads like owls)

ry changes depending on the direction GoalCF’s LookVector is in. However, the way it changes is different depending on whether the target position is ABOVE or BELOW the character’s head’s CFrame.

I discovered this through some experimentation. We now have to determine whether the target’s position is at a higher or lower elevation than the head.

local TargetAbove = (LookTarget.Position - BodyPart.Position).Unit.Y > 0

This is where things get math-heavy. We now need to determine whether the target’s position is to the LEFT or the RIGHT of the character. This can be done by applying the sine function to ry.

Once that is done, we have now split this problem into 4 distinct cases. This is where you apply math.clamp() to keep ry inclusive within a range.

To change the limits, just change the numbers inside the math.rad() brackets to suit your needs.

if TargetAbove then -- Target is above head
	if math.sin(ry) > 0 then
		ry = math.clamp(ry, math.rad(0), math.rad(90))
	else
		ry = math.clamp(ry, math.rad(-90), math.rad(0))
	end
else -- Target is below head (the clamping has to be different because the way ry changes is different)
	if math.sin(ry) > 0 then
		ry = math.clamp(ry, math.rad(90), math.rad(180))
	else
		ry = math.clamp(ry, math.rad(-180), math.rad(-90))
	end
end

Almost there! Now that ry has been modified to be constrained to certain ranges, we can now convert these rotation components back into a CFrame using CFrame.fromOrientation().

Just remember, you also need to multiply this by the original position component of C0, or else C0’s position will be reset to its relative origin (0, 0, 0). Once that’s done, apply the new CFrame to C0.

local FinalGoalCF = (CFrame.new(Joint.C0.Position) * CFrame.fromOrientation(rx, ry, rz))
Joint.C0 = Joint.C0:Lerp(FinalGoalCF, 0.2) -- Lerp for an extra 'smooth' effect. Otherwise, set the CFrame directly.

Wrap all that up in a while loop or bind it to RunService, and it should work!

FULL CODE BLOCK
local GoalCF = CFrame.lookAt(BodyPart.Position, LookTarget.Position, Torso.CFrame.UpVector)
local RelativeGoalCF = worldCFrameToC0ObjectSpace(Joint, GoalCF)

local rx, ry, rz = RelativeGoalCF:ToOrientation()
local TargetAbove = (LookTarget.Position - BodyPart.Position).Unit.Y > 0

if TargetAbove then -- Target is above head
	if math.sin(ry) > 0 then
		ry = math.clamp(ry, math.rad(0), math.rad(90))
	else
		ry = math.clamp(ry, math.rad(-90), math.rad(0))
	end
else -- Target is below head (the clamping has to be different because the way ry changes is different)
	if math.sin(ry) > 0 then
		ry = math.clamp(ry, math.rad(90), math.rad(180))
	else
		ry = math.clamp(ry, math.rad(-180), math.rad(-90))
	end
end

local FinalGoalCF = (CFrame.new(Joint.C0.Position) * CFrame.fromOrientation(rx, ry, rz))
Joint.C0 = Joint.C0:Lerp(FinalGoalCF, 0.2) -- Lerp for an extra 'smooth' effect. Otherwise, set the CFrame directly.

UPDATE: There is a minor bug in which if the target position is directly in front of the character’s head, the head may twitch sideways occasionally. To solve this, check if there is sufficient change in the dot product to justify changing the CFrame of the motor6D’s C0.

if math.round((BodyPart.CFrame.LookVector:Dot((LookTarget.Position - BodyPart.Position).Unit))*1000)/1000 < 0.999 then
	local FinalGoalCF = (CFrame.new(Joint.C0.Position) * CFrame.fromOrientation(rx, ry, rz))
	Joint.C0 = Joint.C0:Lerp(FinalGoalCF, 0.2)
end
1 Like