NPC Pathfinding Keeps Breaking When Its Time To Jump Or Climb

I am currently working on a class-based fighting game similar to Brawl Stars where one of the things players can do is spawn NPCs to help join the fight. The NPCs must be capable of traversing the maps which will be vertically complex and large. However when it comes time to jump or climb, it goes well at first and then completely breaks. I have been completely unable to find a fix for the issue, either by myself or on the DevForum.

robloxapp-20230625-1451459.wmv (3.6 MB)
In this video is a demonstration of how it breaks. The red balls represent waypoints reached by walking, and the yellow balls represent waypoints reached by jumping. It appears to form two waypoints bunched up together (its like this no matter how spaced the waypoints are, or no matter how much I edit the agent parameters) and it seems to be attempting to reach one of these bunched up waypoints first before proceeding with the jump or climb.

If the NPC fails a jump, or if I move it to the ground after it reaches the top, then the same thing will happen where it will be unable to jump up because it seems to be trying to reach a second waypoint. This time however, the second waypoint will sometimes be where the jump point should be, and it will then start attempting to walk up the block instead of jumping (which is impossible) as seen below:
robloxapp-20230625-1504008.wmv (5.5 MB)

Once that happens, I cannot get it to stop trying. Even with the path constantly recalculating so it can follow me, it is determined to walk up that block as seen with the red coloured ball. It sees it as a 100% necessary step to reaching me no matter where I am. The only way I can break its determination is to move the block.

Below is the code used by the NPC to find a target and begin pathfinding. Its basically a copy-paste from the official roblox documentation on pathfinding, except its been heavily modified to fit my needs.

---VARIABLES----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

PLAYER_SERVICE = game:GetService("Players")
PATHFINDING_SERVICE = game:GetService("PathfindingService")

NPC = script.Parent
MAX_RANGE = script.Parent.CONFIGURE.MAX_CHASE_RANGE.Value

LOOP_TIME = 0.1 -- How Often Path Is Calculated.

---FUNCTIONS----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

PATH = PATHFINDING_SERVICE:CreatePath({

	AgentRadius = 3,
	AgentHeight = 5,
	AgentCanJump = true,
	AgentCanClimb = true,
	WaypointSpacing = 50,

	Costs = {
		Ice = 10,
		Water = 20,

		Climb = 5,
		DANGER_ZONE = 100,
		DEADLY_ZONE = math.huge
	}	
})

local WAYPOINTS
local NEXT_WAYPOINT_INDEX

local BLOCKED_CONNECTION
local REACHED_CONNECTION

function FOLLOW_PATH(DESTINATION)

	local SUCCESS, ERROR = pcall(function() -- Compute The Path.
		PATH:ComputeAsync(NPC.HumanoidRootPart.Position, DESTINATION.Position)	
	end)

	if SUCCESS and PATH.Status == Enum.PathStatus.Success then
		WAYPOINTS = PATH:GetWaypoints() -- Get The Path's Waypoints.		

		for C,nms in pairs (script.Parent.nms:GetChildren()) do -- TEMPORARY, JUST FOR DEBUGGING!
			nms:Destroy()
		end	

		local old_nm
		for C, WAYPOINT in pairs (WAYPOINTS) do -- TEMPORARY, JUST FOR DEBUGGING!

			local nm = game.Workspace.WP:Clone()

			if WAYPOINT.Action == Enum.PathWaypointAction.Jump then
				nm.BrickColor = BrickColor.new("New Yeller")			
			end

			if old_nm ~= nil then
				nm.Beam.Attachment1 = old_nm.Attachment
			end

			nm.Position = WAYPOINT.Position 
			nm.Parent = script.Parent.nms	

			old_nm = nm
		end

		BLOCKED_CONNECTION = PATH.Blocked:Connect(function(BLOCKED_WAYPOINT_INDEX) -- Detect If Path Becomes Blocked.
			if BLOCKED_WAYPOINT_INDEX >= NEXT_WAYPOINT_INDEX then -- Detect If Blockage Is Futher Down The Path.

				BLOCKED_CONNECTION:Disconnect() -- Stop Detecting Until Path Is Re-Computed.
				FOLLOW_PATH(DESTINATION) -- Re-Compute The Path.
			end		
		end)

		if not REACHED_CONNECTION then -- Detect When Movement To Next Waypoint Is Complete.
			REACHED_CONNECTION = NPC.Humanoid.MoveToFinished:Connect(function(REACHED)		
				if REACHED and NEXT_WAYPOINT_INDEX < #WAYPOINTS then -- Increase Waypoint Index & Move To The Next Waypoint.
					
					NEXT_WAYPOINT_INDEX += 1
					NPC.Humanoid:MoveTo(WAYPOINTS[NEXT_WAYPOINT_INDEX].Position)
					
					if WAYPOINTS[NEXT_WAYPOINT_INDEX].Action == Enum.PathWaypointAction.Jump then -- Jump If Required.
						NPC.Humanoid.Jump = true
					end
				else
					REACHED_CONNECTION:Disconnect()
					BLOCKED_CONNECTION:Disconnect()	
				end	
			end)			
		end

		NEXT_WAYPOINT_INDEX = 2
		NPC.Humanoid:MoveTo(WAYPOINTS[NEXT_WAYPOINT_INDEX].Position)
	end
