Pathfinding NPCs follow the player in groups/lining up

I made a script where the NPC follows the player using pathfinding.

local character = script.Parent
local humanoid = character.Humanoid

local chasing = false

local PathfindingService = game:GetService("PathfindingService")

local ignoreList = {character}


for i, v in pairs(workspace:GetDescendants()) do
	-- Adds other NPCs and parts to be ignored
	if v:IsA("Model") and v ~= character and v:FindFirstChildOfClass("Humanoid") then
		table.insert(ignoreList, v)
		print("Added "..v.Name.." to ignore list")
	elseif v:IsA("Part") and v.Transparency >= 0.3 or v:FindFirstChild("SeeThrough") then
		table.insert(ignoreList, v)
		print("Added "..v.Name.." to ignore list")
	end
	
	-- Removes from ignore list when changed/deleted
	v.Changed:Connect(function()
		if v:IsA("Model") and v ~= character and v:FindFirstChildOfClass("Humanoid") then
			table.remove(ignoreList, tonumber(v))
			print("Removed "..v.Name.." to ignore list")
		elseif v:IsA("Part") and v.Transparency < 0.3 then
			table.remove(ignoreList, tonumber(v))
			print("Removed "..v.Name.." to ignore list")
		end
	end)
	v.Destroying:Connect(function()
		if v:IsA("Model") and v ~= character and v:FindFirstChildOfClass("Humanoid") then
			table.remove(ignoreList, tonumber(v))
			print("Removed "..v.Name.." to ignore list")
		elseif v:IsA("Part") and v.Transparency < 0.3 then
			table.remove(ignoreList, tonumber(v))
			print("Removed "..v.Name.." to ignore list")
		end
	end)
end

-- Main
local function canSeeTarget(target)
	local origin = character.HumanoidRootPart.Position
	local direction = (target.HumanoidRootPart.Position - character.HumanoidRootPart.Position).Unit * 500
	local raycastParams = RaycastParams.new()
	raycastParams.FilterType = Enum.RaycastFilterType.Exclude
	raycastParams.FilterDescendantsInstances = {ignoreList}
	local ray = workspace:Raycast(origin,direction,raycastParams)

	if ray then
		if ray.Instance:IsDescendantOf(target) then
			return true
		end
	else
		return false
	end
end

local function findTarget()
	local maxDistance = 100000
	local nearestTarget = nil
	
	for i, v in pairs(game.Players:GetPlayers()) do
		if v.Character then
			local target = v.Character
			local distance = (character.HumanoidRootPart.Position - target.HumanoidRootPart.Position).Magnitude
			
			if distance < maxDistance and canSeeTarget(target) and target.Humanoid.Health > 0 then
				nearestTarget = target
				maxDistance = distance
				chasing = true
			else
				chasing = false
			end
		end
	end
	
	return nearestTarget
end

local function getPath(destination)
	local path = PathfindingService:CreatePath(
		{
			["AgentHeight"] = 5,
			["AgentRadius"] = 3,
			["AgentCanJump"] = true
		})
	
	path:ComputeAsync(character.HumanoidRootPart.Position, destination.Position)
	return path
end

local function waypointPath(pos: Vector3) -- Creates the path thingy
	local part = Instance.new("Part")
	part.Name = "Waypoint"
	part.Size = Vector3.new(1,1,1)
	part.Color = Color3.fromRGB(255, 0, 0)
	part.CanCollide = false
	part.CanQuery = false
	part.CanTouch = false
	part.Anchored = true
	part.Position = pos
	part.Parent = workspace
	game.Debris:AddItem(part, 2)
	return part
end

local function walkTo(destination,target)
	local path = getPath(destination)
	local waypoints = path:GetWaypoints()
	
	if path.Status == Enum.PathStatus.Success then
		for index, waypoint in pairs(waypoints) do
			--waypointPath(waypoint.Position)
			humanoid:MoveTo(waypoint.Position)
		end
	end
end

while true do
	task.wait()
	local target = findTarget()
	if target then
		walkTo(target.HumanoidRootPart,target)
	end
end

How it should work

The NPC follows the player when the player is in its sight. If the wall’s transparency is at least 0.3 or contains a boolvalue named “SeeThrough”, it ignores it and walks to the player.

If there are many NPCs with the same follow script, they should not follow the player in groups.

Problems

  1. If the wall’s transparency is 0.3 and higher or if it has the “SeeThrough” boolvalue, it walks to the player, but it just collides with the wall instead of moving to the sides in order to get to the player. If the wall is deleted and when I go past through it, the NPC follows me, but when I go behind the wall which was deleted, the NPC stopped following me when it reached the position of the deleted. When changing or deleting these, it doesn’t remove them from the ignore list.

  2. When there many NPCs following the player, they’re following me in groups or line up, colliding with each other instead of spreading.

I’ve searched topics related to this, but none of these helps.

2 Likes

Is your script local or server side?

It’s a server-sided script. Why would I use a client-sided script to make NPCs move?

make a table for the parts(lines) and place tiny parts sizes to 1,1,1 and put them in the center of the corners and make the rigs travel to the parts like

