Multiple PathFinding for npcs

As the titles says , how could i create pathfiding for multiple npcs that follows a player and when they move they update their path finding (just like the zombie model from roblox) i don’t how to achieve this i have taken a look at pathfinder but still it doesn’t work properly.
In addition is there a way to minimize the pressure on the server when doing this ?

Im guessing use CollectionService to tag each NPC, and script pathfinding, and connect the two, you get the point.

yeah i thought about this but 2 things came into my mind
1.what should i use to keep them still detecting like task.spawn or whar?
2.even if it works i think this would cause lag to thse server since a lot of calculations are going to be made
so any suggestions about this

I don’t know what you mean. So they always walk towards the player and never stop?

Depending on the amount of NPCs, if theres less than 10, you might get away with it without additional conditions, if more, you would probably have to check if the player is around, so it doesn’t start unless the player is close enough so as to not do to many calculations.

thabks for helping me out

well about the first question i ve asked you if i use collectionservice should i use task.spawn function with it?

If you need it to run on separate threads then go ahead, but I don’t think its necessary, if you just use :GetTagged() with a for loop, you should be fine. But for the checking part, I guess you’ll have to use it.

Im not sure, just test things out, you know better than I do.

yeah i run into an unexpected behavior
well you see i ve tried to put 2 scripts in 2 models and i ve configured the path and everything but i want to test what would happen if i use a while loop without task.spawn() and guess what both of them started chasing .
well this is what i want but no what i was expecting since only one script can run at a time

You should only use a single Script in ServerScriptService to control all of them at once. Essentially using a while loop with iterations, creating a new thread for each character.

local NPC_Folder = workspace:WaitForChild("NPC", 300) -- Folder with all NPCS

local function HandleNPC(NPC_Character)
    --Pathfinding
    --MoveTo
    --Attack if player in range?
end

while task.wait(0.1) do
    for _, NPC_Character in NPC_Folder:GetChildren() do
        task.spawn(HandleNPC, NPC_Character)
    end
end

while loop in a server script is the best solution i think

here’s my script that handles pathfinding logic (Except for pathfinding itself, simple path handles it)
i’m leaking it fully because i’m gonna rewrite it anyway

local sScriptService = game:GetService("ServerScriptService")
local dService = game:GetService("Debris")
local players = game:GetService("Players")

-- Path to module;
local enemyScriptsFolder = sScriptService:WaitForChild("Enemies")

-- Module;
local simplePath = require(enemyScriptsFolder.SimplePath)
local enemyStats = require(enemyScriptsFolder.EnemyStats)

-- Folder of existing enemies (on the map);
local enemiesFolder = workspace:WaitForChild("Enemies")

-- Functions;
function GetEnemyBehaviorType(enemy: Model): number
	return enemyStats[enemy.Name]["behavior"]
end

function GetEnemyCriticalHealthAmount(enemy: Model): number
	return enemyStats[enemy.Name]["criticalHealth"]
end

function GetSpeedsOfEnemy(enemy: Model): number
	return enemyStats[enemy.Name]["chaseSpeed"], enemyStats[enemy.Name]["wanderSpeed"], enemyStats[enemy.Name]["crippledSpeed"]
end

function CreatePart(parentToAssignTo: Instance)
	local part = Instance.new("Part")
	
	part.Anchored = true
	part.CanCollide = false
	part.Transparency = 1
	part.Parent = parentToAssignTo
	
	return part
end

function ClearAllPathfindParts(enemy: Model)
	local enemyRootPart: BasePart = enemy:FindFirstChild("HumanoidRootPart")
	
	for _, instance in enemyRootPart:GetChildren() do
		if not instance:IsA("BasePart") then continue end
		
		instance:Destroy()
	end
end

function GetClosestPlayer(enemy: Model, limb: BasePart, closestPlayer: any): Player
	local character: Model = limb.Parent
	local humanoid: Humanoid = character:FindFirstChildOfClass("Humanoid")
	
	if not character or not humanoid then return end
	
	local targetPlayer = players:GetPlayerFromCharacter(character)
	
	if not targetPlayer then return end
	
	local enemyRootPart: BasePart = enemy:FindFirstChild("HumanoidRootPart")
	local playerRootPart: BasePart = character:FindFirstChild("HumanoidRootPart")
	
	-- long if statement
	-- checks for closest player
	if closestPlayer == nil or (enemyRootPart.Position - playerRootPart.Position).Magnitude < (enemyRootPart.Position - closestPlayer.Character.HumanoidRootPart.Position).Magnitude then
		closestPlayer = targetPlayer
		
		return closestPlayer
	end
	
	return nil
