Help Needed to Improve Pathfinding Module

Hi, recently I’ve been working on a Roblox Pathfinding NPC Module and I’ve been wondering if there are any other ways to improve it.

Now, I’m not a huge fan of pathfinding, infact it’s pretty new to me as I’ve only been working on Pathfinding for about 1-2 years. But I’ve slowly gotten better at working with it but I still think there is much more to be done to really make NPCs smarter.

A couple issues I’ve been having with this script is NPCs tend to be dull if they’re not chasing a player directly and fall off the basepart they are standing on, or that NPCs don’t jump when needed to, you get the point.
What I really want here is pretty simple, NPCs that can actively chase players down (if possible) and for NPCs to be able to use the module at the same time (The script only allows one NPC to pathfind at a time for some reason.)

Module:

local PathfindModule = {}

local PlayersService = game:GetService("Players")
local PathfindingService = game:GetService("PathfindingService")
local RunService = game:GetService("RunService")

local debugMode = false
local raycastParams = RaycastParams.new()
raycastParams.FilterType = Enum.RaycastFilterType.Blacklist

function PathfindModule.startup(npcModel : Model)
	if npcModel == nil then warn("No NPC found!", npcModel) return end
	
	local npc = npcModel
	if not npc:IsA("Model") then return end
	
	local humanoid = npc:FindFirstChildOfClass("Humanoid")
	local rootPart = npc:FindFirstChild("HumanoidRootPart")
	
	local lastPathUpdate = 0
	local pathfindingInProgress = false

	if not humanoid or not rootPart then
		warn("NPC is missing needed instances: Humanoid or HumanoidRootPart.", npcModel)
		return
	end

	rootPart:SetNetworkOwner(nil) -- reduces lag

	local function debugPrint(message)
		if debugMode then
			print("[npc Debug]: " .. message, npcModel)
		end
	end

	local function raycastToPlayer(player)
		local playerHRP = player.Character and player.Character:FindFirstChild("HumanoidRootPart")
		if not playerHRP then return false end

		raycastParams.FilterDescendantsInstances = {npc}
		
		local direction = (playerHRP.Position - rootPart.Position).Unit * 45
		local rayResult = workspace:Raycast(rootPart.Position, direction, raycastParams)
		return not rayResult -- clear path if not hit.
	end

	local function moveToPlayer(playerHRP)
		local targetPosition = playerHRP.Position
		humanoid:MoveTo(targetPosition)
	end

	local function usePathfindingToPlayer(playerHRP)
		if pathfindingInProgress then return end
		pathfindingInProgress = true

		local path = PathfindingService:CreatePath({
			AgentRadius = 2,
			AgentHeight = 5,
			AgentCanJump = true,
			AgentJumpHeight = 5,
			AgentCanClimb = true,
			AgentCanSwim = true
		})

		path:ComputeAsync(rootPart.Position, playerHRP.Position)
		if path.Status == Enum.PathStatus.Success then
			local waypoints = path:GetWaypoints()
			for _, waypoint in ipairs(waypoints) do
				humanoid:MoveTo(waypoint.Position)
				if waypoint.Action == Enum.PathWaypointAction.Jump then
					humanoid:ChangeState(Enum.HumanoidStateType.Jumping)
				end
				humanoid.MoveToFinished:Wait()
				if (rootPart.Position - playerHRP.Position).Magnitude <= 5 then
					break
				end
			end
		else
			debugPrint("Pathfinding failed.")
		end

		pathfindingInProgress = false
	end

	local function checkPlayerInPath(playerHRP)
		local direction = (playerHRP.Position - rootPart.Position).Unit * 5
		raycastParams.FilterDescendantsInstances = {npc}
		local rayResult = workspace:Raycast(rootPart.Position, direction, raycastParams)
		return rayResult ~= nil
	end

	local function trackPlayer(player)
		local playerHRP = player.Character and player.Character:FindFirstChild("HumanoidRootPart")
		if not playerHRP then return end

		local isPathfinding = humanoid:FindFirstChild("IsPathfinding")
		local canPathfind = not isPathfinding or isPathfinding.Value -- default true if the bool doesn't exist

		if canPathfind then
			if checkPlayerInPath(playerHRP) then
				usePathfindingToPlayer(playerHRP)
			else
				moveToPlayer(playerHRP)
			end
		else
			debugPrint("pathfinding disabled for this npc.")
		end
	end

	local function getNearestPlr()
		local players = PlayersService:GetPlayers()
		local nearestPlayer = nil
		local minDistance = math.huge

		for _, player in ipairs(players) do
			local playerHRP = player.Character and player.Character:FindFirstChild("HumanoidRootPart")
			if playerHRP then
				local distance = (rootPart.Position - playerHRP.Position).Magnitude
				if distance < minDistance then
					nearestPlayer = player
					minDistance = distance
				end
			end
		end

		if nearestPlayer then
			getNearestPlr(nearestPlayer)
		end
	end
	
	RunService.Stepped:Connect(function(_, deltaTime)
		if tick() - lastPathUpdate >= 0.5 then
			lastPathUpdate = tick()
			getNearestPlr()
		end
	end)
end

return PathfindingService

A couple of ideas to consider,

You can cache paths and objects in a table / dictionary for each NPC. This reduces the amount of method calls and re-referencing an existing part. So for example, if you create a new NPC, you can initialize its’ data with FindFirstChild() or more preferrably WaitForChild(), then you won’t have to re-call the same method again unless you add a new check for nil.

In your handler:

local newNPC = PathfindingModule.new(myModel, {
    -- custom agent parameters
})

In your pathfinding module:

local defaultParams = {
    AgentRadius = 2,
    AgentHeight = 5,
    AgentCanJump = true,
    -- and so forth
}

function PathfindingModule.new(model: Model, agentParams)
    local self = setmetatable({}, activeNPCs)
    self.humanoid = model:WaitForChild("Humanoid")
    self.agentParams = agentParams or defaultParams
    -- and so forth
    return self
end

This is merely an example of how my team works with objects through an Object Orientated Programming structure, and I’m sure that there are different ways to implement NPC data for pathfinding, but it’s a common way that can give you an idea of what you can accomplish by caching certain data.

There are other points in your current module that can be improved upon, such as the range in which you allow your NPC to pathfind or the amount of iterations that your pathfinding performs per process (specifically the for-loop for players in the range). You can simply reduce these based on the context of your game: what you can / can’t afford to cut. For example, if a player is beyond the maximum range of your path, it’s possible to remove that iteration.

Hope this helps you.

1 Like