NPC Suspicion rates: Opinions?

howdy fellow roblox devs! i’ve been trying to create a NPC suspicion system for a stealth game that i’m working on, lots of games have this for example, Roblox Entry Point and Hitman 1, 2, and 3. I’ve been thinking about it for some time and i can’t really think up a great way to like keep track of suspicion, moving the value of suspicion up while the player is in the view of the NPC, keeping it in the same position when the player leaves for a second, then gradually decreasing it after the player has not been in view for a while. here’s the script that i have:

local RunService = game:GetService("RunService")
local Players = game:GetService("Players")
local Workspace = game:GetService("Workspace")
local DebrisService = game:GetService("Debris")

local NPC = script.Parent
local head = NPC:WaitForChild("Head")
local rootPart = NPC:WaitForChild("HumanoidRootPart")
local detectionRange = 50 -- Adjust the detection range as needed
local fieldOfViewThreshold = 0.5 -- Adjust the field of view threshold as needed

local suspicionAmount = 0

local suspicionMeter = Instance.new("Part")
suspicionMeter.Size = Vector3.new(7.5, 0.25, 0.5)
suspicionMeter.BrickColor = BrickColor.new("Really black")
suspicionMeter.CanCollide = false
suspicionMeter.Position = rootPart.Position + rootPart.CFrame.UpVector * -2.875 + rootPart.CFrame.LookVector * 1.5

local susGui = Instance.new("SurfaceGui")
susGui.Face = Enum.NormalId.Top
susGui.Parent = suspicionMeter

local susFrame = Instance.new("Frame")
susFrame.Size = UDim2.new(1,0,.5,0)
susFrame.BackgroundColor3 = Color3.new(255,255,0.3)
susFrame.Parent = susGui

local susWeld = Instance.new("WeldConstraint")
susWeld.Part0 = suspicionMeter
susWeld.Part1 = rootPart
susWeld.Parent = suspicionMeter
suspicionMeter.Parent = NPC

local rayPartsFolder = Instance.new("Folder",workspace)

local playerCharacters = {}

Players.PlayerAdded:Connect(function(player)
	player.CharacterAdded:Connect(function(char)
		table.insert(playerCharacters, char)
		char:WaitForChild("Humanoid").Died:Connect(function()
			table.remove(playerCharacters, table.find(playerCharacters, char))
		end)
	end)
end)

function isInFieldOfView(npcHead, playerHead, npcForwardVector, threshold)
	local npcToPlayer = (playerHead.Position - npcHead.Position).Unit
	local dotProduct = npcToPlayer:Dot(npcForwardVector)
	return dotProduct > threshold
end

RunService.Stepped:Connect(function(deltaTime)
	local npcForwardVector = head.CFrame.LookVector

	for _, char in pairs(playerCharacters) do
		local pHead = char:FindFirstChild("Head")
		if pHead then
			if isInFieldOfView(head, pHead, npcForwardVector, fieldOfViewThreshold) then
				local raycastParams = RaycastParams.new()
				raycastParams.FilterDescendantsInstances = {NPC,workspace.RayParts}
				raycastParams.FilterType = Enum.RaycastFilterType.Exclude
				local raycastResult = Workspace:Raycast(head.Position, (pHead.Position - head.Position).Unit * detectionRange, raycastParams)
				if raycastResult then
					local distance = (head.Position - raycastResult.Position).Magnitude
					local linePart = Instance.new("Part")
					linePart.Anchored = true
					linePart.CanCollide = false
					linePart.Size = Vector3.new(0.1, 0.1, distance)
					linePart.CFrame = CFrame.lookAt(head.Position, raycastResult.Position) * CFrame.new(0, 0, -distance / 2)
					linePart.Transparency = 0.7
					linePart.Parent = Workspace.RayParts

					DebrisService:AddItem(linePart,10)
					
					if raycastResult.Instance:FindFirstAncestor(char.Name) then
						suspicionAmount = math.clamp(suspicionAmount + 0.01,0,1)
					else
						suspicionAmount = math.clamp(suspicionAmount - 0.01,0,1)
						print(raycastResult.Instance)
					end
				end

				--print(suspicionAmount)
				susFrame.Size = UDim2.new(1,0,suspicionAmount,0)
			end
		end
	end
end)

if you want to test it out you can just shove a server script into any rig and it should work. i mean the script works fine but obviously it’s not that great. does anyone have any suggestions? should i use deltatime? i really have no idea what the best way to do this is. thanks!

Here’s a modified version of the script you made.

