Struggling with making a detection system for a stealth game

Here you go. I’d personally recommend and while true do loop, but this should solve your problems. You’ll have to readd your remote event code, I did this quickly so I could test it. Realistic angles of fov, a bar using tween service to go up on down depending on detection, etc.

-- Get necessary services
local RunService = game:GetService("RunService")
local PlayersService = game:GetService("Players")
local TweenService = game:GetService("TweenService")

-- Set NPC and character, along with their sight and range properties
local npc = workspace.World.Debug.NPC.DetectionTest
local npcSight = 0.7
local npcRange = 100

-- GUI elements and reaction time
local player = PlayersService.LocalPlayer
local character = player.Character or player.CharacterAdded:Wait()
local head = character:WaitForChild("Head")

local playerGui = player.PlayerGui
local DetectionBar = playerGui:WaitForChild("DetectionGUI")
local reactionTime = 1.8

-- Create a table to hold TweenInfo
local tweenInfo = TweenInfo.new(
	2,  -- Time
	Enum.EasingStyle.Linear,  -- EasingStyle
	Enum.EasingDirection.Out,  -- EasingDirection
	0,  -- RepeatCount (0 implies the tween will not repeat)
	false,  -- Reverses (should the tween reverse once reaching the endpoint)
	0  -- DelayTime
)

local characterIsInFov = false

RunService.Heartbeat:Connect(function()
	if character and npc then -- Check if the character and the NPC are not null

		-- Calculate the vector between the NPC and the character
		local npcToCharacter = (head.Position - npc.Head.Position).Unit
		local npcLook = npc.Head.CFrame.LookVector
		local dotProduct = npcToCharacter:Dot(npcLook)

		if dotProduct > npcSight then
			--character is in field of view        
			characterIsInFov = true
		else
			-- character is not in field of view so we do not need to take any action
			characterIsInFov = false
		end

		-- Prepare parameters for raycasting
		local raycastParams = RaycastParams.new()
		raycastParams.FilterType = Enum.RaycastFilterType.Exclude
		raycastParams.FilterDescendantsInstances = {npc}
		raycastParams.IgnoreWater = true

		-- Perform a raycast to check if the character is in sight
		local ray = workspace:Raycast(npc.PrimaryPart.Position, (character.PrimaryPart.Position - npc.PrimaryPart.Position).Unit * npcRange, raycastParams)

		-- Check if the character is in range and in field of view
		if characterIsInFov and (head.Position - npc.PrimaryPart.Position).Magnitude <= npcRange then
			if ray and ray.Instance.Parent == character then
				local hitPlayer = PlayersService:GetPlayerFromCharacter(ray.Instance.Parent)
				if hitPlayer then
					-- Animate the detection bar
					local tween = TweenService:Create(DetectionBar.detectionFrame.detectionBar, tweenInfo, {Size = UDim2.new(1, 0, math.clamp(dotProduct * 2, 0, 1), 0)})
					tween:Play()
					DetectionBar.detectionFrame.detectionBar.BackgroundColor3 = Color3.fromRGB(255, 0, 0)

					-- Wait for reaction time and check again
					task.wait(reactionTime)

					if characterIsInFov and (head.Position - npc.PrimaryPart.Position).Magnitude <= npcRange then
						print('Player detected: ', hitPlayer.Name)
					end
				end
			else
				-- Decrease the detection bar when not in sight
				local tween = TweenService:Create(DetectionBar.detectionFrame.detectionBar, tweenInfo, {Size = UDim2.new(1, 0, 0, 0)})
				tween:Play()
			end
		else
			-- Decrease the detection bar when not in sight
			local tween = TweenService:Create(DetectionBar.detectionFrame.detectionBar, tweenInfo, {Size = UDim2.new(1, 0, 0, 0)})
			tween:Play()
		end
	end
end)
1 Like

It doesn’t quite work as intended but we are REALLY close, I think just a little bit of tweaking or something and we might be alright

Here’s what happened:

1 Like

Very close. Let me write up a possible fix…

Try this:

-- Get necessary services
local RunService = game:GetService("RunService")
local PlayersService = game:GetService("Players")
local TweenService = game:GetService("TweenService")

-- Set NPC and character, along with their sight and range properties
local npc = workspace.World.Debug.NPC.DetectionTest
local npcSight = 0.7
local npcRange = 100

-- GUI elements and reaction time
local player = PlayersService.LocalPlayer
local character = player.Character or player.CharacterAdded:Wait()
local head = character:WaitForChild("Head")

