NPC Pathfinding Error

Hello, I am making an NPC handler for my new up and coming roblox MMORPG called Kingdom of Eldor. However I am encountering a bug, although it does not break anything, it is quite annoying and uses memory unneeded.

This happens because I destroy the NPC whenever I walk outside of the Region3 causing an error since the humanoid is not found.

Here is the script where the errors are found:

-- Services
local PathfindingService = game:GetService("PathfindingService")

-- Variables
local waypointsFolder = workspace:WaitForChild("Waypoints")
local npcsFolder = workspace:WaitForChild("NPCs")

local Behaviour = {}

-- Sees if you need to moveto or pathfind 
local function NeedsMoveTo(npc: Model, target: Vector3): boolean
	local humanoidRootPart = npc:FindFirstChild("HumanoidRootPart")
	if not humanoidRootPart then
		return false
	end

	local raycastParams = RaycastParams.new()
	raycastParams.FilterType = Enum.RaycastFilterType.Exclude
	raycastParams.FilterDescendantsInstances = {npc}

	local raycast = workspace:Raycast(humanoidRootPart.Position, (target - humanoidRootPart.Position).unit * 500, raycastParams)
	
	if raycast.Instance.Position == target then
		return true
	else
		return false
	end
end

-- Returns the closest waypoint set
local function FindClosestWaypointSet(npc: Model): Model?
	local humanoidRootPart = npc:FindFirstChild("HumanoidRootPart")
	if not humanoidRootPart then
		return nil
	end

	local closestWaypointSet = nil
	local closestDistance = math.huge

	for _, waypointSet in pairs(waypointsFolder:GetChildren()) do
		if waypointSet:IsA("Model") and #waypointSet:GetChildren() > 0 and waypointSet.PrimaryPart then
			local waypointPosition = waypointSet.PrimaryPart.Position
			local distance = (humanoidRootPart.Position - waypointPosition).magnitude

			if distance < closestDistance then
				closestDistance = distance
				closestWaypointSet = waypointSet
			end
		end
	end

	return closestWaypointSet
end