local RunService = game:GetService("RunService")
local Players = game:GetService("Players")
local Workspace = game:GetService("Workspace")
local DebrisService = game:GetService("Debris")

local NPC = script.Parent
local head = NPC:WaitForChild("Head")
local rootPart = NPC:WaitForChild("HumanoidRootPart")
local detectionRange = 50
local fieldOfViewThreshold = 0.5

local suspicionAmount = 0

local raycastParams = RaycastParams.new()
raycastParams.FilterDescendantsInstances = {NPC, workspace.RayParts}
raycastParams.FilterType = Enum.RaycastFilterType.Exclude
local playerCharacters = {}

Players.PlayerAdded:Connect(function(player)
	player.CharacterAdded:Connect(function(char)
		table.insert(playerCharacters, char)
		char:WaitForChild("Humanoid").Died:Connect(function()
			table.remove(playerCharacters, table.find(playerCharacters, char))
		end)
	end)
end)

local suspicionMeter = Instance.new("Part")
suspicionMeter.Size = Vector3.new(7.5, 0.25, 0.5)
suspicionMeter.BrickColor = BrickColor.new("Really black")
suspicionMeter.CanCollide = false
suspicionMeter.Position = rootPart.Position + rootPart.CFrame.UpVector * -2.875 + rootPart.CFrame.LookVector * 1.5

local susGui = Instance.new("SurfaceGui")
susGui.Face = Enum.NormalId.Top
susGui.Parent = suspicionMeter

local susFrame = Instance.new("Frame")
susFrame.Size = UDim2.new(1,0,.5,0)
susFrame.BackgroundColor3 = Color3.new(255,255,0.3)
susFrame.Parent = susGui

local susWeld = Instance.new("WeldConstraint")
susWeld.Part0 = suspicionMeter
susWeld.Part1 = rootPart
susWeld.Parent = suspicionMeter
suspicionMeter.Parent = NPC

local function isInFieldOfView(npcHead, playerHead, npcForwardVector, threshold)
	local npcToPlayer = (playerHead.Position - npcHead.Position).Unit
	local dotProduct = npcToPlayer:Dot(npcForwardVector)
	return dotProduct > threshold
end

RunService.Stepped:Connect(function(deltaTime)
	local npcForwardVector = head.CFrame.LookVector
	local suspiciousPlayers = {}

	for _, char in ipairs(playerCharacters) do
		local pHead = char:FindFirstChild("Head")
		if pHead and isInFieldOfView(head, pHead, npcForwardVector, fieldOfViewThreshold) then
			local distance = (head.Position - pHead.Position).Magnitude
			if distance <= detectionRange then
				local raycastResult = Workspace:Raycast(head.Position, (pHead.Position - head.Position).Unit * detectionRange, raycastParams)
				if raycastResult and raycastResult.Instance:FindFirstAncestor(char.Name) then
					local distance = (head.Position - raycastResult.Position).Magnitude
					local linePart = Instance.new("Part")
					linePart.Anchored = true
					linePart.CanCollide = false
					linePart.Size = Vector3.new(0.1, 0.1, distance)
					linePart.CFrame = CFrame.lookAt(head.Position, raycastResult.Position) * CFrame.new(0, 0, -distance / 2)
					linePart.Transparency = 0.7
					linePart.Parent = Workspace.RayParts
					DebrisService:AddItem(linePart,10)
					
					table.insert(suspiciousPlayers, char)
				end
			end
		end
	end

	local suspicionDelta = #suspiciousPlayers > 0 and 0.01 or -0.01
	suspicionAmount = math.clamp(suspicionAmount + suspicionDelta, 0, 1)
	susFrame.Size = UDim2.new(1, 0, suspicionAmount, 0)
end)

AI comparison of the script:

  1. Raycast Parameters: Script 2 defines raycast parameters once outside the loop, while Script 1 defines them inside the loop. Defining them outside the loop in Script 2 reduces redundancy and potentially improves performance.
  2. Loop Optimization: Script 2 uses ipairs for iterating through playerCharacters, which is more efficient for arrays than pairs, as it avoids unnecessary hash lookups. Additionally, Script 2 checks if the player is within the field of view and detection range before performing the raycast, potentially saving unnecessary raycasting calculations.
  3. Suspicion Logic: Script 2 calculates the suspicion delta based on the number of suspicious players found, while Script 1 directly adjusts the suspicion amount based on whether a player is detected or not. Script 2’s approach is more concise and likely easier to maintain.
  4. Code Organization: Script 2 organizes the code more logically, separating variable declarations from event connections and other logic. This improves readability and maintainability.
2 Likes