local playerGui = player.PlayerGui
local DetectionBar = playerGui:WaitForChild("DetectionGUI")
local reactionTime = 1.8

-- Create a table to hold TweenInfo
local tweenInfo = TweenInfo.new(
	1,  -- Time
	Enum.EasingStyle.Linear,  -- EasingStyle
	Enum.EasingDirection.Out,  -- EasingDirection
	0,  -- RepeatCount (0 implies the tween will not repeat)
	false,  -- Reverses (should the tween reverse once reaching the endpoint)
	0  -- DelayTime
)

local characterIsInFov = false
local detectionProgress = 0 -- Variable to keep track of detection progress

RunService.Heartbeat:Connect(function()
	if character and npc then -- Check if the character and the NPC are not null

		-- Calculate the vector between the NPC and the character
		local npcToCharacter = (head.Position - npc.Head.Position).Unit
		local npcLook = npc.Head.CFrame.LookVector
		local dotProduct = npcToCharacter:Dot(npcLook)

		if dotProduct > npcSight then
			--character is in field of view        
			characterIsInFov = true
		else
			-- character is not in field of view so we do not need to take any action
			characterIsInFov = false
		end

		-- Prepare parameters for raycasting
		local raycastParams = RaycastParams.new()
		raycastParams.FilterType = Enum.RaycastFilterType.Exclude
		raycastParams.FilterDescendantsInstances = {npc}
		raycastParams.IgnoreWater = true

		-- Perform a raycast to check if the character is in sight
		local ray = workspace:Raycast(npc.PrimaryPart.Position, (character.PrimaryPart.Position - npc.PrimaryPart.Position).Unit * npcRange, raycastParams)

		-- Check if the character is in range and in field of view
		if characterIsInFov and (head.Position - npc.PrimaryPart.Position).Magnitude <= npcRange then
			if ray and ray.Instance.Parent == character then
				local hitPlayer = PlayersService:GetPlayerFromCharacter(ray.Instance.Parent)
				if hitPlayer then
					-- Increase the detection progress
					detectionProgress = math.min(detectionProgress + dotProduct * 2, 1)

					-- Animate the detection bar
					local tween = TweenService:Create(DetectionBar.detectionFrame.detectionBar, tweenInfo, {Size = UDim2.new(1, 0, detectionProgress, 0)})
					tween:Play()
					DetectionBar.detectionFrame.detectionBar.BackgroundColor3 = Color3.fromRGB(255, 0, 0)

					-- Check if the detection bar is filled completely before triggering the NPC detection
					if detectionProgress == 1 then
						-- Wait for reaction time and check again
						task.wait(reactionTime)
						if characterIsInFov and (head.Position - npc.PrimaryPart.Position).Magnitude <= npcRange then
							print('Player detected: ', hitPlayer.Name)
						end
					end
				end
			else
				-- Decrease the detection bar when not in sight
				detectionProgress = math.max(detectionProgress - 0.05, 0) -- Decrease the detection progress
				local tween = TweenService:Create(DetectionBar.detectionFrame.detectionBar, tweenInfo, {Size = UDim2.new(1, 0, detectionProgress, 0)})
				tween:Play()
			end
		else
			-- Decrease the detection bar when not in sight
			detectionProgress = math.max(detectionProgress - 0.05, 0) -- Decrease the detection progress
			local tween = TweenService:Create(DetectionBar.detectionFrame.detectionBar, tweenInfo, {Size = UDim2.new(1, 0, detectionProgress, 0)})
			tween:Play()
		end
	end
end)
1 Like

Detection part of it is excellent, just the bar that’s being a bit dodgy (going to have my tea now so I’ll be back in about 10 minutes or so :laughing:)

1 Like
-- Get necessary services
local RunService = game:GetService("RunService")
local PlayersService = game:GetService("Players")
local TweenService = game:GetService("TweenService")

-- Set NPC and character, along with their sight and range properties
local npc = workspace.World.Debug.NPC.DetectionTest
local npcSight = 0.7
local npcRange = 100

-- GUI elements and reaction time
local player = PlayersService.LocalPlayer
local character = player.Character or player.CharacterAdded:Wait()
local head = character:WaitForChild("Head")

local playerGui = player.PlayerGui
local DetectionBar = playerGui:WaitForChild("DetectionGUI")
local reactionTime = 1.8

-- Create a table to hold TweenInfo
local tweenInfo = TweenInfo.new(
	1.5,  -- Time
	Enum.EasingStyle.Quad,  -- EasingStyle
	Enum.EasingDirection.Out,  -- EasingDirection
	0,  -- RepeatCount (0 implies the tween will not repeat)
	false,  -- Reverses (should the tween reverse once reaching the endpoint)
	0  -- DelayTime
)

