Character following the closest player

Hello! I’m really having a hard time making a character following the closest player to it, there are 2 reasons:

  • Extreme lag (studio almost crashing)
  • The character for some reason sometimes does not detect the player, and even being very close to it, it does not detect the player anymore, so maybe it’s the detection distance.

So, I would like to have the script below this text to make it way less laggy and also make it detect the player at a big distance (like 100 studs).

Players = game:GetService("Players")
PathfindingService = game:GetService("PathfindingService")

local closest_player_distance = math.huge
local closest_player_hrp = nil

local path = PathfindingService:CreatePath({
	AgentRadius = 5,
	AgentHeight = 6,
	AgentCanJump = false,
	AgentCanClimb = false,
	WaypointSpacing = 2
})

local waypoints = {}

local humanoid = script.Parent:WaitForChild("Humanoid")
local humanoidrootpart = script.Parent:WaitForChild("HumanoidRootPart")

local whileLoopCoroutine : thread = nil

function UpdatePathfinding()
	if whileLoopCoroutine ~= nil then
		coroutine.close(whileLoopCoroutine)
	end
	
	--Source : https://devforum.roblox.com/t/how-to-make-monster-follow-the-player/1554990/4
	--I still did edit things :)
	for i, player in pairs(Players:GetPlayers()) do
		--Check if the player has a character
		local character = player.Character
		if not character then
			return;
		end
		
		local hrp = character:FindFirstChild("HumanoidRootPart") --Yes, HRP = HumanoidRootPart
		if not hrp then
			return;
		end
		
		local this_distance = (humanoidrootpart.Position - hrp.Position).Magnitude
		if this_distance < closest_player_distance then
			closest_player_distance = this_distance
			closest_player_hrp = hrp
		end
	end
	
	path:ComputeAsync(humanoidrootpart.Position, closest_player_hrp.Position) --Info: ComputeAsync(start : Vector3, end : Vector3) : ()
	waypoints = path:GetWaypoints()
	
	whileLoopCoroutine = coroutine.create(function()
		while true do
			for i, waypoint in pairs(waypoints) do
				
				if waypoint.Action == Enum.PathWaypointAction.Walk then
					humanoid:ChangeState(Enum.HumanoidStateType.Running)
				end
				
				humanoid:MoveTo(waypoint.Position, closest_player_hrp)
				humanoid.MoveToFinished:Wait()
			end
		end
	end)

	coroutine.resume(whileLoopCoroutine)
end

function Init()
	while true do
		UpdatePathfinding()
		wait(0.1)
	end
end

Init()

And as you can see in the script, I tried my best to make this topic’s answer #4 work for me, but my skills have limits so it’s not efficient at all.

