Trouble with Pathfinding + Chasing Player

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

  1. What do you want to achieve? Keep it simple and clear!
    I want the Monster to turn quicker and increase responsiveness to player changing their position(moving) and also have solutions when it gets stuck

  2. What is the issue? Include screenshots / videos if possible!
    YOUTUBE LINK: https://youtu.be/9DbD8D213uw

  3. What solutions have you tried so far? Did you look for solutions on the Developer Hub?
    I’ve tried reducing the task.wait() in while loops but it didnt help and its already pretty low
    I think maybe neatening my code may help identify the problem better but I have no idea on how I should do that while preserving the functionality.

This Model is custom made using blender, it still have all the components as a regular rig but that shouldn’t be the root cause of any problems found here.

Whole script:(Server)

--[[TODO 
1.) Grimace can wander around the building with pathfinding like random spots that arent occupied by buildings	
2.) He can be alerted by lound sounds like kicking doors
3.) When player gets in line of sight he starts chasing
4.) Whoever he is chasing a sound should be played to them only with a frame so show how close he is
5.) He should have attribute that talks about his state(Wandering, Chasing, Killing)
]]--

local GrimaceModel = script.Parent
local Humanoid = GrimaceModel:WaitForChild("Humanoid")
local G_HumanoidRootPart = GrimaceModel:WaitForChild("HumanoidRootPart")

local PathfindingService = game:GetService("PathfindingService")
local path = PathfindingService:CreatePath({
	AgentRadius = 6,
	AgentHeight = 6,
	AgentCanJump = false
})

local PatrolPoints = game.Workspace.HorrorMap.PatrolPoints
local RandoPatrolOrder = {}

local LineOfSightRequired = 0.6 --The perferction of line of sight minimum(1 = Perfect, -1 Perfect Oppose, 0 Perpendicular)
local ChaseCooldown = 1 --To make grimace chill before he chase again after chase time
local isChasing = false --Debounce to ensure functionality
local Range = 175 --There is fog so we gonna make range to make it even more realistic


--//Utility Functions\\--
local function FindVictims()
	local victims = {}

	for _, player in pairs(game.Players:GetChildren()) do
		if player.Character and player.Character:FindFirstChild("HumanoidRootPart") then
			local HumanoidRootPart = player.Character:FindFirstChild("HumanoidRootPart")
			local distance = (G_HumanoidRootPart.Position - HumanoidRootPart.Position).magnitude

			local raycastParams = RaycastParams.new()
			raycastParams.FilterDescendantsInstances = {GrimaceModel, workspace.HorrorMap.Raycasting}
			raycastParams.FilterType = Enum.RaycastFilterType.Exclude
			raycastParams.IgnoreWater = true

			local origin = G_HumanoidRootPart.Position
			local direction = (HumanoidRootPart.Position - G_HumanoidRootPart.Position).unit
			local rayLength = distance

			local raycastResult = workspace:Raycast(origin, direction * rayLength, raycastParams)

			if raycastResult then
				local part = Instance.new("Part", workspace.HorrorMap.Raycasting)
				part.Size = Vector3.new(0.1, 0.1, rayLength)
				part.CFrame = CFrame.new(origin, origin + direction * rayLength) * CFrame.new(0, 0, -rayLength / 2)
				part.Anchored = true
				part.CanCollide = false
				part.BrickColor = BrickColor.new("Bright red")
				part.Transparency = 0.5
				game.Debris:AddItem(part, 0.5) -- Remove part after 0.5 seconds

				if player.Character:FindFirstChild(raycastResult.Instance.Name) then
					-- If the ray does not hit anything, the line of sight is clear
					if distance <= Range then
						local G_LV = G_HumanoidRootPart.CFrame.LookVector
						local directionToTarget = (HumanoidRootPart.Position - G_HumanoidRootPart.Position).unit
						local dotProduct = G_LV:Dot(directionToTarget)

						if dotProduct >= LineOfSightRequired then
							table.insert(victims, player.Name)  -- Add the character to the victims table
						end
					end
				end
			end
		end
	end

	return victims  -- Return the table of valid characters
end

