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