The topic that I linked above is the best topic I found so far about this, but it’s not working well enough and couldn’t find any better solution sadly :(.

Before replying, here’s how my character works:

PS:

  • please don’t make (if possible) a script with hundreds of lines that aren’t very important, because this would make the script easier to edit for me.
    For example, don’t to put methods (character:Pathfind() for example, instead just use Pathfind()) because they complexify the whole script.
  • Make it with no delay for calculation, because even with the lag, I could see they stop a bit because I put a wait(0.1) in the while loop but this was a mistake
  • Don’t make huge tables for data, the character has a Configuration folder (that is named Configurations). For each of the values in this folder, just describe the value so I can put it how it should work for my character.

If you do not understanding something about my post (which I wouldn’t be surprised about because I suck at explaining), tell me in the answers please.

1 Like

Your while true do loop does not have a wait when their is no waypoints…

if #waypoints ==0 then task.wait(1) else 
for i, waypoint....
end

I would suggest trying to eliminate the while true do loop in favor of the for loop instead.
Unless you are loopint through the waypoints. indefinitely.
This is my centralize findpath function my games global module. It is very good function I use as my all in one solution for NPC pathing.
You can elimnate any depencies in here mostly without issue. The Checkspace function is just to make sure enemys are not walking on top of players or each other.

function dfg.FindPath(Subject,Pos)
 if not PathfindingService then   PathfindingService = game:GetService("PathfindingService") end
    if not RunService then   RunService = game:GetService("RunService") end
    spawn(function()
	local character = Subject
	local humanoid = character:WaitForChild("Humanoid")
	local TEST_DESTINATION = Pos
	local waypoints
	local nextWaypointIndex
	local reachedConnection
	local blockedConnection
	local PathingTag
	local tagConnection
	local Offset
	local root= Subject:FindFirstChild("HumanoidRootPart")
	if root==nil then
		root=Subject:FindFirstChildOfClass("BasePart")
	end
	PathingTag= Subject:FindFirstChild("PathingTag")	
	if PathingTag==nil then
		PathingTag=script.PathingTag:Clone()
		PathingTag.Name="PathingTag"
		PathingTag.Parent=Subject	
	end
      local Targ     
        if Pos==nil then 
              
    Targ=humanoid.Parent:FindFirstChild("Targeting")
	if Targ~=nil then
		local t=Targ.Value
		if t~=nil then
			Pos=t.Position
		end
	end
	------print(tostring(Subject).." is transfering nil position.")
	end	
	local flyer=false
	if Subject:FindFirstChild("Occupied") and Subject:FindFirstChild("MagicID") then
		if Subject.Occupied.Value==false and Subject.MagicID:GetAttribute("Fly")==true then
			flyer=true
		--print("flyer debug true")	
		end
	end
	if Pos~=nil then
            local path = PathfindingService:CreatePath()
            PathingTag.Value=true
		local function followPath(locstance,Subject)
		-- Compute the path
		local Destin
		local Target=locstance	
		if typeof(Target) ~="Vector3" then
		Offset=Target.CFrame:ToWorldSpace(CFrame.new(0,0,Target.Size.Z/2)).Position
		Destin=CheckSpace(root,Vector3.new(math.floor(Offset.X),math.floor(Offset.Y),math.floor(Offset.Z)))
		else 
			Destin=locstance
		end
		local function DisconnectPath()
			PathingTag:SetAttribute("Complete",true)
			PathingTag:SetAttribute("Running",false)
		if reachedConnection then	reachedConnection:Disconnect()end
		if blockedConnection and flyer==false then	blockedConnection:Disconnect()end
		if tagConnection then	tagConnection:Disconnect()end
		end
		tagConnection= PathingTag:GetPropertyChangedSignal("Value"):Connect(function()
			if PathingTag.Value==true then
				DisconnectPath()
			end
		end)
		local success, errorMessage = pcall(function()
			path:ComputeAsync(character.PrimaryPart.Position, Destin)
		end)
		if success and path.Status == Enum.PathStatus.Success then --and flyer==false then
			-- Get the path waypoints
			waypoints = path:GetWaypoints()
			-- Detect if path becomes blocked
			blockedConnection = path.Blocked:Connect(function(blockedWaypointIndex)
				-- Check if the obstacle is further down the path
					if blockedWaypointIndex >= nextWaypointIndex and flyer==false then
					-- Stop detecting path blockage until path is re-computed
					blockedConnection:Disconnect()
					-- Call function to re-compute new path
					if locstance~=nil and PathingTag.Value==false then
						tagConnection:Disconnect()
						followPath(Target,Subject)
					else 
				DisconnectPath()
					end	
				end
			end)
			
		
			-- Detect when movement to next waypoint is complete
			if not reachedConnection then
				reachedConnection = humanoid.MoveToFinished:Connect(function(reached)
					if reached and nextWaypointIndex < #waypoints then
						if PathingTag.Value==true then
							DisconnectPath()
						end
						-- Increase waypoint index and move to next waypoint
						nextWaypointIndex += 1
						-- Use boat if waypoint modifier ID is "UseBoat"; otherwise move to next waypoint						
							humanoid:MoveTo(waypoints[nextWaypointIndex].Position)	
							PathingTag:SetAttribute("Position",waypoints[nextWaypointIndex].Position+Vector3.new(0,5,0))
					else
						DisconnectPath()
					end
				end)
			end
			-- Initially move to second waypoint (first waypoint is path start; skip it)
			nextWaypointIndex = 2
				humanoid:MoveTo(waypoints[nextWaypointIndex].Position)
				PathingTag:SetAttribute("Position",waypoints[nextWaypointIndex].Position+Vector3.new(0,5,0))
                elseif flyer==true and success and path.Status == Enum.PathStatus.Success  then
				--warn("Path not computed!", errorMessage)
				--if not reachedConnection then
				PathingTag:SetAttribute("Position",Destin+Vector3.new(0,5,0))
				humanoid:MoveTo(Destin+Vector3.new(0,5,0))
				reachedConnection = humanoid.MoveToFinished:Connect(function(reached)
						if reached  then
						--	if PathingTag.Value==true then
								DisconnectPath()
							--end
							-- Increase waypoint index and move to next waypoint
						--	nextWaypointIndex += 1
							-- Use boat if waypoint modifier ID is "UseBoat"; otherwise move to next waypoint						
							--humanoid:MoveTo(waypoints[nextWaypointIndex].Position)	
						--	PathingTag:SetAttribute("Position",Destin+Vector3.new(0,5,0))
							--PathingTag:SetAttribute("Position",waypoints[nextWaypointIndex].Position+Vector3.new(0,5,0))
						--else
							--DisconnectPath()
						end
					end)
				--end
				
			else	
				DisconnectPath()
				if humanoid.Parent:FindFirstChildOfClass("BasePart")~=nil then
				if humanoid.Parent:FindFirstChild("HumanoidRootPart")~=nil then
						humanoid:MoveTo(humanoid.Parent.HumanoidRootPart.Position)
						PathingTag:SetAttribute("Position",waypoints[nextWaypointIndex].Position+Vector3.new(0,5,0))
					else Humanoid:MoveTo(humanoid.Parent:FindFirstChildOfClass("BasePart").Position)	
						PathingTag:SetAttribute("Position",humanoid.Parent:FindFirstChildOfClass("BasePart").Position+Vector3.new(0,5,0))
					end	
				end	
			end
		end	
	if PathingTag:GetAttribute("Running")==true and (Subject.HumanoidRootPart.Velocity).Magnitude>1 then
		repeat task.wait(.0333) until PathingTag:GetAttribute("Complete")==true
		end		
	
	PathingTag.Value=false
	PathingTag:SetAttribute("Complete",false)
	PathingTag:SetAttribute("Running",true)
	--PathingTag:SetAttribute("Position",TEST_DESTINATION)
			followPath(Pos,Subject)
        end
    end) 
end
2 Likes

Just add a task.wait().

Code:

Players = game:GetService("Players")
PathfindingService = game:GetService("PathfindingService")

local closest_player_distance = math.huge
local closest_player_hrp = nil

local path = PathfindingService:CreatePath({
	AgentRadius = 5,
	AgentHeight = 6,
	AgentCanJump = false,
	AgentCanClimb = false,
	WaypointSpacing = 2
})

local waypoints = {}

local humanoid = script.Parent:WaitForChild("Humanoid")
local humanoidrootpart = script.Parent:WaitForChild("HumanoidRootPart")

local whileLoopThread : thread = nil

function UpdatePathfinding()
	if whileLoopThread then
		task.cancel(whileLoopThread)
	end

	--Source : https://devforum.roblox.com/t/how-to-make-monster-follow-the-player/1554990/4
	--I still did edit things :)
	for i, player in Players:GetPlayers() do
		--Check if the player has a character
		local character = player.Character
		if not character then
			return;
		end

		local hrp = character:FindFirstChild("HumanoidRootPart") --Yes, HRP = HumanoidRootPart
		if not hrp then
			return;
		end

		local this_distance = (humanoidrootpart.Position - hrp.Position).Magnitude
		if this_distance < closest_player_distance then
			closest_player_distance = this_distance
			closest_player_hrp = hrp
		end
	end

	path:ComputeAsync(humanoidrootpart.Position, closest_player_hrp.Position) --Info: ComputeAsync(start : Vector3, end : Vector3) : ()
	waypoints = path:GetWaypoints()

	whileLoopThread = task.spawn(function()
		while task.wait() do
			for i, waypoint in waypoints do
				if waypoint.Action == Enum.PathWaypointAction.Walk then
					humanoid:ChangeState(Enum.HumanoidStateType.Running)
				end

				humanoid:MoveTo(waypoint.Position, closest_player_hrp)
				humanoid.MoveToFinished:Wait()
			end
		end
	end)