local function AttemptSetTarget()
	local victims = FindVictims()

	if #victims > 0 then
		if isChasing == false then
			isChasing = true

			local targetPlayerName = victims[math.random(1, #victims)]

			Humanoid:SetAttribute("TargettingPlayer", targetPlayerName)

			return true
		end
	else
		return false
	end
end

--//Patrolling Coroutine\\--
coroutine.wrap(function()
	while Humanoid:GetAttribute("Patrolling") == true do

		if Humanoid:GetAttribute("Status") ~= "Wandering" then
			Humanoid:SetAttribute("Status", "Wandering")
		end

		--Initializing Order if needed
		if #RandoPatrolOrder == 0 then
			for _, v in pairs(PatrolPoints:GetChildren()) do
				table.insert(RandoPatrolOrder, v)
				v.Color = Color3.fromRGB(162, 162, 162)
			end
		end

		local randomPoint : Part = RandoPatrolOrder[math.random(1, #RandoPatrolOrder)]

		randomPoint.Color = Color3.fromRGB(0, 255, 0)

		local succ, err = pcall(function()
			path:ComputeAsync(G_HumanoidRootPart.Position, randomPoint.Position)
		end)

		if succ then
			local waypoints = path:GetWaypoints()

			for i, waypoint in pairs(waypoints) do
				if isChasing == false then --Stops in the middle of computed path is chasing is true
					Humanoid:MoveTo(waypoint.Position)
					Humanoid.MoveToFinished:Wait()
				end
			end

			table.remove(RandoPatrolOrder, table.find(RandoPatrolOrder, randomPoint))
			
			task.wait(0)
		else
			warn(err)
		end

		randomPoint.Color = Color3.fromRGB(255, 0, 0)
	end
end)()


--//Targetting Coroutine\\--
local targetting = false

Humanoid:GetAttributeChangedSignal("TargettingPlayer"):Connect(function()
	if Humanoid:GetAttribute("TargettingPlayer") ~= "" then
		targetting = true
		
		while targetting do

			if Humanoid:GetAttribute("Status") ~= "Chasing" then
				Humanoid:SetAttribute("Status", "Chasing")
			end

			local targetsName = Humanoid:GetAttribute("TargettingPlayer")
			local targetsPlayer = game.Players:FindFirstChild(targetsName)
			local targetsChar = targetsPlayer.Character or targetsPlayer.CharacterAdded:Wait()
			local targetsHRP = targetsChar:WaitForChild("HumanoidRootPart")

			local succ, err = pcall(function()
				path:ComputeAsync(G_HumanoidRootPart.Position, targetsHRP.Position)
			end)

			if succ then

				local waypoints = path:GetWaypoints()

				for i, waypoint in ipairs(waypoints) do
					if isChasing == true then
						Humanoid:MoveTo(waypoint.Position)
						Humanoid.MoveToFinished:Wait()
					end
				end

			else
				warn(err)
			end

			task.wait(0)
		end
	else
		targetting = false --Targetting Player field is empty theirfor no one to target
	end
end)

--//Main Couroutine\\--
coroutine.wrap(function()
	while true do

		if AttemptSetTarget() and isChasing == true then

			Humanoid:SetAttribute("Patrolling", false)
		end

		task.wait(0.1)
	end
end)()

If you can give me tips of suggestions that may help me in the future It is highly appreciated.

1 Like

I’m not good with pathfinding, but did u try to lower wait time in this loop:

coroutine.wrap(function()
	while true do
		if AttemptSetTarget() and isChasing == true then
			Humanoid:SetAttribute("Patrolling",false)
		end
		task.wait(0.1)
	end
end)()

also, you don’t have to put 0 in the wait, you can just leave it blank

2 Likes

Oh, good catch and ive seen comparisins of using task.wait(), wait(), task.wait(0) and so on and notably putting 0 in task.wait actually is slightly shorter than calling alone to my knowledge, im not sure if it was changed although it probably was. But ill shorten it and tell you the results

1 Like

if that doesnt help then its most likely the Humanoid.MoveToFinished:Wait() line

for i, waypoint in ipairs(waypoints) do
	if isChasing == true then
		Humanoid:MoveTo(waypoint.Position)
		--Humanoid.MoveToFinished:Wait()
	end
end
1 Like

Result when i put task.wait(0.1) to task.wait(0) everything else is the same but the raycast visualization seems to render more frequently

Ok it seems to be more responsive when i remove that line however, that line helps to make sure it doesnt get stuck, now when I go behind a wall the monster just gets stuck trying to walk through the wall instead of pathfinding its way around it.

replace:

local waypoints = path:GetWaypoints()

for i, waypoint in ipairs(waypoints) do
	if isChasing == true then
		Humanoid:MoveTo(waypoint.Position)
		Humanoid.MoveToFinished:Wait()
	end
end

end

with this:

local waypoints = path:GetWaypoints()

if #waypoints > 0 then
	if isChasing == true then
		Humanoid:MoveTo(waypoint.Position)
	end
end
my bad explanation:

The first code sample pathfinds through all the waypoints and waits until it completes the way point before it can move onto the other waypoints (stuck in a queue), The new sample only moves to the first waypoint, and because you’re calling the function at a fast rate, the first waypoint would constantly be updated with the correct pathfinding and with good response time

edit: forgot to add the if statement for the chasing part

1 Like

Sorry for late response: I tried it no diffferent effect noticed ( I already deleted the Humanoid.MoveToFinished:Wait())

just realized i messed up the sample, heres the new one which should work:

local waypoints = path:GetWaypoints() -- table of waypoints

--[[ replaced for i loop with if statement so we arent getting
the waypoints we dont need
--]]

if #waypoints > 0 then -- if there is more than 0 waypoints
	if isChasing == true then
		Humanoid:MoveTo(waypoints[1].Position) -- only move to the first waypoint
	end
end

and sorry for the even later response

1 Like

Replacing that means it only moves to the first position. If im implementing it correctly.

yeah, since its recalculating the path every time that first waypoint is repeatly getting updated

1 Like

I never made any code that makes the first waypoint repeatedly update if you are talking about how it works, im pretty sure its a table of waypoints that stay the same until the path is re-computed thats the whole reason i use “for i, v in pairs(waypoints)” since it loops in an ordered structure through the waypoints

in your code the path has to re-computed for the “for i loop” to run, so using the first waypoint would be fine

1 Like

Ok if thats the case can you show me how to implement it, right now I have the section of the code like this:

local waypoints = path:GetWaypoints()

if #waypoints > 0 then -- if there is more than 0 waypoints
	if isChasing == true then
		Humanoid:MoveTo(waypoints[1].Position) -- only move to the first waypoint
	end
end

Instead of this:

local waypoints = path:GetWaypoints()
	for _, waypoint in pairs(waypoints) do
		if #waypoints > 0 then
			if isChasing == true then
				Humanoid:MoveTo(waypoint.Position)
			end
	end
end 

This is the implemented Script, I have tested this in game and it works😸

--[[TODO 
1.) Grimace can wander around the building with pathfinding like random spots that arent occupied by buildings	
2.) He can be alerted by lound sounds like kicking doors
3.) When player gets in line of sight he starts chasing
4.) Whoever he is chasing a sound should be played to them only with a frame so show how close he is
5.) He should have attribute that talks about his state(Wandering, Chasing, Killing)
]]--