local characterIsInFov = false
local detectionProgress = 0 -- Variable to keep track of detection progress
local hitPlayer = nil -- Move hitPlayer variable outside the if statement

RunService.Heartbeat:Connect(function()
	if character and npc then -- Check if the character and the NPC are not null

		-- Calculate the vector between the NPC and the character
		local npcToCharacter = (head.Position - npc.Head.Position).Unit
		local npcLook = npc.Head.CFrame.LookVector
		local dotProduct = npcToCharacter:Dot(npcLook)

		if dotProduct > npcSight then
			--character is in field of view        
			characterIsInFov = true
		else
			-- character is not in field of view so we do not need to take any action
			characterIsInFov = false
		end

		-- Prepare parameters for raycasting
		local raycastParams = RaycastParams.new()
		raycastParams.FilterType = Enum.RaycastFilterType.Exclude
		raycastParams.FilterDescendantsInstances = {npc}
		raycastParams.IgnoreWater = true

		-- Perform a raycast to check if the character is in sight
		local ray = workspace:Raycast(npc.PrimaryPart.Position, (character.PrimaryPart.Position - npc.PrimaryPart.Position).Unit * npcRange, raycastParams)

		-- Check if the character is in range and in field of view
		if characterIsInFov and (head.Position - npc.PrimaryPart.Position).Magnitude <= npcRange then
			if ray and ray.Instance.Parent == character then
				hitPlayer = PlayersService:GetPlayerFromCharacter(ray.Instance.Parent) -- This will now update the variable declared earlier
				if hitPlayer then
					-- Increase the detection progress
					detectionProgress = math.min(detectionProgress + dotProduct * 2, 1)
				end
			else
				-- Decrease the detection bar when not in sight
				detectionProgress = math.max(detectionProgress - 0.05, 0) -- Decrease the detection progress
			end
		else
			-- Decrease the detection bar when not in sight
			detectionProgress = math.max(detectionProgress - 0.05, 0) -- Decrease the detection progress
		end

		-- Animate the detection bar according to detection progress
		local tween = TweenService:Create(DetectionBar.detectionFrame.detectionBar, tweenInfo, {Size = UDim2.new(1, 0, detectionProgress, 0)})
		tween:Play()
		DetectionBar.detectionFrame.detectionBar.BackgroundColor3 = Color3.fromRGB(255, 0, 0)

		-- Check if the detection bar is filled completely before triggering the NPC detection
		if detectionProgress >= 1 then
			detectionProgress = 0 -- Reset detection progress after detection
			-- Wait for the bar to finish filling up visually, then check again
			tween.Completed:Wait() -- Wait for the current tween to complete
			
			task.wait(reactionTime)
			if characterIsInFov and (head.Position - npc.PrimaryPart.Position).Magnitude <= npcRange and hitPlayer then -- Make sure hitPlayer is not nil
				print('Player detected: ', hitPlayer.Name)
			end
		end
	end
end)

It’s getting slightly better but not quite right

Mind sharing the .rbxl file? You can hit me up on Discord, or private message me here? It’ll be easier that way.

You don’t need to tween if it’s gonna update every Heartbeat.

1 Like

You mean the file for the whole place? My discord is [redacted - post solved]

1 Like

You can contribute a possible fix, y’know? :wink: I’m trying.

-- Get necessary services
local RunService = game:GetService("RunService")
local PlayersService = game:GetService("Players")
local TweenService = game:GetService("TweenService")

-- Set NPC and character, along with their sight and range properties
local npc = workspace:WaitForChild("Rig")
local npcSight = 0.7
local npcRange = 100

-- GUI elements and reaction time
local player = PlayersService.LocalPlayer
local character = player.Character or player.CharacterAdded:Wait()
local head = character:WaitForChild("Head")

local playerGui = player.PlayerGui
local DetectionBar = playerGui:WaitForChild("DetectionGUI")
local reactionTime = 1.8

local characterIsInFov = false
local detectionProgress = 0 -- Variable to keep track of detection progress
local hitPlayer = nil -- Move hitPlayer variable outside the if statement