end

function Init()
	while task.wait(0.1) do
		UpdatePathfinding()
	end
end

Init()
1 Like

Yes also handle when the #waypoints are 0. In fact I don’t see why you would loop through the waypoints indefinitely Your for i, waypoint loop should handle the waypoint array. I assume looping through it again would just make it repeat its path.

2 Likes

Hello, thanks for your reply!

I forgot to give my structure because I’m stupid (I’ll add it to the original message (the topic’s message)), but your solution seems great! Sadly, I can’t really find a way to integrate this to my script. I’m still trying but it’s causing me a headache lol

2 Likes
Players = game:GetService("Players")
PathfindingService = game:GetService("PathfindingService")

local closest_player_distance = math.huge
local closest_player_hrp = nil

local path = PathfindingService:CreatePath({
	AgentRadius = 5,
	AgentHeight = 6,
	AgentCanJump = false,
	AgentCanClimb = false,
	WaypointSpacing = 2
})

local waypoints = {}

local humanoid = script.Parent:WaitForChild("Humanoid")
local humanoidrootpart = script.Parent:WaitForChild("HumanoidRootPart")

local whileLoopCoroutine : thread = nil

function UpdatePathfinding()
	if whileLoopCoroutine ~= nil then
		coroutine.close(whileLoopCoroutine)
	end
	
	--Source : https://devforum.roblox.com/t/how-to-make-monster-follow-the-player/1554990/4
	--I still did edit things :)
	for i, player in pairs(Players:GetPlayers()) do
		--Check if the player has a character
		local character = player.Character
		if not character then
			return;
		end
		
		local hrp = character:FindFirstChild("HumanoidRootPart") --Yes, HRP = HumanoidRootPart
		if not hrp then
			return;
		end
		
		local this_distance = (humanoidrootpart.Position - hrp.Position).Magnitude
		if this_distance < closest_player_distance then
			closest_player_distance = this_distance
			closest_player_hrp = hrp
		end
	end
	
	path:ComputeAsync(humanoidrootpart.Position, closest_player_hrp.Position) --Info: ComputeAsync(start : Vector3, end : Vector3) : ()
	waypoints = path:GetWaypoints()
	
	whileLoopCoroutine = coroutine.create(function()
		--while true do
			for i, waypoint in pairs(waypoints) do
				
				if waypoint.Action == Enum.PathWaypointAction.Walk then
					humanoid:ChangeState(Enum.HumanoidStateType.Running)
				end
				
				humanoid:MoveTo(waypoint.Position, closest_player_hrp)
				humanoid.MoveToFinished:Wait()
			end
		--end
	end)
	coroutine.resume(whileLoopCoroutine)
