Struggling with making a detection system for a stealth game

You can write your topic however you want, but you need to answer these questions:

  1. What do you want to achieve?
    I would like for the detection bar to increase ONLY when the player is in the NPC’s field of view but not behind a wall for example, hence why I had to combine dot product and ray casting. Plus any other improvements that you think I might need to add.

  2. What is the issue?
    Currently the detection bar does not work as intended, yes the NPC cannot detect you through walls but it’s only when standing right in front of them that the detection bar instantly maxes out and you’re spotted. I need it to increase and decrease based on the dot product value as long as the player is in sight (i.e. not behind a wall)

  3. What solutions have you tried so far?
    I have looked through a lot of devforum posts and found nothing, I changed the entire system several times but didn’t get the intended results.

To summarise, I am trying to create hitman-style stealth mechanics but I do not want an exact copy of course.

This is a local script currently in StarterCharacterScripts. The NPC is located in Workspace. The GUIs mentioned are in StarterGui.

local RunService = game:GetService("RunService")
local npc = workspace.NPC
local character = game.Players.LocalPlayer.Character
local characterIsInFov = false
local npcSight = .9
local characterIsInSight = false
local npcRange = 100


local TweenService = game:GetService("TweenService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local detectedevent = ReplicatedStorage:WaitForChild("PlayerDetected")

local playerGui = game.Players.LocalPlayer.PlayerGui
local DetectionBar = playerGui:WaitForChild("DetectionGUI")



-- // Configure \\
local reactionTime = 1.8	--Once you're spotted, how long it will take for the NPC to take action. If you move out of their sight before this time runs out, nothing will happen


RunService.RenderStepped:Connect(function()
	local npcToCharacter = (character.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

	
	
	
	
	
	
	-- Now we can determine if the player is in the field of view or not. Now let's prevent the npc being able to see the player behind walls:

	local raycastParams = RaycastParams.new()

	raycastParams.FilterType = Enum.RaycastFilterType.Exclude

	local list = {}

	for i,v in pairs(workspace:GetDescendants()) do -- This will ignore all the chairs (you would have to set a custom state for all the chairs in your game)
		if 
			v.Name == "Chair" or v.Name == "Part" -- It can be something simple like this!
		then		
			table.insert(list, #(list) + 1, v)
		end
	end

	raycastParams.FilterDescendantsInstances = list
	raycastParams.IgnoreWater = true

	local ray = workspace:Raycast(npc.PrimaryPart.Position, character.PrimaryPart.Position, raycastParams)

	if ray and (ray.Instance.Position - npc.PrimaryPart.Position).Magnitude <= npcRange and characterIsInFov == true then -- In real life, 
		-- somebody can't see someone else from 1000 studs away even if it's a precise straight line! This is why we need a range.
		characterIsInSight = true
		
		DetectionBar.detectionFrame.detectionBar.Dot.Value = dotProduct
		DetectionBar.detectionFrame.detectionBar:TweenSize(UDim2.new(1, 0, dotProduct*2, 0), Enum.EasingDirection.Out, Enum.EasingStyle.Linear, 0)

		DetectionBar.detectionFrame.detectionBar.BackgroundColor3 = Color3.fromRGB(255, 0, 0)

		wait(reactionTime)
		
		if ray and (ray.Instance.Position - npc.PrimaryPart.Position).Magnitude <= npcRange and characterIsInFov == true then	
			--After a second check
		detectedevent:FireServer(game.Players.LocalPlayer) --Tell the server that the player has been caught, either mark them as suspicious or start combat
		end
	else
		characterIsInSight = false
	end
	
	--print("Character is in fov: " .. tostring(characterIsInFov), "Character is in sight: " ..  tostring(characterIsInSight))
end)

If you’d like an example of what I have managed to create so far and what the issue is, I recorded a video of the system so far.

Any help would be greatly appreciated, thank you!

3 Likes

First of all this is not to be done every frame, discard the RenderStepped and run the code in while true loop which runs every 1/10th of a second (or even less; test with the value).

Second of all, you are not checking if the hit part is a descendant of a character or not; you just continue regardless of whether the hit part is a wall or not.

Third of all (oh god), after waiting for reactionTime (which is just stupid in a RenderStepped loop btw), you are reusing the last raycast rather than raycasting again.

Also as another optimization, once you realize that the player isn’t in your NPC’s FOV, you can just skip the rest of the calculation.

Lastly for the bar thing, try this:

DetectionBar.detectionFrame.detectionBar:TweenSize(UDim2.new(1, 0, math.clamp(dotProduct, 0, 1), 0), Enum.EasingDirection.Out, Enum.EasingStyle.Linear, 0)

Maybe I should have mentioned that I’m new to scripting, sorry if you thought what I did was stupid or annoying. I tried many different things so it was bound to be a mess. I’ve taken care of your first point and put a while loop in there, and I added your version of the bar method.

How would I go about doing the second and third points? I just want to see what it should look like in case I mess it up again.

Having a RenderStepped loop isn’t an almighty sin lol.

2 Likes

So do you want the bar to slowly increase while you’re being detected?

1 Like

Yes, as like a little warning feature to say that someone can see you and you’re about to get spotted if you don’t do something

Yes, but however RenderStepped runs way too many times a second of so many calculations and will slow down the game significantly. If it must be ran so many times a second, I personally would recommend a while true loop with Heartbeat. RenderStepped should only be used for camera manipulation.

I think this should work

local speed = 0.1

local newYScale = math.clamp(DetectionBar.detectionFrame.detectionBar.Size.Y.Scale + speed * dt, 0, 1)

DetectionBar.detectionFrame.detectionBar.Size = UDim2.new(1, 0, newYScale, 0)

you can get the deltatime from RenderStepped connection, it returns it.

1 Like

Decrease the speed if it’s too fast.

I tried it but you’re marked as suspicious before the bar can fill completely, I used the dot product result to calculate this originally so it’s never going to work with deltatime and a speed variable. Thanks for the idea though

Try this for the bar

DetectionBar.detectionFrame.detectionBar:TweenSize(UDim2.new(1, 0, math.clamp(dotProduct, 0, 1), 0), Enum.EasingDirection.Out, Enum.EasingStyle.Linear, 0)

You can just add a check to see if the bar is filled then do whatever you want if they’re suspicious.

Considering OP is making a hitman-like game, he possibly might have more NPCs in the future, so yea just wanted to lead him down a better path

1 Like
DetectionBar.detectionFrame.detectionBar:TweenSize(UDim2.new(1, 0, dotProduct*2, 0), Enum.EasingDirection.Out, Enum.EasingStyle.Linear, 0)

This seems to be the best working solution for the bar. All I need now is the raycasting to be sorted out so that the bar increases when the dotproduct is 0.5 or more AND the ray casting says that you are not behind a wall or something like that (because dot product on its own doesnt care if you’re behind a wall or not)

Correct, I will be using multiple NPCs

The third point will automatically start working once you switch over to a while loop and as for the foruth one, you just have to perform the raycast onec again after waiting for reactionTime.

(also apologies if u were hurt or offended by smth i said, I was just tryna be factual and clear so ye mb)

Edit:- I got stuff switched up, for the second point just do:

if raycast.Instance.Parent:FindFirstChild("Humanoid") then
    --// Do code
end

Or if you want NPCs to detect only players, then

local player = Players:GetPlayerFromCharacter(raycast.Instance.Parent)

if player then
    --// Do code
end

As for the third, it will automatically start working once you switch over to a while loop.

@iiPizzaCraver ping cuz edit

Technically it’s not bad to have a renderstepped loop regardless of how many NPCs, I mean, what do you suggest?

Tried this, the bar is now a bit jumpy and doesn’t move seamlessly anymore and I am now detectable through walls but the “spotted” event doesn’t happen at all now.

And it’s all good don’t worry, it just needlessly came off as a bit rude and obnoxious.

If it’s really necessary to run code that many times a second, its recommended to use a Heartbeat loop. RenderStepped is a *very* bad idea for something like this and will drastically decrease the framerate of a game. As a general rule, RenderStepped should only be used when manipulating the camera.

Apparently, both Heartbeat and RenderStepped can both decrease FPS if performance-intensive code is placed in them. But I guess he doesn’t need Heartbeat for this.