local GrimaceModel = script.Parent
local Humanoid = GrimaceModel:WaitForChild("Humanoid")
local G_HumanoidRootPart = GrimaceModel:WaitForChild("HumanoidRootPart")

local PathfindingService = game:GetService("PathfindingService")
local path = PathfindingService:CreatePath({
	AgentRadius = 6,
	AgentHeight = 6,
	AgentCanJump = false
})

local PatrolPoints = game.Workspace.HorrorMap.PatrolPoints
local RandoPatrolOrder = {}

local LineOfSightRequired = 0.6 --The perferction of line of sight minimum(1 = Perfect, -1 Perfect Oppose, 0 Perpendicular)
local ChaseCooldown = 1 --To make grimace chill before he chase again after chase time
local isChasing = false --Debounce to ensure functionality
local Range = 175 --There is fog so we gonna make range to make it even more realistic


--//Utility Functions\\--
local function FindVictims()
	local victims = {}

	for _, player in pairs(game.Players:GetChildren()) do
		if player.Character and player.Character:FindFirstChild("HumanoidRootPart") then
			local HumanoidRootPart = player.Character:FindFirstChild("HumanoidRootPart")
			local distance = (G_HumanoidRootPart.Position - HumanoidRootPart.Position).magnitude

			local raycastParams = RaycastParams.new()
			raycastParams.FilterDescendantsInstances = {GrimaceModel, workspace.HorrorMap.Raycasting}
			raycastParams.FilterType = Enum.RaycastFilterType.Exclude
			raycastParams.IgnoreWater = true

			local origin = G_HumanoidRootPart.Position
			local direction = (HumanoidRootPart.Position - G_HumanoidRootPart.Position).unit
			local rayLength = distance

			local raycastResult = workspace:Raycast(origin, direction * rayLength, raycastParams)

			if raycastResult then
				local part = Instance.new("Part", workspace.HorrorMap.Raycasting)
				part.Size = Vector3.new(0.1, 0.1, rayLength)
				part.CFrame = CFrame.new(origin, origin + direction * rayLength) * CFrame.new(0, 0, -rayLength / 2)
				part.Anchored = true
				part.CanCollide = false
				part.BrickColor = BrickColor.new("Bright red")
				part.Transparency = 0.5
				game.Debris:AddItem(part, 0.5) -- Remove part after 0.5 seconds

				if player.Character:FindFirstChild(raycastResult.Instance.Name) then
					-- If the ray does not hit anything, the line of sight is clear
					if distance <= Range then
						local G_LV = G_HumanoidRootPart.CFrame.LookVector
						local directionToTarget = (HumanoidRootPart.Position - G_HumanoidRootPart.Position).unit
						local dotProduct = G_LV:Dot(directionToTarget)

						if dotProduct >= LineOfSightRequired then
							table.insert(victims, player.Name)  -- Add the character to the victims table
						end
					end
				end
			end
		end
	end

	return victims  -- Return the table of valid characters
