How to Create a Realistic NPC Eyesight System

,

Introduction

Hello everyone! In this tutorial, I’m going to teach you how to create a realistic NPC eyesight system so NPCs can see and detect players and other objects. For example, you could use this to make a guard detect a player.


Steps

  1. Create a script in ServerScriptService and name it whatever you’d like. This script will control every NPC with eyesight.

  2. Give the NPCs you want to have eyesight a tag named “NPC” and the objects you want to NPC to detect a tag named “Detectable.” Players will be automatically detected. The “Detectable” tag may only be added to models.

  3. Start defining the variables in your script.

local PlayerService = game:GetService("Players")
local CollectionService = game:GetService("CollectionService")

local npcModels = CollectionService:GetTagged("NPC")

local viewRange = 30 -- How far the NPC can see in studs
local fieldOfView = 70 -- The NPC's field of view in degrees

local objectLastDetected = nil -- The object the NPC last saw
  1. Create a getCharacters function. This function will return a table of every player’s character in the game.
local function getCharacters()
	local characters = {}
	local players = PlayerService:GetPlayers()
	
	for index, player in players do
		table.insert(characters, player.Character)
	end
	
	return characters
end
  1. Next, create a raycast function. This function will be used to check if anything is blocking the NPC’s line of sight, as it cannot see through walls.
local function raycast(npc, start, finish)
	local parameters = RaycastParams.new()
	
	parameters.FilterDescendantsInstances = {npc, getCharacters()}
	parameters.FilterType = Enum.RaycastFilterType.Exclude
	
	local raycast = workspace:Raycast(start, (finish - start), parameters)
	
	if raycast then
		return true
	else
		return false
	end
end
  1. Now, create a checkSight function. We will use this to determine whether/not an object is being seen by the NPC.
local function checkSight(npc)
	local detectable = {}
	
	local characters = getCharacters()
	local taggedObjects = CollectionService:GetTagged("Detectable")
	
	for index, character in characters do
		table.insert(detectable, character)
	end
	
	for index, object in taggedObjects do
		table.insert(detectable, object)
	end
	
	for index, object in detectable do
		if object:IsA("Model") then
			object = object.PrimaryPart
		end
		
		local headPosition = npc.Head.Position
		
		local objectPosition = object.Position
		local headCFrame = npc.Head.CFrame
		
		local direction = (objectPosition - headPosition).Unit
		local lookVector = headCFrame.LookVector
		
		local dotProduct = direction:Dot(lookVector)
		local angle = math.deg(math.acos(dotProduct))
		
		local distance = (headPosition - objectPosition).Magnitude
		
		if angle > fieldOfView then
			continue
		end
		
		if distance > viewRange then
			continue
		end
		
		if raycast(npc, headPosition, objectPosition) then
			return
		end
		
		if object:IsA("Model") then
			objectLastDetected = object.Parent
		else
			objectLastDetected = object
		end
		
		return true
	end
end
  1. Lastly, create a for loop that goes through every NPC and sets up detection every second with error prevention.
for index, npc in npcModels do
	local humanoid = npc:FindFirstChildOfClass("Humanoid")
	
	if not npc:IsA("Model") then
		continue
	end
	
	if not humanoid then
		continue
	end
	
	local function checking()
		while task.wait(1) do
			if checkSight(npc) then
				-- Code for when an NPC sees a detectable object
			else
				-- Code for when an NPC does not see a detectable object
			end
		end
	end
	
	coroutine.wrap(checking)()
end

Resources

Dot Product - Wikipedia
Coroutine - Roblox Documentation

-- Formula for distance (in studs)
local distance = (pointA.Position - pointB.Position).Magnitude

Conclusion

Thank you for checking out my tutorial! If you have any questions, let me know in the comments down below.

If this tutorial helped you, please consider leaving a like! :sparkling_heart:

84 Likes

I have no idea what a ‘npc fov’ is. It would help if there was images or videos visualizing what your trying to make. Use cases listed before even starting the tutorial would also help us understand what it is your making.

7 Likes

This code may be useful for me in a 3008 game I am going to be working on. Thanks for providing it! :slightly_smiling_face:

6 Likes

is there anyway to add a cooldown? debounce won’t work

5 Likes

if you want it to only check every few seconds, just do

while task.wait(3) do
    FOVCast()
end
5 Likes

Personally, I don’t think anybody should be using next in a for loop. I have been programming for several years on Roblox, and yet I still have no idea why it’s used, why it exists or the syntax for even using it. I prefer to use the “generic iteration” syntax:

for _, Player in Players:GetPlayers() do

, which I feel like is less visually cluttered and, reading through it, makes more sense:

  • for Players in next Players:GetPlayers

vs

  • for Player in Players:GetPlayers

Other than that, I think this tutorial is pretty good. +1 :+1:

5 Likes

as the distance of the target from the origin increases, the field of view boundaries increases. this is a directly proportional relation. as this distance D increases let there be two vector2 values, C which denotes the maximum boundary, and T which denotes the minimum boundary. let B be D divided by the maximum distance the npc can see, yielding a [0,1] value and the limitation is that the target must be in front of the origin.
let H be a value which influences how much B is multiplied by
let G be B*H
as B changes, C = v2(G, G), T = v2(-G, -G). you can take the relative position of the target from the origin and check if the targets position is within T and C based on the targets X and Y position

5 Likes

bruh its fine it doesn’t matter as long as it works and isn’t unreadable :derp:

3 Likes

It’s just a small nitpick. I’m not attacking them, I’m just sharing my thoughts.

2 Likes

(post deleted by author)

1 Like

(post deleted by author)

1 Like

if you mean generalized iteration, it is fairly new to luau, being introduced a year ago, and should be used over an iterator such as next or iterator function such as pairs

2 Likes

(post deleted by author)

1 Like

(post deleted by author)

1 Like

I thought this line should be FOV/2 and Guard = NPC.
But your code is great. Thanks.

if Angle <= FOV/2 and Distance <= Range and CheckLineOfSight(NPC.Head.Position, Character.Head.Position) then

2 Likes

(post deleted by author)

1 Like

Thanks for your feedback.
I see that:

  1. The main center line is the Lookvector of the NPC’s face.
  2. So the angle from the center line to the limits will be FOV/2
  3. Currently we are calculating the angle between the lookvector and the vector connecting the face of the NPC and the player’s face, so we will compare it with FOV/2
  4. Since the heights of the NPC’s head and the player’s head are different, I think it’s a good idea to remove the y component by multiplying the vectors by vector3.new(1,0,1) and then comparing in the xz plane would be more accurate.
    Here it is assumed that the NPC is always looking sideways (not looking up or down).

local NPCToCharacterFlatten = NPCToCharacter * vector3.new(1,0,1)
local NPCLookVectorFlatten = NPCLookVector * vector3.new(1,0,1)

local DotProduct = NPCToCharacterFlatten:Dot(NPCLookVectorFlatten)

3 Likes

(post deleted by author)

Wait can I ask,I want to check if a dead body/corpse is in the vision so I can make like withnessers. How do I do that?

1 Like

(post deleted by author)