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
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?