for i=1, part in ipairs(PartsFolder) do
 character:MoveTo(part.Position)
 character.MoveToFinished:Wait()
end

I got confused and don’t know what to do with this but I added MoveToFinished after :MoveTo(). Now they only follow the last position I was standing on in a straight line.


Hi, @Gildas3410.

Could you do a change to while-loop Into this code ?

Also you might consider doing for-loop in native instead of pairs or ipairs since you used it with not using it for some feature (they waste some perfomance, see it’s bytecode)

-- this is possible, locking into 60 fps rate
while task.wait() do
-- yours code here
end

-- this is also possible
for i,v in tablehere do

end

I changed it

local character = script.Parent
local humanoid = character.Humanoid

local chasing = false

local PathfindingService = game:GetService("PathfindingService")

local ignoreList = {character}


for i, v in pairs(workspace:GetDescendants()) do
	-- Adds other NPCs and parts to be ignored
	if v:IsA("Model") and v ~= character and v:FindFirstChildOfClass("Humanoid") then
		table.insert(ignoreList, v)
		print("Added "..v.Name.." to ignore list")
	elseif v:IsA("Part") and v.Transparency >= 0.3 or v:FindFirstChild("SeeThrough") then
		table.insert(ignoreList, v)
		print("Added "..v.Name.." to ignore list")
	end
	
	-- Removes from ignore list when changed/deleted
	v.Changed:Connect(function()
		if v:IsA("Model") and v ~= character and v:FindFirstChildOfClass("Humanoid") then
			table.remove(ignoreList, tonumber(v))
			print("Removed "..v.Name.." to ignore list")
		elseif v:IsA("Part") and v.Transparency < 0.3 then
			table.remove(ignoreList, tonumber(v))
			print("Removed "..v.Name.." to ignore list")
		end
	end)
	v.Destroying:Connect(function()
		if v:IsA("Model") and v ~= character and v:FindFirstChildOfClass("Humanoid") then
			table.remove(ignoreList, tonumber(v))
			print("Removed "..v.Name.." to ignore list")
		elseif v:IsA("Part") and v.Transparency < 0.3 then
			table.remove(ignoreList, tonumber(v))
			print("Removed "..v.Name.." to ignore list")
		end
	end)
end

-- Main
local function canSeeTarget(target)
	local origin = character.HumanoidRootPart.Position
	local direction = (target.HumanoidRootPart.Position - character.HumanoidRootPart.Position).Unit * 500
	local raycastParams = RaycastParams.new()
	raycastParams.FilterType = Enum.RaycastFilterType.Exclude
	raycastParams.FilterDescendantsInstances = {ignoreList}
	local ray = workspace:Raycast(origin,direction,raycastParams)

	if ray then
		if ray.Instance:IsDescendantOf(target) then
			return true
		end
	else
		return false
	end
end

local function findTarget()
	local maxDistance = 100000
	local nearestTarget = nil
	
	for i, v in game.Players:GetPlayers() do
		if v.Character then
			local target = v.Character
			local distance = (character.HumanoidRootPart.Position - target.HumanoidRootPart.Position).Magnitude
			
			if distance < maxDistance and canSeeTarget(target) and target.Humanoid.Health > 0 then
				nearestTarget = target
				maxDistance = distance
				chasing = true
			else
				chasing = false
			end
		end
	end
	
	return nearestTarget
end

local function getPath(destination)
	local path = PathfindingService:CreatePath(
		{
			["AgentHeight"] = 5,
			["AgentRadius"] = 3,
			["AgentCanJump"] = true
		})
	
	path:ComputeAsync(character.HumanoidRootPart.Position, destination.Position)
	return path
end

local function waypointPath(pos: Vector3) -- Creates the path thingy
	local part = Instance.new("Part")
	part.Name = "Waypoint"
	part.Size = Vector3.new(1,1,1)
	part.Color = Color3.fromRGB(255, 0, 0)
	part.CanCollide = false
	part.CanQuery = false
	part.CanTouch = false
	part.Anchored = true
	part.Position = pos
	part.Parent = workspace
	game.Debris:AddItem(part, 5)
	return part
end

local function walkTo(destination,target)
	local path = getPath(destination)
	local waypoints = path:GetWaypoints()
	
	if path.Status == Enum.PathStatus.Success then
		for index, waypoint in waypoints do
			waypointPath(waypoint.Position)
			humanoid:MoveTo(waypoint.Position)
			humanoid.MoveToFinished:Wait()
		end
	end
end

while task.wait() do
	local target = findTarget()
	if target then
		walkTo(target.HumanoidRootPart,target)
	end
end

But how do I make it so it doesn’t walk to the last time I was standing on? And also how do I make multiple NPCs follow the player spread when they get too close to each other instead of bumping?

You will want to get all nearby NPCs similar to how you did it for the nearest target. Compare all other NPC’s position. Count how much NPCs are too close to the current NPC, if there is too much, then stop the current path, and in the next getPath call, calculate a random vector combination of X and Z like + Vector3.new(math.random(-5, 5), 0, math.random(-5, 5)) so that the point to travel to can be a random point in a plane with the origin at the target’s root. Then call path:ComputeAsync to that new point instead of directly at the target’s root.