How to make this script, that lets NPCs look at players, less jumpy?

Hello,

I’ve been working on a script that makes NPCs in my game look over towards the player when they are within 15 studs of the player. My problem is that it is really jumpy when the player walks in/out of the 15 stud radius and when the player walks in certain points around the NPC. I am trying to make the NPC transition back to its default state smoothly if the player walks out of the radius, and make it not jump around when I walk around it.

Here is the script that I have currently, I referenced this page while I was making the script.

local NpcFold = workspace["NPC Holder"]
local player = game.Players.LocalPlayer

function limitAngles(CF,X,Y)
X = math.rad(X); Y = math.rad(Y)
local x, y = CF:toEulerAnglesXYZ()
return CFrame.new(CF.p) * CFrame.Angles(
	math.clamp(x, 0, X),
	math.clamp(y, -Y, Y),
	0
)
end


game:GetService("RunService").Stepped:Connect(function()
local NPCs = NpcFold:GetChildren()
if player.Character then
	for i,v in pairs(NPCs) do
		if v:IsA("Folder") then
			local group = v:FindFirstChild(" ")
			if group then
				local torso = group.HumanoidRootPart
				local localTorso = player.Character:FindFirstChild("HumanoidRootPart")
				local waist = group.UpperTorso.Waist
				local neck = group.Head.Neck
				if localTorso then
					local char = player.Character
						local magnitude = (torso.Position - localTorso.Position).magnitude
				    	if magnitude < 20 then
				    		local RotationOffset = (group.PrimaryPart.CFrame-group.PrimaryPart.CFrame.p):inverse()
				    		local realPos = -(group.PrimaryPart.CFrame.p - char.PrimaryPart.Position).unit *5000
				    		local targetCFrame = RotationOffset * CFrame.new(Vector3.new(0, 0, 0), realPos)
							waist.Transform = limitAngles(targetCFrame,0,20)							
		    				local RotationOffset = (group.UpperTorso.CFrame-group.UpperTorso.CFrame.p):inverse()
		    				local realPos = -(group.UpperTorso.CFrame.p - char.PrimaryPart.Position).unit *5000
	    					local targetCFrame = RotationOffset * CFrame.new(Vector3.new(0, 0, 0), realPos)
    						neck.Transform = limitAngles(targetCFrame,30,65)
    					end
    				end
    			end
    		end
    	end
	end
end)

Here is a video of the problem that I’m having
YouTube Link
As you can see, it’s jumpy looking and doesn’t look that good.

Here is an example of what I’m trying to do, this is from Egg Hunt 2018.
YouTube Link

Any help is appreciated, thanks. Let me know if I need to provide any more information.

6 Likes

Lerping is your best friend here:

-- Outside of your stepped loop,
local currentCFrame1 = -- current waist cframe
local currentCFrame2 = -- ^ but for neck

-- the smaller it is, 
-- the smoother and slower the transition
local alpha1 = 0.1
local alpha2 = 0.1

-- in your stepped loop,
currentCFrame1 = currentCFrame1:Lerp(limitAngles(targetCFrame,0,20),alpha1)
waist.Transform = currentCFrame1

currentCFrame2 = currentCFrame2:Lerp(limitAngles(targetCFrame,30,65), alpha2)
neck.Transform = currentCFrame2

You would want to add an else statement to your magnitude check to return the NPC to it’s original CFrame, and break the connection after maybe 10 iterations and the player not interfering in the meantime.

7 Likes

Thank you! And by Waist CFrame, do you mean the CFrame of the UpperTorso? Because I thought that Motor6D didn’t have CFrame?

Motor6Ds are pretty much welds, so they have a .C0 and .C1 property for offsetting.

4 Likes

Now I have another issue, when I try to walk towards it and into the radius, it moves all of the parts in from some other place.

Here is my script as of now

local NpcFold = workspace["NPC Holder"]
local player = game.Players.LocalPlayer

local NPCFolders = {}

for i,v in pairs(NpcFold:GetChildren()) do
local char = v:FindFirstChild(" ")
if char then
	local npc = {
		currentCFrame1 = char.HumanoidRootPart.CFrame,
		currentCFrame2 = char.Head.CFrame,
		default1 = char.HumanoidRootPart.CFrame,
		default2 = char.Head.CFrame,
		iteration = 0
	}
	NPCFolders[v.Name] = npc
end
end

function limitAngles(CF,X,Y)
X = math.rad(X); Y = math.rad(Y)
local x, y = CF:toEulerAnglesXYZ()
return CFrame.new(CF.p) * CFrame.Angles(
	math.clamp(x, 0, X),
	math.clamp(y, -Y, Y),
	0
)
end