end

function Init()
	while true do
		UpdatePathfinding()
		wait(0.1)
	end
end

Init()

So I think where you are losing performing is this while true do loop i commented it out. I assume it would work as you intend with that modification.
Beyond that I think the issue lies within whileLoopCoroutine = coroutine.create(function()
while true do
Does this thread yield to the main thread or is it its own thread? Why use coroutine at all?
Isn’t this designed so that the npc may do its pathing?
Also on this line

Your functions if not conditions break your for do loop. So if a character had no hrp it would break the function.
That’s one of the point in the function I shared where it doesn’t fire again until the previous pathing is completed the pathingtag is set to completed.

1 Like

I used coroutine so I can stop it somewhere else in this script, so it does not yield the whole script, well, forever.

I’ll see the code right now!

After reviewing your code again I think eliminating the while true do loop should help a little bit. But their is an issue with your pathing in that it can accumulate multiple paths So you should create a thing liek I did in my function called a PathingTag and set a bool value as completed or not completed. and break your for loop when it’s triggered or wait everytime you check your players until the pathing is complete
So you start pathing. set the tag to true. and add break if tag is false
complete pathing set tag to false.
When you check pathing either do pathing if the tag is false or set the tag to false to stop the previous pathing.

A different way to do this is make a global variable
local plrpaths={}
if plrpaths[player.Name]==nil then
plrpaths[player.Name]=false
end
if plrpaths[player.Name]==false then
plrpaths[player.Name]=true

or

 plrpaths[player.Name]=false
then in your for loop
 if plrpaths[player.Name]==false then break end
1 Like

I’ll take a better look at it, but I’ll try my best lol. Hope it will work!
Sadly enough I’m very very very bad at pathfinding so I don’t really know what I’m doing to be honest :pensive: but I’ll still try and see if it works.

1 Like

It’s easy! the pathing function is returning a table

waypoints={[1]=CFrame(),
[2]=CFrame as well,
etc}

Your for i,waypoint in waypoints do is going through that table in sequence and using the humanoids moveto to move the player character.
But if your function is running on its own thread while you are looping the update paths function every .1 seconds. You have to tell it to stop navigating the array. or it will continue until completed. Which will cause your npcs to not move or stutter their movements

The problem is that even myself I don’t know how to use my own script (so I feel stupid but let’s forget this). What can set the plrpaths[player.Name] (except the for loop)?
Also, weirdly enough, the character just make something weird that I can’t explain because I don’t have the words in English or in my own language, but they seem to be bugging out a bit. I’ll check your solutions and see if it fixes

plrpaths={}-- a table to store the player pathing bool
--plrpaths[player.Name]=false --example usage pathing bool set to false assume not pathing
-- `plrpaths[player.Name]=true --example usage pathing bool set to true character is pathing
at the end of your for i=1 waypoitn, in waypoints do function 
Also add a condition inside the for loop that says if plrpaths[player.Name]==false then break end

 plrpaths[player.Name]=false

Main point im trying to illustrate is create a external table plrpaths to store the playersi n an array.
plrpaths[player.Name] this is a direct index method to make navigating the table simple.
set the bool to true when pathing starts set it to false when pathing ends
Or
Do that and set add the break in the for loop so you can set the bool to false and create a new path.

Maybe I should lower the .1 to like 0.05 because I noticed some delay because of this, which makes sense because 0.1 second is a lot. But what will tell them to stop navigating? The method at the start? I don’t really know because I’m really not in my best capacities right now so I’m taking so much time programming, understanding and even replying with a few lines like I just did.
But is there an alternative to my while true do loop? Because I find it really useless because I feel like there’s another solution to this.

Oh yeah, forgot to mention: the NPC should be able to suddenly switch player, because it constantly needs to find another player if there’s a closer one.

I’m telling you how to fix your function and it’s very simple. It’s akin to a debounce but since you want to utilize a for i,v in players do then you need to apply that logic your debounce. Which this is the way it’s done. Or you can cretae an object.

If you’re not going to work on your own code I’m not going to do it after explaining it in detail several times. It’s a simple concept and a effective solution that allows the npc to change targets according to your updatepaths function.

I never said you should use my script, the script you originally gave me was too hard to apply to it. So that’s why I added the structure of my character to the original message of that topic.

I’ll just go do something, I’ll be back later. Sorry if you feel that I’m mean or I make you angry, it’s really not my goal :slight_smile:

okay.

plrtbl={}
plrtbl[player.Name]=false

was too hard to apply to your code.

It’s a pathing state true or false. Good luck with your pathing function.

if plrtbl[player.Name]==false then break end--would allow you to change the pathing subject whenever

We are talking about your code. I posted my code as an example. That you could compare to, and see your issue. I pointed out your issue and gave you the solution. By comparing yours and mine and applying principals of programming and function to yours. That being said I’m out. Sit down and add those 5 lines of code to your function where they need to go. if you want to make your function able to break the previous paths and not spawn multiple paths at once for the same player.

I ended up by fixing it by remaking the whole script. Thanks for your help!

And yes, I’m not gonna leave the topic without giving the solution, of course! So, because I know nothing about Pathfinding, I decided to watch this YouTube video, it’s the most up-to-date one, and I made it adapted for myself.

Modified script
--[[
SOURCE: https://youtu.be/69yK42OOuNo
I edited things and made it adapted for my character. Watch the video to get explainations etc.
]]--
local PathfindingService = game:GetService("PathfindingService")

local npc = script.Parent
local humanoid = npc:FindFirstChild("Humanoid")
local humanoidRootPart = npc:FindFirstChild("HumanoidRootPart")
humanoidRootPart:SetNetworkOwner(nil) --Prevents some lag with PathfindingService

local walkAnim = humanoid.Animator:LoadAnimation(script.Walk)
local attackAnim = humanoid.Animator:LoadAnimation(script.Attack)

local pathParams = {
	AgentHeight = 4,
	AgentRadius = 4.5,
	AgentCanJump = false
}

local rayParams = RaycastParams.new()
rayParams.FilterType = Enum.RaycastFilterType.Exclude
rayParams.FilterDescendantsInstances = {workspace.Map, workspace.NPCs}

local lastPos
local status
local animPlaying = false

local configs = npc:FindFirstChild("Configurations")
local range = configs:FindFirstChild("MaximumDetectionDistance").Value
local damage = configs:FindFirstChild("AttackDamage").Value

local function canSeeTarget(target)
	local origin = humanoidRootPart.Position
	local direction = (target.HumanoidRootPart.Position - humanoidRootPart.Position).Unit * range
	local ray = workspace:Raycast(origin, direction, rayParams)
	
	if ray and ray.Instance then
		if ray.Instance:IsDescendantOf(target) then
			return true
		end
	end
	
	return false
end

local function findTarget()
	local players = game.Players:GetPlayers()
	local maxDistance = range
	local nearestTarget
	
	for i, player in pairs(players) do
		local char = player.Character
		if char then
			local target = char
			local distance = (humanoidRootPart.Position - target.HumanoidRootPart.Position).Magnitude
			
			if distance < maxDistance and canSeeTarget(target) then
				nearestTarget = target
				maxDistance = distance
			end
		end
	end
	
	return nearestTarget
end

local function getPath(destination)
	local path = PathfindingService:CreatePath(pathParams)
	
	path:ComputeAsync(humanoidRootPart.Position, destination.Position)
	
	return path
end

local function attack(target)
	local distance = (humanoidRootPart.Position - target.HumanoidRootPart.Position).Magnitude
	local debounce = false
	
	if distance > 5 then
		humanoid:MoveTo(target.HumanoidRootPart.Position)
	else
		if debounce == false then
			debounce = true
			
			attackAnim:Play()
			target.Humanoid.Health -= damage
			task.wait(0.5)
			debounce = false
		end
	end
end

local function walkTo(destination)
	local path = getPath(destination)
	
	if path.Status == Enum.PathStatus.Success then
		for i, waypoint in pairs(path:GetWaypoints()) do
			path.Blocked:Connect(function()
				path:Destroy()
			end)
			
			if not animPlaying then
				walkAnim:Play()
				attackAnim:Stop()
				animPlaying = true
			end
			
			local target = findTarget()
			
			if target and target.Humanoid.Health > 0 then
				lastPos = target.HumanoidRootPart.Position
				attack(target)
				break
			else
				if waypoint.Action == Enum.PathWaypointAction.Jump then
					humanoid:ChangeState(Enum.HumanoidStateType.Jumping)
				end
				
				if lastPos then
					humanoid:MoveTo(lastPos)
					humanoid.MoveToFinished:Wait()
					lastPos = nil
					break
				else
					humanoid:MoveTo(waypoint.Position)
					humanoid.MoveToFinished:Wait()
				end
			end
		end
	else
		return;
	end
end

local function patrol()
	local waypoints = workspace.NPCs.NoCharacterWaypoints:GetChildren()
	local randomNum = math.random(1, #waypoints)
	walkTo(waypoints[randomNum])
end

while task.wait(0.2) do
	patrol()
end
1 Like

It looks good! I would reccomend implementing your pathing function to a module and calling it that way so you can use it as a all in one solution for things like NPC pathing

modulescript=require(script.Parent.ModuleScript)--returns a the contents of the module as a table including tables and functions you can use as a library
``
1 Like

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