Making an NPC face the player when near

  1. What do you want to achieve?
    I want to create a script that rotates the head of an NPC on the Y-axis so that it faces the direction of the player head.

  2. What is the issue?
    Currently, I am having issues getting my head to face correctly. I have tried using this post discussing how to do this, and I have adapted the solution in this snippet of my script:


local YOffset = neck.C0.Y

while wait() do
	if isWithinRadius(player) then
		CameraDirection = (CFrame.new(NPC_HRP.Position, hrp.Position):inverse() * NPC_HRP.CFrame ).lookVector * -1 
		local neckCFrame = neck.C0

		local Goal = CFrame.new(0, YOffset, 0) * CFrame.Angles(0, -math.asin(CameraDirection.x), 0) * CFrame.Angles(math.asin(CameraDirection.), 0, 0)

		local tweenInfo = TweenInfo.new(.2,Enum.EasingStyle.Quad,Enum.EasingDirection.Out)
		local Tween = TweenService:Create(neck, tweenInfo, {C0 = Goal})
		Tween:Play()
	end
end

but I still encounter this issue:

Iā€™ve tried lots of stuff, but as I donā€™t understand CFrame that well itā€™s difficult to understand whatā€™s happening and fix the issue through fiddling around. Could someone let me know what I am doing wrong here?

CFrame.lookAt(neck.C0.Position, hrp.Position) creates a cframe positioned at neck position, looking towards hrp position

then use ToOrientation to get the components of the orientation so you can get rid of the x and z rotations

local _, GoalRotationY, _ = CFrame.lookAt(neck.C0.Position, hrp.Position):ToOrientation()
local Goal = neck.C0.Position * CFrame.Angles(0, GoalRotationY, 0)

Sorry, disregard what I just said.
I have changed my code to this, according to what you said:

while wait() do
	if isWithinRadius(player) then
		local _, directiony, _ = CFrame.lookAt(neck.C0.Position, hrp.Position):ToOrientation()
		local Goal = CFrame.new(neck.C0.Position) * CFrame.Angles(math.rad(90), directiony, 0)
		local Tween = TweenService:Create(neck, tweenInfo, {C0 = Goal})
		Tween:Play()
	else 
		local Goal = default
		local Tween = TweenService:Create(neck, tweenInfo, {C0 = Goal})
		Tween:Play()
	end
end

I changed the first argument of CFrame.Angles because otherwise the characterā€™s head would just point downwards.
Now I am encountering an issue where the neck.C0 will rotate on the X axis, even though the directionY is in the Y spot in CFrame.Angles?

How can I fix this?

try putting the directiony in the first and third argument

Yes I have tried that. Here are the effects of that:

local Goal = CFrame.new(neck.C0.Position) * CFrame.Angles(directiony, 0, 0)

snip4

local Goal = CFrame.new(neck.C0.Position) * CFrame.Angles(0, 0, directiony)

snip3

Iā€™m pretty sure you can just use CFrame.lookAt, which might not result in what you want but at the moment itā€™s probably the best solution I can think of.

while wait() do
	if otherhead.Parent:FindFirstChild("Humanoid").FloorMaterial ~= Enum.Material.Air or otherhead.Parent:FindFirstChild("Humanoid").FloorMaterial ~= Enum.Material.Plastic then -- prevents NPC from breaking or floating when you jump or stand on it.
		NPChead.CFrame = CFrame.lookAt(NPChead.Position, otherhead.Position) -- NPChead being the NPC's head, and otherhead being the players head.
	end
end

The npcā€™s body moves with the head and Iā€™m aware of that, I just donā€™t have the brain power to think right now. So hopefully this will help you. This script can just give you an idea of what you could do.

NOTE: You canā€™t stand on Plastic because if you were to stand on the NPC it starts floating. So the NPC cant look at you while your standing on plastic unless you can find a workaround.

And yes I just realized, CFrame.lookAt is already in the code, but this is a more sophisticated version. Maybe when I can think better I could make a better version of this.

Okay cool, Iā€™ll see what I can do with that. Iā€™m not at my desktop right now but once I experiment Iā€™ll let you know if I can do anything. Since itā€™s a non-moving R6 NPC, I could just make the head invisible and use another rig, with only the head visible and run the .lookat script on that Alternate rig. Then Iā€™d just have to use :Dot() to decide what angles the NPC can look at you.