game:GetService("RunService").Stepped:Connect(function()
local NPCs = NpcFold:GetChildren()
if player.Character then
	for i,v in pairs(NPCs) do
		if v:IsA("Folder") then
			local group = v:FindFirstChild(" ")
			if group then
				local torso = group.HumanoidRootPart
				local localTorso = player.Character:FindFirstChild("HumanoidRootPart")
				local waist = group.UpperTorso.Waist
				local neck = group.Head.Neck
				if localTorso then
					local char = player.Character
					local magnitude = (torso.Position - localTorso.Position).magnitude
					if magnitude < 20 then
						local alpha1 = 0.1
						local alpha2 = 0.1
						
						local NPC = NPCFolders[v.Name]
						
						local RotationOffset = (group.PrimaryPart.CFrame-group.PrimaryPart.CFrame.p):inverse()
						local realPos = -(group.PrimaryPart.CFrame.p - char.PrimaryPart.Position).unit *5000
						local targetCFrame = RotationOffset * CFrame.new(Vector3.new(0, 0, 0), realPos)
						NPC.currentCFrame1 = NPC.currentCFrame1:Lerp(limitAngles(targetCFrame,10,25),alpha1) 
						waist.Transform = NPC.currentCFrame1
						
						local RotationOffset = (group.UpperTorso.CFrame-group.UpperTorso.CFrame.p):inverse()
						local realPos = -(group.UpperTorso.CFrame.p - char.PrimaryPart.Position).unit *5000
						local targetCFrame = RotationOffset * CFrame.new(Vector3.new(0, 0, 0), realPos)
						NPC.currentCFrame2 = NPC.currentCFrame2:Lerp(limitAngles(targetCFrame,40,75), alpha2)
						neck.Transform = NPC.currentCFrame2
						NPC.iteration = 0
					else
						local npc = NPCFolders[v.Name]
						if npc.iteration < 10 then
							print("iterating")
							npc.iteration = npc.iteration + 1
							local alpha1 = 0.1
							local alpha2 = 0.1
							
							local NPC = NPCFolders[v.Name]
							
							
							NPC.currentCFrame1 = NPC.currentCFrame1:Lerp(NPC.default1,alpha1) 
							waist.Transform = NPC.currentCFrame1
							
							
							NPC.currentCFrame2 = NPC.currentCFrame2:Lerp(NPC.default2, alpha2)
							neck.Transform = NPC.currentCFrame2
						else
							print("not moving")
						end
					end
				end
			end
		end
	end
end
end)

Here is a video that shows what I mean, it works fine when I am either in or out of the radius, but when I transition between in and out it does that.

Edit: Nevermind, I solved it.

Just a small idea, but you can do

NPC:SetPrimaryPartCFrame(CFrame.new(NPC.CFrame, Player:GetPrimaryPartCFrame())

This would aim the entire NPC at the player, but you could easily change that. Also you could use tweenservice or lerping instead of just setprimarypartcframe to do smoother moves.

2 Likes

How did you solve the problem?

@goldenstein64’s solution worked for me

I mean the problem with the Lerping in this video https://www.youtube.com/watch?v=UJWvG1roAs4&feature=youtu.be

you wrote Edit: Nevermind, I solved it.
and I am asking how you solved that Lerping problem. I’m curious

My best guess is that when he was saving the data for the NPC in the NPCFolders table near the beginning of his script:

He probably just switched out the char.?.CFrame in the default keys to just CFrame.new(), similarly with currentCFrame.

if char then 
	local npc = { 
		currentCFrame1 = CFrame.new(), 
		currentCFrame2 = CFrame.new(), 
		default1 = CFrame.new(), 
		default2 = CFrame.new(), 
		iteration = 0 
	} 
	NPCFolders[v.Name] = npc 
end

The reason for why this would’ve worked (given my guess is right) is because the Waist.Transform CFrame is a very different value from HumanoidRootPart.CFrame. One describes a relative orientation from part to part in T-pose while the other describes an absolute location in space.
In the video, the torso and head most likely flew off to the their absolute positions marked by the default keys, but relative to their Transforms.
I am not entirely sure though, this is just a guess. It could be that he had to solve a problem that evolved from this too, or that he chose to use Waist.Transform as a default value instead of CFrame.new().

2 Likes

oh that makes a lot of sense. Also thank you for the good description :grinning:

1 Like

So, I have been looking into how to make this with only one script that effects all NPC’s in the folder. While doing so, I found a flaw in the code when you loop through the elements in the folder

In the old method, you looped through the folder then used the first for loop to set as v.Name which then overrides itself over and over again. this completely discards the chat for loop which is the loop you need. this creates a table for each NPC and stores it in NPCFolders.

if char then 
	local npc = { 
		currentCFrame1 = char.HumanoidRootPart.CFrame, 
		currentCFrame2 = char.Head.CFrame, 
		default1 = char.HumanoidRootPart.CFrame, 
		default2 = char.Head.CFrame, 
		iteration = 0 
	} 

NPCFolders[v.Name] = npc

end

New Version:

for i,v in pairs(NpcFold:GetChildren()) do
	for i,char in pairs(v:GetChildren()) do
		if char then 
			local npc = { 
				currentCFrame1 = CFrame.new(), 
				currentCFrame2 = CFrame.new(), 
				default1 = CFrame.new(), 
				default2 = CFrame.new(), 
				iteration = 0 
			} 
			NPCFolders[char.Name] = npc 
		end	
	end
end

I am working on a game and I would like to shout you two out (goldenstein64 and Tybearic) for helping me with the Lerping and general structure of the script.

4 Likes