end

local function AttemptSetTarget()
	local victims = FindVictims()

	if #victims > 0 then
		if isChasing == false then
			isChasing = true

			local targetPlayerName = victims[math.random(1, #victims)]

			Humanoid:SetAttribute("TargettingPlayer", targetPlayerName)

			return true
		end
	else
		return false
	end
end

--//Patrolling Coroutine\\--
coroutine.wrap(function()
	while Humanoid:GetAttribute("Patrolling") == true do

		if Humanoid:GetAttribute("Status") ~= "Wandering" then
			Humanoid:SetAttribute("Status", "Wandering")
		end

		--Initializing Order if needed
		if #RandoPatrolOrder == 0 then
			for _, v in pairs(PatrolPoints:GetChildren()) do
				table.insert(RandoPatrolOrder, v)
				v.Color = Color3.fromRGB(162, 162, 162)
			end
		end

		local randomPoint : Part = RandoPatrolOrder[math.random(1, #RandoPatrolOrder)]

		randomPoint.Color = Color3.fromRGB(0, 255, 0)

		local succ, err = pcall(function()
			path:ComputeAsync(G_HumanoidRootPart.Position, randomPoint.Position)
		end)

		if succ then
			local waypoints = path:GetWaypoints()

			for i, waypoint in pairs(waypoints) do
				if isChasing == false then --Stops in the middle of computed path is chasing is true
					Humanoid:MoveTo(waypoint.Position)
					Humanoid.MoveToFinished:Wait()
				end
			end

			table.remove(RandoPatrolOrder, table.find(RandoPatrolOrder, randomPoint))
		else
			warn(err)
		end

		randomPoint.Color = Color3.fromRGB(255, 0, 0)
	end
end)()


--//Targetting Coroutine\\--
local targetting = false

Humanoid:GetAttributeChangedSignal("TargettingPlayer"):Connect(function()
	if Humanoid:GetAttribute("TargettingPlayer") ~= "" then
		targetting = true

		while targetting do

			if Humanoid:GetAttribute("Status") ~= "Chasing" then
				Humanoid:SetAttribute("Status", "Chasing")
			end

			local targetsName = Humanoid:GetAttribute("TargettingPlayer")
			local targetsPlayer = game.Players:FindFirstChild(targetsName)
			local targetsChar = targetsPlayer.Character or targetsPlayer.CharacterAdded:Wait()
			local targetsHRP = targetsChar:WaitForChild("HumanoidRootPart")

			local succ, err = pcall(function()
				path:ComputeAsync(G_HumanoidRootPart.Position, targetsHRP.Position)
			end)

			if succ then

				local waypoints = path:GetWaypoints() -- table of waypoints

				--[[ replaced for i loop with if statement so we arent getting
					the waypoints we dont need
					(edit: i forgot that the first waypoint will be where the npc stands, so 
					we need to use the second waypoint)
				--]]

				if #waypoints > 1 then -- if there is more than 1 waypoint
					if isChasing == true then
						Humanoid:MoveTo(waypoints[2].Position) -- only move to the second waypoint
					end
				end

			else
				warn(err)
			end
		end
	else
		targetting = false --Targetting Player field is empty theirfor no one to target
	end
end)

--//Main Couroutine\\--
coroutine.wrap(function()
	-- while task.wait() is better to use then white true do
	while task.wait() do

		if AttemptSetTarget() and isChasing == true then

			Humanoid:SetAttribute("Patrolling", false)
		end
	end
end)()
1 Like

Thank you for your time in advance :sob: I will get back to you as soon as this wifi starts running right.

for my enemy npcs i made them walk straight to the target if there are no obstacles between them (checked with a raycast), so they react instantly if the target is close enough

1 Like

you know i am contributing absolutely nothing to this topic

but i really hate the fact the enemy is called “grimace”, it reminds me of grimace shake and i HATE it

but the code is pretty good though :grin:

1 Like

Thats unfortunate for you because my game is themed around Grimace and the Shake >:) and thanks

This was the approach i was initially going to take but I realized that there were more appropriate methods for my case.