-- Walks around waypoints
local function Waypoint(npc: Model)
	local closestWaypointSet = FindClosestWaypointSet(npc)

	if not closestWaypointSet then return end

	local waypoints = closestWaypointSet:GetChildren()
	if #waypoints == 0 then
		warn("No waypoints in closest waypoint set")
		return
	end

	local randomWaypoint = waypoints[math.random(1, #waypoints)]
	if NeedsMoveTo(npc, randomWaypoint.Position) then
		local humanoid = npc:FindFirstChildOfClass("Humanoid")
		if humanoid then
			humanoid:MoveTo(randomWaypoint.Position)
		end
	else
		local path = PathfindingService:CreatePath({
			AgentRadius = 2,
			AgentHeight = 5,
			AgentCanJump = true,
			AgentJumpHeight = 10,
			AgentMaxSlope = 45
		})
		path:ComputeAsync(npc.PrimaryPart.Position, randomWaypoint.Position)
		
		for _, waypoint in pairs(path:GetWaypoints()) do
			if npc then 
				if waypoint.Action == Enum.PathWaypointAction.Jump then
					npc.Humanoid:ChangeState(Enum.HumanoidStateType.Jumping)
				end
				npc:FindFirstChild("Humanoid"):MoveTo(waypoint.Position)
				npc:FindFirstChild("Humanoid").MoveToFinished:Wait()
			end
		end
	end
end

-- Locates the enemies
local function FindEnemy(npc: Model, enemies: {string}, attackDistance: number): Model?
	local closestEnemy = nil
	local closestDistance = attackDistance
	
	for _, enemyName in ipairs(enemies) do
		local enemy = npcsFolder:FindFirstChild(enemyName)
		if not enemy then continue end

		for _, enemyModel in pairs(enemy:GetChildren()) do
			local humanoidRootPart = enemyModel:FindFirstChild("HumanoidRootPart")
			if enemyModel:FindFirstChild("HumanoidRootPart") then
				local distance = (npc.HumanoidRootPart.Position - humanoidRootPart.Position).Magnitude

				if distance <= attackDistance and distance < closestDistance then
					closestDistance = distance
					closestEnemy = enemyModel
				end
			else
				warn("No HumanoidRootPart found in enemy model: " .. enemyName)
			end
		end
	end

	return closestEnemy
end

-- For guards/enemies
local function AttackLoop(npc: Model, npcDictionary: {Enemies: {string}, AttackDistance: number})
	while npc do
		local enemy = FindEnemy(npc, npcDictionary.Enemies, npcDictionary.AttackDistance)

		if enemy then
			if NeedsMoveTo(npc, enemy.HumanoidRootPart.Position) then
				-- Move to enemy
				local humanoid = npc:FindFirstChild("Humanoid")
				if humanoid and enemy:FindFirstChild("HumanoidRootPart") then
					humanoid:MoveTo(enemy.HumanoidRootPart.Position)
					task.wait(0.5)
				else
					break
				end
			else
				-- Pathfinding to enemy
				task.wait(0.5)
				if npc:FindFirstChild("HumanoidRootPart") and enemy:FindFirstChild("HumanoidRootPart") then
					local path = PathfindingService:CreatePath()
					path:ComputeAsync(npc.HumanoidRootPart.Position, enemy.HumanoidRootPart.Position)
					for _, waypoint in pairs(path:GetWaypoints()) do
						if waypoint.Action == Enum.PathWaypointAction.Jump then
							npc.Humanoid:ChangeState(Enum.HumanoidStateType.Jumping)
						end
						
						npc:FindFirstChild("Humanoid"):MoveTo(waypoint.Position)
						npc:FindFirstChild("Humanoid").MoveToFinished:Wait()
					end
				else
					break
				end
			end
		else
			task.wait(0.5)
			if npc then
				Waypoint(npc)
			else
				break
			end
		end
	end
end

-- For civilians
local function RunLoop(npc: Model)
	-- Implement behavior for civilians
end

-- Creates a new behaviour instance
function Behaviour.New(npc: Model?, behaviourType: boolean?, npcDictionary: {Enemies: {string}, AttackDistance: number})
	if not npc then
		warn("No NPC provided")
		return
	end

	if not behaviourType then
		-- Attacks enemies
		task.spawn(AttackLoop, npc, npcDictionary)
	else
		-- Runs from enemies
		task.spawn(RunLoop, npc)
	end
end

return Behaviour

The lines with the errors are:

npc:FindFirstChild("Humanoid"):MoveTo(waypoint.Position)
npc:FindFirstChild("Humanoid").MoveToFinished:Wait()

And

npc:FindFirstChild("Humanoid"):MoveTo(waypoint.Position)
npc:FindFirstChild("Humanoid").MoveToFinished:Wait()

Thankyou for all the help that I may recieve!

1 Like

All help is appreciated! :slight_smile: :pray:

Thank you for at least looking at this post!

If you rewrite the Behaviour module using an object oriented approach, you could create an instance method for Behaviour which ends the running tasks and destroys the npc to ensure memory is not wasted

-- Services
local PathfindingService = game:GetService("PathfindingService")

-- Variables
local waypointsFolder = workspace:WaitForChild("Waypoints")
local npcsFolder = workspace:WaitForChild("NPCs")

local Behaviour = {}
Behaviour.__index = Behaviour

-- Sees if you need to moveto or pathfind 
local function NeedsMoveTo(npc: Model, target: Vector3): boolean
	local humanoidRootPart = npc:FindFirstChild("HumanoidRootPart")
	if not humanoidRootPart then
		return false
	end

	local raycastParams = RaycastParams.new()
	raycastParams.FilterType = Enum.RaycastFilterType.Exclude
	raycastParams.FilterDescendantsInstances = {npc}

	local raycast = workspace:Raycast(humanoidRootPart.Position, (target - humanoidRootPart.Position).unit * 500, raycastParams)

	if raycast.Instance.Position == target then
		return true
	else
		return false
	end
end

-- Returns the closest waypoint set
local function FindClosestWaypointSet(npc: Model): Model?
	local humanoidRootPart = npc:FindFirstChild("HumanoidRootPart")
	if not humanoidRootPart then
		return nil
	end

	local closestWaypointSet = nil
	local closestDistance = math.huge

	for _, waypointSet in pairs(waypointsFolder:GetChildren()) do
		if waypointSet:IsA("Model") and #waypointSet:GetChildren() > 0 and waypointSet.PrimaryPart then
			local waypointPosition = waypointSet.PrimaryPart.Position
			local distance = (humanoidRootPart.Position - waypointPosition).magnitude

			if distance < closestDistance then
				closestDistance = distance
				closestWaypointSet = waypointSet
			end
		end
	end

	return closestWaypointSet
end

-- Walks around waypoints
local function Waypoint(npc: Model)
	local closestWaypointSet = FindClosestWaypointSet(npc)

	if not closestWaypointSet then return end

	local waypoints = closestWaypointSet:GetChildren()
	if #waypoints == 0 then
		warn("No waypoints in closest waypoint set")
		return
	end

	local randomWaypoint = waypoints[math.random(1, #waypoints)]
	if NeedsMoveTo(npc, randomWaypoint.Position) then
		local humanoid = npc:FindFirstChildOfClass("Humanoid")
		if humanoid then
			humanoid:MoveTo(randomWaypoint.Position)
		end
	else
		local path = PathfindingService:CreatePath({
			AgentRadius = 2,
			AgentHeight = 5,
			AgentCanJump = true,
			AgentJumpHeight = 10,
			AgentMaxSlope = 45
		})
		path:ComputeAsync(npc.PrimaryPart.Position, randomWaypoint.Position)

		for _, waypoint in pairs(path:GetWaypoints()) do
			if npc then 
				if waypoint.Action == Enum.PathWaypointAction.Jump then
					npc.Humanoid:ChangeState(Enum.HumanoidStateType.Jumping)
				end
				npc:FindFirstChild("Humanoid"):MoveTo(waypoint.Position)
				npc:FindFirstChild("Humanoid").MoveToFinished:Wait()
			end
		end
	end
end

-- Locates the enemies
local function FindEnemy(npc: Model, enemies: {string}, attackDistance: number): Model?
	local closestEnemy = nil
	local closestDistance = attackDistance

	for _, enemyName in ipairs(enemies) do
		local enemy = npcsFolder:FindFirstChild(enemyName)
		if not enemy then continue end

		for _, enemyModel in pairs(enemy:GetChildren()) do
			local humanoidRootPart = enemyModel:FindFirstChild("HumanoidRootPart")
			if enemyModel:FindFirstChild("HumanoidRootPart") then
				local distance = (npc.HumanoidRootPart.Position - humanoidRootPart.Position).Magnitude

				if distance <= attackDistance and distance < closestDistance then
					closestDistance = distance
					closestEnemy = enemyModel
				end
			else
				warn("No HumanoidRootPart found in enemy model: " .. enemyName)
			end
		end
	end

	return closestEnemy
end

-- For guards/enemies
local function AttackLoop(npc: Model, npcDictionary: {Enemies: {string}, AttackDistance: number})
	while npc do
		local enemy = FindEnemy(npc, npcDictionary.Enemies, npcDictionary.AttackDistance)

		if enemy then
			if NeedsMoveTo(npc, enemy.HumanoidRootPart.Position) then
				-- Move to enemy
				local humanoid = npc:FindFirstChild("Humanoid")
				if humanoid and enemy:FindFirstChild("HumanoidRootPart") then
					humanoid:MoveTo(enemy.HumanoidRootPart.Position)
					task.wait(0.5)
				else
					break
				end
			else
				-- Pathfinding to enemy
				task.wait(0.5)
				if npc:FindFirstChild("HumanoidRootPart") and enemy:FindFirstChild("HumanoidRootPart") then
					local path = PathfindingService:CreatePath()
					path:ComputeAsync(npc.HumanoidRootPart.Position, enemy.HumanoidRootPart.Position)
					for _, waypoint in pairs(path:GetWaypoints()) do
						if waypoint.Action == Enum.PathWaypointAction.Jump then
							npc.Humanoid:ChangeState(Enum.HumanoidStateType.Jumping)
						end

						npc:FindFirstChild("Humanoid"):MoveTo(waypoint.Position)
						npc:FindFirstChild("Humanoid").MoveToFinished:Wait()
					end
				else
					break
				end
			end
		else
			task.wait(0.5)
			if npc then
				Waypoint(npc)
			else
				break
			end
		end
	end
end

-- For civilians
local function RunLoop(npc: Model)
	-- Implement behavior for civilians
end

-- Creates a new behaviour instance
function Behaviour.New(npc: Model?, behaviourType: boolean?, npcDictionary: {Enemies: {string}, AttackDistance: number})
	local self = setmetatable({}, Behaviour)
	if not npc then
		warn("No NPC provided")
		return
	end
	self.npc = npc
	if not behaviourType then
		-- Attacks enemies
		self.mainTask = task.spawn(AttackLoop, npc, npcDictionary)
	else
		-- Runs from enemies
		self.mainTask = task.spawn(RunLoop, npc)
	end
	
	return self
end

function Behaviour:destroyNPC()
	self.npc:Destroy()
	task.cancel(self.mainTask)
	setmetatable(self, nil)
end

return Behaviour

example usage:

-- create an npc behaviour instance:
local npcBehaviour = Behaviour.new(...)

-- to destroy the npc/behaviour instance:
npcBehaviour:destroyNPC()
4 Likes

Dang, you long have you been programming in Roblox. I would have never guessed that!

1 Like

Quick question, could you example kinda of everything!

While this did help with lag, there are still errors, again they do not cause any problems since the NPCs are already destroyed however it just causes a lot of errors which I would prefer not to have!

can you copy paste the error from output?

1 Like

I am sorry, I just added some waitforchilds and some modifications and fixed it!

also just realised the destroynpc function should destroy the npc after cancelling the thread

function Behaviour:destroyNPC()
	task.cancel(self.mainTask)
    self.npc:Destroy()
	setmetatable(self, nil)
end
1 Like

Yeah, that is exactly what I did, I guess great minds think alike. (Who am I kidding I am not smart)

1 Like

But could you explain how you can up with this?

When you are creating many objects that share similar properties in your game like NPCs you should normally look towards OOP as your first approach for efficient handling. You should look into some OOP tutorials like this one if you want to learn how to set it up:

Roblox OOP tutorial

Wait aren’t you the guy from Badass expriences, and the maker of 2 player obby!

OMG I love those games!

2 Likes

Thanks lol I’ve learnt loads of this stuff while scripting at badass experiences, just keep coding and you will pick up most of this stuff with time

2 Likes

Thank you for those words of inspiration!

You truly are an awesome guy.

:goat:

2 Likes

Before you go, could you explain function including : and the word self.

I have tried tutorial after tutorial and the documentation but it just does not make sense!

1 Like

when you call the class.new() function, you can think of self as being created to reference the new instance of the class that you called .new() on. So when you change something of ‘self’ you are affecting that new instance rather than the actual class.

For example with the Behaviour class, when we do
local newBehaviour = Behaviour.new(…)
It is creating a new instance of the class Behaviour and “self” is created as a reference to that new instance and saving it under the variable newBehaviour. If you did newBehaviour.x = 1, this would be thesame as doing self.x = 1 for that instance. if you were to print Behaviour.x it would return nil, because x has been defined as a variable of the instance newBehaviour, not the class itself.

. is used for class functions, while : is used for instance functions. Instance functions let you access ‘self’, which you cannot access from a class function.

When calling newBehaviour:destroyNPC()
the function will use “self” to refer to that instance of the Behaviour class, and destroy the npc that was defined under self for that instance.
If we instead tried doing
Behaviour:destroyNPC()
it would throw an error, because Behaviour is not an instance of the class, it is the class, and can only use class functions.

Sorry if the explanation is abit unclear, if you have any more questions ill try answer them

1 Like

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