end

---FUNCTIONALITY------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

wait(0.25) -- Allow NPC To Load In First, Just To Be Safe.

while true do -- Scan For Nearest Humanoid, And Create A Path To Them.
	wait(LOOP_TIME)

	local NEAREST_DISTANCE = math.huge
	local NEAREST_HUMANOID

	for COUNT, TARGET in ipairs(game.Workspace.WORLD_CONFIGURATION.WORLD_DEBRIS.HUMANOIDS:GetChildren()) do
		local TARGET_HUMANOID = TARGET:FindFirstChild("Humanoid")
		local TARGET_HRP = TARGET:FindFirstChild("HumanoidRootPart")

		if TARGET:IsA("Model") and TARGET_HUMANOID and TARGET_HRP and TARGET_HUMANOID.Health > 0 and TARGET ~= script.Parent then
			local PLAYER = PLAYER_SERVICE:GetPlayerFromCharacter(TARGET)

			local DISTANCE = (NPC.Head.Position - TARGET_HRP.Position).magnitude
			if DISTANCE <= MAX_RANGE and DISTANCE < NEAREST_DISTANCE then

				if PLAYER then -- Dont Mark Players/NPCs Of The Same Team.				
					if PLAYER.TeamColor ~= NPC.NPC_TEAM.Value then

						NEAREST_HUMANOID = TARGET
						NEAREST_DISTANCE = DISTANCE
					end
				else

					if TARGET.NPC_TEAM.Value ~= NPC.NPC_TEAM.Value then

						NEAREST_HUMANOID = TARGET
						NEAREST_DISTANCE = DISTANCE
					end
				end
			end
		end		
	end	

	if NEAREST_HUMANOID ~= nil then	-- If Humanoid Is In Range.	
		local PLAYER = PLAYER_SERVICE:GetPlayerFromCharacter(NEAREST_HUMANOID)

		if PLAYER ~= nil then -- Humanoid Is A Player.
			if PLAYER.TeamColor ~= NPC.NPC_TEAM.Value and NPC.Humanoid.Health > 0 and PLAYER.Character.Humanoid.Health > 0 then	

				FOLLOW_PATH(NEAREST_HUMANOID:FindFirstChild("HumanoidRootPart"))
			end	

		elseif PLAYER == nil then -- Humanoid Is An NPC.		
			if NEAREST_HUMANOID.NPC_TEAM.Value ~= NPC.NPC_TEAM.Value and NPC.Humanoid.Health > 0 and NEAREST_HUMANOID.Humanoid.Health > 0 and NEAREST_HUMANOID.STATUS_EFFECTS.INVISIBLE.Value == false then

				FOLLOW_PATH(NEAREST_HUMANOID:FindFirstChild("HumanoidRootPart"))
			end
		end
	end
end

---END----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

Ive been stuck on this problem for two days now, any help would be greatly appreciated!

UPDATE:
I managed to fix the jumping by removing:

else
    REACHED_CONNECTION:Disconnect()
    BLOCKED_CONNECTION:Disconnect()

From the code, however it still struggles to climb and still gets stuck if it misses a jump.

UPDATE 2:
I just added a second script that will set Humanoid.Jump to true if the NPCs WalkToPoint Coordinates haven’t changed in over a second while tracking a target (Which is shown as an ObjectValue). Its not an ideal solution and can be buggy, but it succeeds at getting the NPC unstuck, even if it takes a few tries. It also doesn’t fix the climbing but I can live without NPCs that climb. If anyone else has a better solution im more than willing to hear it out, but for now im just going to say this is solved.

2 Likes

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