AI Pathfinding Cannot Properly Jump To Elevated Platforms

Hello. I’m creating a game that requires Roblox’s pathfinding agent to simply jump to reach elevated areas. On paper, this sounds simple, but the agent is incapable of doing this ingame for some reason.


The video shows the agent trying and failing over and over again trying to jump onto the object. No, it is not because of the slope angle or collision, the agent will do this on every single platform regardless of shape or size. His behavior value is set to “Forward” during the entirely of the video and his target is above him on the elevated area you can see just out of frame.

Pretty much any time the AI is faced with jumping onto an elevated platform, it will do this. For some reason, it does not have trouble jumping over an obstacle, just onto one.

One band-aid fix I’ve tried employing is just removing the :wait() from it’s pathfinding script, which somewhat patched the issue but now they jump repeatedly and it doesn’t look very pretty.

Here’s the entirety of the pathfinding code. Please forgive the messiness.

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

local Dummy = script.Parent
local Humanoid: Humanoid = Dummy:WaitForChild("Humanoid")
local Root: BasePart = Dummy:WaitForChild("HumanoidRootPart")

local MaxDis = Dummy.NPC.MaxDistance
local behavior = Dummy.WalkBehavior
local nodes = game.Workspace.Nodes:GetChildren()


local MaterialCosts = {}

MaterialCosts.AIGoodNode = 0
MaterialCosts.AIBadNode = 50000
MaterialCosts.AIKillNode = math.huge

local Path = PathfindingService:CreatePath({
	AgentHeight = 6,
	AgentRadius = 6,
	AgentCanJump = true,
	AgentCanClimb = false,
	Costs = MaterialCosts
})

local PathWaypoints = {}
local PathCurrent = 0
local PathLength = 0

-- Track progress
local lastPosition = Root.Position
local stuckTime = 0
local stuckThreshold = 5 -- seconds
local minMoveDistance = 6 -- studs

local function recomputePath(targetPos)
	Path:ComputeAsync(Root.Position, targetPos)
	if Path.Status == Enum.PathStatus.Success then
		PathWaypoints = Path:GetWaypoints()
		PathCurrent = 2
		PathLength = #PathWaypoints
	else
		PathWaypoints = {}
		PathCurrent = 0
		PathLength = 0
	end
end


task.spawn(function()
	while task.wait(0.05) do
		if Humanoid.Health <= 0 then return end

		local PlayerCharacter = Dummy.target.Value
		if not (PlayerCharacter and PlayerCharacter:FindFirstChild("HumanoidRootPart") and PlayerCharacter.Humanoid.Health > 0) then
			continue
		end

		local distance = (Root.Position - PlayerCharacter.HumanoidRootPart.Position).Magnitude
		if distance <= MaxDis.Value then
			recomputePath(PlayerCharacter.HumanoidRootPart.Position)
		end
	end
end)


task.spawn(function()
	while task.wait(0.01) do
		if Humanoid.Health <= 0 then return end

		local PlayerCharacter = Dummy.target.Value
		if not (PlayerCharacter and PlayerCharacter:FindFirstChild("HumanoidRootPart") and PlayerCharacter.Humanoid.Health > 0) then
			continue
		end

		local PathWaypointCurrent = PathWaypoints[PathCurrent]
		if not PathWaypointCurrent then continue end

		if behavior.Value == "Forward" then
			Humanoid:MoveTo(PathWaypointCurrent.Position)


			local requiresJump = PathWaypointCurrent.Action == Enum.PathWaypointAction.Jump 
			

			local jumpLoop = true
			if requiresJump then
				task.spawn(function()
					while jumpLoop and Humanoid.Health > 0 do
						Humanoid.Jump = true
						task.wait(0.25) 
					end
				end)
			end


			if (Root.Position - lastPosition).Magnitude > minMoveDistance then
				lastPosition = Root.Position
				stuckTime = 0
			else
				stuckTime += 0.1
			end

			if stuckTime >= stuckThreshold then
				stuckTime = 0
				recomputePath(PlayerCharacter.HumanoidRootPart.Position)
				continue
			end

			Humanoid.MoveToFinished:Wait()
			jumpLoop = false 
			PathCurrent += 1

		elseif behavior.Value == "Backpedal" and Dummy.NPC.Backpedal.Value then
			Retreat(PlayerCharacter)

		elseif behavior.Value == "Still" then
			if Dummy.NPC.Strafe.Value or math.random(1, 15) == 1 then
				Strafe(PlayerCharacter)
			end
		end
	end
end)

function Retreat(Target)


	local result,pos = checkDirection(Dummy,-(Target.HumanoidRootPart.Position - Dummy.HumanoidRootPart.Position).Unit)
	if result then
		Dummy.Humanoid:MoveTo(pos)
		return
	end


	result,pos = checkDirection(Dummy,Dummy.HumanoidRootPart.CFrame.RightVector)
	if result then
		Dummy.Humanoid:MoveTo(pos)
		return
	end


	result,pos = checkDirection(Dummy,-Dummy.HumanoidRootPart.CFrame.RightVector)
	if result then
		Dummy.Humanoid:MoveTo(pos)
		return
	end
end

function Strafe(Target)
	if Target then
	local goalPos = Target.HumanoidRootPart.Position
	local origin,dir = Dummy.HumanoidRootPart.Position,nil

	local action = math.random(2)
	if action == 1 then
		dir = Dummy.HumanoidRootPart.CFrame.RightVector
	elseif action == 2 then
		dir = -Dummy.HumanoidRootPart.CFrame.RightVector
	end

	for _=1,10 do
		if not raycast(origin,dir*2) and raycast(origin,Vector3.new(0,-7,0)) then
			origin += dir*math.random(1.2,2)
		else
				origin += dir*-math.random(1.2,2)
			break
		end
	end

	local _,result = raycast(origin,(Target.HumanoidRootPart.Position - origin).Unit * 30)
	if result then
		if result.Instance:IsDescendantOf(Target.HumanoidRootPart.Parent) then

		else

			origin = Dummy.HumanoidRootPart.Position
		end
		origin = Dummy.HumanoidRootPart.Position
	end

	Dummy.Humanoid:MoveTo(origin)
		Dummy.Humanoid.MoveToFinished:Wait()
	end
end

function checkDirection(npc, dir)
	if npc.target.Value then
	local goalPos = npc.target.Value.HumanoidRootPart.Position
	local origin = Dummy.HumanoidRootPart.Position

	local success = false
	for i=1, 5 do
		if not raycast(origin,dir*1) and raycast(origin,Vector3.new(0,-7,0)) then
			origin = origin + dir*1
		else
			origin = origin + dir*-1
			break
		end

		if i == 5 then
			success = true
		end
	end

	return success,origin
	end
end

function raycast(origin,dir)
	local raycastParams = RaycastParams.new()
	raycastParams.FilterDescendantsInstances = {Dummy, nodes}
	local result = workspace:Raycast(origin,dir,raycastParams)
	if result then
		return true, result
	else
		return false
	end
end

If anyone else has encountered this issue and has a solution, I would appreciate it.

It could be because when it jumps, you are still pathfinding a path and making it walk that new path, and since that new path hasn’t reached the ledge yet, the NPC think’s it hasn’t jumped yet and goes back to retry the jump or something. This can easily be fix by just stopping the Pathfinding for like .25 or wait until the NPC lands. Not sure howd you do it cause the code is kinda all over the place :d

2 Likes