Okay, update: I think Iā€™ve made a pretty decent script. I am gonna share it here for any poor soul whoā€™s crawling through the devforum looking for an easy solution for this, but please suggest improvements because I donā€™t really know what Iā€™m doing.

At first I just used a localscript to manually change the head of a single NPC, which gave me this:

But in order to have the same effect with more NPCs, I needed to do this:

My solution uses collection service to go through every instance with the tag ā€˜NPCā€™ and then initialise the script for that NPC

  1. Modulescript that goes in ReplicatedStorage (or wherever else youā€™d like, of course):
-- this has been edited according to the post below
local TweenService = game:GetService("TweenService")

local NPCBehavior = {}
local tweenInfo = TweenInfo.new(0.25, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)

function NPCBehavior.startup(npc, player)
	local head = npc:WaitForChild("Head")
	local defaultCFrame = head.CFrame
	
	npc:FindFirstChild("Neck", true):Destroy()

	local function isWithinRadius()
		local distance = (player.Character.HumanoidRootPart.Position - npc.HumanoidRootPart.Position).Magnitude
		return distance <= 15
	end

	local function isWithinNPCFOV()
		local npcToCharacter = (player.Character.Head.Position - defaultCFrame.Position).Unit
		local npcLookVector = defaultCFrame.LookVector
		local dotProduct = npcToCharacter:Dot(npcLookVector)
		return dotProduct >= 0.55 --about 80 degrees, check the image i attached for my reference
	end

	local function updateHeadOrientation()
		if isWithinRadius() and isWithinNPCFOV() then
			local goalCFrame = CFrame.lookAt(head.Position, player.Character.Head.Position)
			local tween = TweenService:Create(head, tweenInfo, {CFrame = goalCFrame})
			tween:Play()
		else
			local tween = TweenService:Create(head, tweenInfo, {CFrame = defaultCFrame})
			tween:Play()
		end
	end

	local runService = game:GetService("RunService")
	runService.Heartbeat:Connect(updateHeadOrientation)
end

return NPCBehavior
  1. A local script:
local CollectionService = game:GetService("CollectionService")
local Players = game:GetService("Players")
local NPCBehavior = require(game.ReplicatedStorage:WaitForChild("NPCBehaviour"))

local player = Players.LocalPlayer
local character = player.Character or player.CharacterAdded:Wait()

for _, npc in ipairs(CollectionService:GetTagged("NPC")) do
	NPCBehavior.startup(npc, player)
end

This gets us this result:

Although it doesnā€™t support adding new NPCs, thatā€™s very easily done using CollectionService:GetInstanceAddedSignal(ā€œNPCā€):Connect(function())

Also I just want to note that I believe I have found the next easiest solution to the issue, which is to just destroy the neck joint and use the lookat function with the head, as the neck joint is the reason why adjusting the orientation of the head adjusts the orientation of the rest of the body, unless you needed the joint for some reason. So all the code I made is pretty pointless and the code can be redone to just this:

--npcbehaviour module redone
local TweenService = game:GetService("TweenService")

local NPCBehavior = {}
local tweenInfo = TweenInfo.new(0.25, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)

function NPCBehavior.startup(npc, player)
	local head = npc:WaitForChild("Head")
	local defaultCFrame = head.CFrame
	
	npc:FindFirstChild("Neck", true):Destroy()

	local function isWithinRadius()
		local distance = (player.Character.HumanoidRootPart.Position - npc.HumanoidRootPart.Position).Magnitude
		return distance <= 15
	end

	local function isWithinNPCFOV()
		local npcToCharacter = (player.Character.Head.Position - defaultCFrame.Position).Unit
		local npcLookVector = defaultCFrame.LookVector
		local dotProduct = npcToCharacter:Dot(npcLookVector)
		return dotProduct >= 0.55 --about 80 degrees, check the image i attached for my reference
	end

	local function updateHeadOrientation()
		if isWithinRadius() and isWithinNPCFOV() then
			local goalCFrame = CFrame.lookAt(head.Position, player.Character.Head.Position)
			local tween = TweenService:Create(head, tweenInfo, {CFrame = goalCFrame})
			tween:Play()
		else
			local tween = TweenService:Create(head, tweenInfo, {CFrame = defaultCFrame})
			tween:Play()
		end
	end

	local runService = game:GetService("RunService")
	runService.Heartbeat:Connect(updateHeadOrientation)
end

return NPCBehavior
1 Like