RunService.Heartbeat:Connect(function(dt)
	if character and npc then -- Check if the character and the NPC are not null

		-- Calculate the vector between the NPC and the character
		local npcToCharacter = (head.Position - npc.Head.Position).Unit
		local npcLook = npc.Head.CFrame.LookVector
		local dotProduct = npcToCharacter:Dot(npcLook)

		if dotProduct > npcSight then
			--character is in field of view        
			characterIsInFov = true
		else
			-- character is not in field of view so we do not need to take any action
			characterIsInFov = false
		end

		-- Prepare parameters for raycasting
		local raycastParams = RaycastParams.new()
		raycastParams.FilterType = Enum.RaycastFilterType.Exclude
		raycastParams.FilterDescendantsInstances = {npc}
		raycastParams.IgnoreWater = true

		-- Perform a raycast to check if the character is in sight
		local ray = workspace:Raycast(npc.PrimaryPart.Position, (character.PrimaryPart.Position - npc.PrimaryPart.Position).Unit * npcRange, raycastParams)

		-- Check if the character is in range and in field of view
		if characterIsInFov and (head.Position - npc.PrimaryPart.Position).Magnitude <= npcRange then
			if ray and ray.Instance.Parent == character then
				hitPlayer = PlayersService:GetPlayerFromCharacter(ray.Instance.Parent) -- This will now update the variable declared earlier
				if hitPlayer then
					-- Increase the detection progress
					detectionProgress = math.min(detectionProgress + dotProduct * 2 * dt, 1)
				end
			else
				-- Decrease the detection bar when not in sight
				detectionProgress = math.max(detectionProgress - 0.05 * dt, 0) -- Decrease the detection progress
			end
		else
			-- Decrease the detection bar when not in sight
			detectionProgress = math.max(detectionProgress - 0.05 * dt, 0) -- Decrease the detection progress
		end

		-- Animate the detection bar according to detection progress
		local alpha = TweenService:GetValue(detectionProgress / 1, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)
		DetectionBar.detectionFrame.detectionBar.Size = UDim2.new(1, 0, 0, 0):Lerp(UDim2.new(1, 0, 1, 0), alpha)
		DetectionBar.detectionFrame.detectionBar.BackgroundColor3 = Color3.fromRGB(255, 0, 0)

		-- Check if the detection bar is filled completely before triggering the NPC detection
		if detectionProgress >= 1 then
			-- detectionProgress = 0 -- Reset detection progress after detection
			-- Wait for the bar to finish filling up visually, then check again
			--tween.Completed:Wait() -- Wait for the current tween to complete

			task.wait(reactionTime)
			if characterIsInFov and (head.Position - npc.PrimaryPart.Position).Magnitude <= npcRange and hitPlayer then -- Make sure hitPlayer is not nil
				print('Player detected: ', hitPlayer.Name)
			end
		end
	end
end)

3 Likes

Messed around with an attempt at this …

  1. Check if within range of NPC.
  2. Check if within angle of facing head.
  3. Add to a check loop free from hacking.

This is a test on one NPC. It will need some love,
but seems to be a viable approach for a template.
(script works as is)
.
A non-local script in ServerScriptService

task.wait(5) -- Stalling so npc is ready
local npc = workspace.Sandy.Head -- NPC's head 
local viewRange = 20

-- Function to check if the player is within the NPC's view range
local function isPlayerInNPCViewRange(player)
	local npcPosition = npc.Position
	local playerPosition = player.Character and player.Character.PrimaryPart and player.Character.PrimaryPart.Position

	if not playerPosition then
		return false
	end

	local distance = (npcPosition - playerPosition).magnitude
	return distance <= viewRange
end

-- Function to check if the player is within the 50-degree angle from the NPC's look vector
local function isPlayerInRaycastAngle(player)
	local npcLookDirection = npc.CFrame.LookVector
	local playerPosition = player.Character and player.Character.PrimaryPart and player.Character.PrimaryPart.Position

	if not playerPosition then
		return false
	end

	local directionToPlayer = (playerPosition - npc.Position).unit
	local angle = math.acos(npcLookDirection:Dot(directionToPlayer)) * (180 / math.pi)

	return angle <= 50 --  # degrees both ways from center 50x2 = 100 
end

-- Added to a stepped loop
local RunService = game:GetService("RunService")
RunService.Stepped:connect(function()
	for _, player in ipairs(game.Players:GetPlayers()) do
		if isPlayerInNPCViewRange(player) and isPlayerInRaycastAngle(player) then
			print("Player is within view range and angle!")
		end
	end
end)

Solved over Discord by @AlureonTime

Thank you everyone for your contributions, I really appreciate it!

The solution involves using heartbeat with repeated checks throughout to ensure that a player has or has not been detected, and then update the detection progress based on this information and send that to the actual detection bar.

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.