end

function GenerateRandomWander(enemy: Model)
	-- using the detection part's size for wandering
	local detectionPart: BasePart = enemy:FindFirstChild("DetectionPart")
	local enemyRootPart: BasePart = enemy:FindFirstChild("HumanoidRootPart")
	
	-- assign the position and size of the part
	local partPosition = detectionPart.Position
	local partSize = detectionPart.Size
	
	-- fixed Y position
	local fixedY = enemyRootPart.Position.Y
	
	-- generate a random position
	local randomPosition = Vector3.new(
		math.random() * partSize.X - partSize.X / 2,
		fixedY,
		math.random() * partSize.Z - partSize.Z / 2
	)
	
	-- store it in a variable
	local generatedPosition = partPosition + randomPosition
	
	-- create a part on the position
	local wanderPart = CreatePart(enemyRootPart)
	
	wanderPart.Position = generatedPosition
	
	return wanderPart
end

function GetFleeingDirection(enemy: Model, playerCharacter: Model)
	local playerRootPart: BasePart = playerCharacter:FindFirstChild("HumanoidRootPart")
	local enemyRootPart: BasePart = enemy:FindFirstChild("HumanoidRootPart")
	
	-- get the distance AND direction to the player's rootpart
	local distanceToPlayer = (enemyRootPart.Position - playerRootPart.Position).Magnitude
	local directionToPlayer = (enemyRootPart.Position - playerRootPart.Position).Unit
	
	-- flip the direction
	local fleeDirection = directionToPlayer * -1
	
	-- create a part to put on the end of the distance
	local fleePart = CreatePart(enemyRootPart)
	
	-- put the part at the end of the distance line (flipped)
	fleePart.Position = enemyRootPart.Position + fleeDirection * distanceToPlayer
	
	-- return the part
	return fleePart
end

function ActBasedOnEnemyBehavior(enemy: Model, playerPathfindPart: BasePart, path: any)
	local enemyBehavior = GetEnemyBehaviorType(enemy)
	local enemyHumanoid: Humanoid = enemy:FindFirstChild("Humanoid")
	local playerCharacter: Model = playerPathfindPart.Parent.Parent
	
	-- get the speeds of the enemy
	local chaseSpeed: number, wanderSpeed: number, crippledSpeed: number = GetSpeedsOfEnemy(enemy)
	
	-- first, passive-aggressive (attacks unless low health)
	if enemyBehavior == 0 then
		local enemyCriticalHealth = GetEnemyCriticalHealthAmount(enemy)
		
		-- if the enemy is not on critical health
		if enemyHumanoid.Health > enemyCriticalHealth then
			-- chase!
			path:Run(playerPathfindPart)
			
			enemyHumanoid.WalkSpeed = chaseSpeed
			
			enemy:SetAttribute("Chasing", true)
			enemy:SetAttribute("Fleeing", false)
			enemy:SetAttribute("Wandering", false)
			
			-- otherwise, flee
		else
			-- get the fleeing direction
			local fleePart = GetFleeingDirection(enemy, playerCharacter)
			
			dService:AddItem(fleePart, 8)
			
			-- flee!
			path:Run(fleePart)
			
			enemyHumanoid.WalkSpeed = crippledSpeed
			
			enemy:SetAttribute("Fleeing", true)
			enemy:SetAttribute("Chasing", false)
			enemy:SetAttribute("Wandering", false)
		end
		
		-- then, aggressive (attacks regardless)
	elseif enemyBehavior == 1 then
		path:Run(playerPathfindPart)

		enemyHumanoid.WalkSpeed = chaseSpeed

		enemy:SetAttribute("Chasing", true)
		enemy:SetAttribute("Fleeing", false)
		enemy:SetAttribute("Wandering", false)
		
		-- TODO; support for defense behavior
	end
end

function HandleWandering(enemy: Model, path: any)
	-- check if the enemy can even wander
	local isWandering: boolean = enemy:GetAttribute("Wandering")
	local wanderOnCooldown: boolean = enemy:GetAttribute("WanderCooldown")
	local isChasing: boolean = enemy:GetAttribute("Chasing")
	
	local enemyHumanoid: Humanoid = enemy:FindFirstChild("Humanoid")
	local enemyRootPart: BasePart = enemy:FindFirstChild("HumanoidRootPart")
	
	enemy:SetAttribute("Chasing", false)
	
	-- get the speeds of the enemy
	local chaseSpeed, wanderSpeed, crippledSpeed = GetSpeedsOfEnemy(enemy)
	
	-- if the enemy can wander
	if not isWandering and not wanderOnCooldown and not isChasing then
		-- set the wandering attribute to true\
		-- and also the cooldown!
		enemy:SetAttribute("Wandering", true)
		enemy:SetAttribute("WanderCooldown", true)
		
		-- get the wander part (generated on a random position in range)
		local wanderPart = GenerateRandomWander(enemy)
		
		-- use it to.. wander!
		path:Run(wanderPart)
		
		enemyHumanoid.WalkSpeed = wanderSpeed
		
		path.Reached:Connect(function()
			enemy:SetAttribute("Wandering", false)
			-- wait 3-6 seconds
			local randomInterval = math.random(3, 6)
			task.wait(randomInterval)
			enemy:SetAttribute("WanderCooldown", false)
		end)
		
		-- if the enemy cannot wander, just make it stop
	else
		if enemy:GetAttribute("Wandering") then return end
		
		enemyHumanoid:MoveTo(enemyRootPart.Position)
			
		-- and also clear the pathfind parts from it
		ClearAllPathfindParts(enemy)
	end
end

function ActBasedOnClosestPlayerStatus(closestPlayer: Player, enemy: Model)
	-- if the enemy does find a closest player
	if closestPlayer then
		local playerCharacter: Model = closestPlayer.Character
		local playerHumanoid: Humanoid = playerCharacter:FindFirstChild("Humanoid")
		local playerTorso: BasePart = playerCharacter:FindFirstChild("Torso")
		local playerPathfindpart: BasePart = playerTorso:FindFirstChild("PathfindPart")
		
		-- incase one of those does not exist
		if not playerCharacter or not playerTorso or not playerPathfindpart then return end
		if playerHumanoid:GetState() == Enum.HumanoidStateType.Dead then
			
			return
		end
		
		-- assign the target position
		local targetPosition = playerPathfindpart.Position
		
		-- create a path via SimplePath
		-- visualize it if needed
		local path = simplePath.new(enemy)
		path.Visualize = true
		
		ActBasedOnEnemyBehavior(enemy, playerPathfindpart, path)
		
		-- if the enemy doesn't find a player
	else
		-- create a path via SimplePath
		-- visualize it if needed
		local path = simplePath.new(enemy)
		path.Visualize = true
		
		HandleWandering(enemy, path)
	end
end

function HandlePathfindInitiating()
	local enemies = enemiesFolder:GetChildren()

	if #enemies == 0 then return end

	for _, enemy in ipairs(enemies) do
		local detectionPart: BasePart = enemy:FindFirstChild("DetectionPart")
		local humanoid: Humanoid = enemy:FindFirstChildOfClass("Humanoid")
		local rootPart: BasePart = enemy:FindFirstChild("HumanoidRootPart")

		local closestPlayer = nil
		local playersInBox = workspace:GetPartBoundsInBox(detectionPart.CFrame, detectionPart.Size)

		for _, limb in playersInBox do
			-- yes i know, doing it twcie but to prevent overlaod on the server
			if not players:GetPlayerFromCharacter(limb.Parent) then continue end

			closestPlayer = GetClosestPlayer(enemy, limb, closestPlayer)
		end
		
		task.spawn(ActBasedOnClosestPlayerStatus, closestPlayer, enemy)
	end
end

-- Runtime;
while task.wait(0.05) do
	HandlePathfindInitiating()
end
1 Like

thank you but i already know that

what i ve done was just an experminent and that result was confusing for me can u explain why?