Pathfinding isn't jumping over a gap even with pathfinding links

it gets to the gap, falls in it, then tries jumping on the second half of the pathfinding link instead of the first

External Media

also i dont know why its doing a bunch of zig zags

these are my pathfinding params

AgentRadius = 2,
AgentHeight = 5,
AgentCanJump = true,
Costs = {
	GapJump = 1
}

i check if its a jump waypoint via

local isJumpWaypoint = node.action == Enum.PathWaypointAction.Jump or node.label == "GapJump"
1 Like

no it works for labels too, also AgentJumpHeight and MaxSlope dont exist
PathfindingService | Documentation - Roblox Creator Hub

without adding the jump label to costs, the path just isnt calculated at all says theres no path

Root cause: off-mesh links put the Jump waypoint on the landing side. If you only jump when you reach that waypoint, the NPC walks off the ledge, falls, then tries to jump. Also your path zig-zags because waypoints are too dense.

Jump before moving to a link waypoint.

local function follow(humanoid, waypoints)
	for i, wp in ipairs(waypoints) do
		-- Link/jump is marked on the NEXT position you’re moving to
		if wp.Action == Enum.PathWaypointAction.Jump or wp.Label == "GapJump" then
			humanoid.Jump = true -- set before MoveTo
		end
		humanoid:MoveTo(wp.Position)
		humanoid.MoveToFinished:Wait()
	end
end

If you built your own node table, ensure you read Action/Label from Roblox PathWaypoints. The API uses .Action and .Label (capitalized)

Configure the path to reduce zig-zags.

local path = PathfindingService:CreatePath({
	AgentRadius = 2,
	AgentHeight = 5,
	AgentCanJump = true,
	WaypointSpacing = 8,          -- fewer turns
	Costs = { GapJump = 10 }      -- discourage overusing links
})

I’m deleting that … you know way more about this than me.

now its not even generating the path at all

i dont use a humanoid, its a custom character that i move. the waypoints are colored red when they are a jump action or labeled “GapJump”, there is no red waypoint before the gap so the actor never jumps before the gap

below is the movement logic

local isJumpWaypoint = node.action == Enum.PathWaypointAction.Jump or node.label == "GapJump"

if distXZ < 0.5 then
	if isJumpWaypoint then
		actor.isAirborne = true
		actor.vy = actor.jumpVelocity
		actor.pos = Vector3.new(actor.pos.x, actor.pos.Y + 0.01, actor.pos.z)
		
		actor.currentWaypoint += 1
		if actor.currentWaypoint > #actor.path then
			actor.path = nil
			actor.currentWaypoint = nil
			break
		end
	else
		actor.currentWaypoint += 1
		if actor.currentWaypoint > #actor.path then
			actor.path = nil
			actor.currentWaypoint = nil
			break
		end
	end
else
	local moveDir = dirXZ.Unit
	local moveStep = math.min(remainingMove, distXZ)
	local newPosXZ = actor.pos + moveDir * moveStep

	local newPos = Vector3.new(newPosXZ.X, getGroundY(newPosXZ), newPosXZ.Z)
	local yaw = math.atan2(-moveDir.x, -moveDir.z)

	actor.pos = newPos
	actor.rotation += (yaw - actor.rotation) * 0.35
	actor.positionChanged = true

	remainingMove -= moveStep
end

Two problems.

  1. You jump too late. Off-mesh link waypoint is on the landing side, so your “jump when I reach the red node” logic walks off the ledge. Look ahead and jump before advancing.
  2. You’re using lowercase vector fields. Vector3 uses .X .Y .Z, not .x .y .z. That line will break math.
local path = actor.path
local i = actor.currentWaypoint
local node = path[i]
local nextNode = path[i+1]

local function isJumpWP(wp)
	return wp and (wp.action == Enum.PathWaypointAction.Jump or wp.label == "GapJump")
end

local distXZ = (Vector2.new(node.pos.X, node.pos.Z) - Vector2.new(actor.pos.X, actor.pos.Z)).Magnitude

if isJumpWP(nextNode) and distXZ <= 2.0 and not actor.isAirborne then
	actor.isAirborne = true
	actor.vy = actor.jumpVelocity
	actor.pos = Vector3.new(actor.pos.X, actor.pos.Y + 0.01, actor.pos.Z)
	actor.currentWaypoint += 1
	return
end

if distXZ < 0.5 then
	actor.currentWaypoint += 1
	if actor.currentWaypoint > #path then actor.path = nil; actor.currentWaypoint = nil end
	return
end

local dirXZ = (node.pos - actor.pos); dirXZ = Vector3.new(dirXZ.X, 0, dirXZ.Z)
local moveDir = dirXZ.Magnitude > 0 and dirXZ.Unit or Vector3.zero
local step = math.min(remainingMove, dirXZ.Magnitude)
local newPosXZ = actor.pos + moveDir * step
actor.pos = Vector3.new(newPosXZ.X, getGroundY(newPosXZ), newPosXZ.Z)
local yaw = math.atan2(-moveDir.X, -moveDir.Z)
actor.rotation += (yaw - actor.rotation) * 0.35
remainingMove -= step

Vector3.X and Vector3.x both give you the X coord of the vector. i did that but the path isnt being generated at all now

Likely NoPath. Confirm first, then fix the link endpoints.

Verify

local PFS = game:GetService("PathfindingService")
local path = PFS:CreatePath{
	AgentRadius = 2, AgentHeight = 5, AgentCanJump = true,
	WaypointSpacing = 8, Costs = { GapJump = 1 }
}
path:ComputeAsync(startPos, goalPos)
print("Status:", path.Status)
for i,wp in ipairs(path:GetWaypoints()) do
	print(i, wp.Action, wp.Label, wp.Position)
end

If Status is not Enum.PathStatus.Success, the agent cannot reach either the start, the goal, or the link endpoints.

Fix the off-mesh link

Most common cause: the link endpoints are off the navmesh.

Place Attachment0 (takeoff) and Attachment1 (landing)
At foot height (≈ floor Y).
At least AgentRadius (2 studs) back from the edge.
On walkable, CanCollide, anchored parts.
PathfindingLink.Enabled = true
IsBidirectional = true if you need both ways.
Label = "GapJump" (exact match).
Platforms must be ≥ 4.5 studs wide (≥ 2*AgentRadius + margin) so a nav band exists.

Quick isolation test:

local link = workspace.GapJump -- your PathfindingLink
local a0, a1 = link.Attachment0.WorldPosition, link.Attachment1.WorldPosition
local test = PFS:CreatePath{AgentRadius=2, AgentHeight=5, AgentCanJump=true}
test:ComputeAsync(a0, a1)
print("Link test:", test.Status) -- if NoPath -> move attachments back/up

Use look-ahead and the correct property names (Action, Label).

local function isJumpWP(wp)
	return wp
	   and (wp.Action == Enum.PathWaypointAction.Jump
	     or (wp.Action == Enum.PathWaypointAction.Custom and wp.Label == "GapJump"))
end

local path = actor.path
local i = actor.currentWaypoint
local cur = path[i]
local nxt = path[i+1]

local dxz = (Vector2.new(cur.Position.X, cur.Position.Z) - Vector2.new(actor.pos.X, actor.pos.Z)).Magnitude

if isJumpWP(nxt) and dxz <= 2 and not actor.isAirborne then
	actor.isAirborne = true
	actor.vy = actor.jumpVelocity
	actor.pos = Vector3.new(actor.pos.X, actor.pos.Y + 0.01, actor.pos.Z)
	actor.currentWaypoint += 1
	return
end

if dxz < 0.5 then
	actor.currentWaypoint += 1
	if actor.currentWaypoint > #path then actor.path = nil; actor.currentWaypoint = nil end
	return
end

local dir = cur.Position - actor.pos
dir = Vector3.new(dir.X, 0, dir.Z)
local step = math.min(remainingMove, dir.Magnitude)
if step > 0 then
	local moveDir = dir.Unit
	local newPosXZ = actor.pos + moveDir * step
	actor.pos = Vector3.new(newPosXZ.X, getGroundY(newPosXZ), newPosXZ.Z)
	local yaw = math.atan2(-moveDir.X, -moveDir.Z)
	actor.rotation += (yaw - actor.rotation) * 0.35
	remainingMove -= step
end

If it still won’t generate

Start/goal must be on walkable surfaces (not in air, not inside parts).
Remove any PathfindingModifier set to Blocked near the link.
Try larger AgentHeight if your actor is tall relative to ledge height.
Reduce clutter by setting WaypointSpacing = 8 to stop zig-zags.

Bottom line: move the link attachments back to be inside the nav band, then pre-jump using Action/Label look-ahead.

did that and this is whats happening

External Media

Cause: your jump sets only vy then immediately skips to the next waypoint. No forward impulse, so the actor drops into the gap.

Fix: give a horizontal velocity computed from the landing point’s distance and the jump’s time-of-flight, and only consume the GapJump node (keep the landing node). Trigger a little before the edge.

-- in your mover
local g = workspace.Gravity

local function doGapJump(actor, nowNode, landNode)
	local p0 = actor.pos
	local p1 = landNode.position

	-- pick your up speed (tune)
	local vy0 = actor.jumpUpSpeed or 55

	-- vertical solve for flight time
	local dy = p1.Y - p0.Y
	local t  = (vy0 + math.sqrt(math.max(0, vy0*vy0 + 2*g*dy))) / g
	if t < 0.05 then t = 0.05 end

	-- horizontal speed needed to reach p1 in time t
	local flat = Vector3.new(p1.X - p0.X, 0, p1.Z - p0.Z)
	local dir  = flat.Magnitude > 0 and flat.Unit or Vector3.new()
	local vh   = flat.Magnitude / t

	actor.vel = dir*vh + Vector3.new(0, vy0, 0)
	actor.isAirborne = true
	actor.landing = p1
end

-- movement loop
local triggerDist = 1.25 -- start jump before the edge
if node.label == "GapJump" and distXZ < triggerDist and not actor.isAirborne then
	local land = actor.path[actor.currentWaypoint + 1] -- node after the link
	if land then
		doGapJump(actor, node, land)
		actor.currentWaypoint += 1 -- consume ONLY the GapJump node
	end
end

-- airborne integration
if actor.isAirborne then
	actor.vel += Vector3.new(0, -g, 0) * dt
	actor.pos += actor.vel * dt
	-- land when close and descending
	if (Vector3.new(actor.pos.X, 0, actor.pos.Z) - Vector3.new(actor.landing.X, 0, actor.landing.Z)).Magnitude < 1.0
		and actor.vel.Y <= 0 then
		actor.isAirborne = false
		actor.currentWaypoint += 1 -- now consume landing node
	end
end

PathfindingLink attachments: start ≈0.5–1 stud before the edge, end ≈0.5–1 stud after the far edge. Label = "GapJump". Enable IsBidirectional if needed.
Keep ground steering off while isAirborne = true.
Tune vy0 and triggerDist until the arc clears the lip.

1 Like

im just gonna post my update function, a lot of these variables dont exist

i allow horizontal movement in the air because after jumping the actor continues moving to the next waypoint. i believe this is how i do it with humanoid based AI, jumping then moving. the jump physics are exactly the same as humanoids

local function updateActor(actor: Actor, dt: number)
	local function updateVertical()
		-- vertical integration
		if actor.isAirborne then
			actor.vy -= GRAVITY * dt
			actor.pos = Vector3.new(actor.pos.X, actor.pos.Y + actor.vy * dt, actor.pos.Z)
			actor.positionChanged = true

			local landHit = raycastDownShort(actor.pos, 3)
			if landHit and actor.vy <= 0 then
				local groundY = landHit.Position.Y
				if actor.pos.Y <= groundY + 0.5 then
					actor.pos = Vector3.new(actor.pos.x, groundY, actor.pos.z)
					actor.vy = 0
					actor.isAirborne = false
				end
			end
		end
	end
	
	updateVertical()
	if actor.path and actor.currentWaypoint ~= nil then
		local remainingMove = actor.moveSpeed * dt

		while actor.path and actor.currentWaypoint and remainingMove > 0 do
			local node = actor.path[actor.currentWaypoint]

			local targetPos = node.pos
			local dirXZ = Vector3.new(targetPos.X - actor.pos.X, 0, targetPos.Z - actor.pos.Z)
			local distXZ = dirXZ.Magnitude

			-- if actor is currently airborne, simulate vertical physics + horizontal movement
			if actor.isAirborne then
				local horizStep = 0
				if distXZ > 0 then
					local moveDir = dirXZ.Unit
					horizStep = math.min(remainingMove, distXZ)
					actor.pos = actor.pos + moveDir * horizStep
				end
				
				updateVertical()

				remainingMove -= horizStep
				if distXZ > 0.001 then
					local moveDir = dirXZ.Unit
					local yaw = math.atan2(-moveDir.x, -moveDir.z)
					actor.rotation += (yaw - actor.rotation) * 0.35
				end

				actor.positionChanged = true
			else
				local nextNode = actor.path[actor.currentWaypoint + 1]
				local isJumpWaypoint = nextNode ~= nil and 
					(nextNode.action == Enum.PathWaypointAction.Jump or nextNode.label == "GapJump")
				
				if distXZ < 0.5 then
					if isJumpWaypoint then
						actor.isAirborne = true
						actor.vy = actor.jumpVelocity
						actor.pos = Vector3.new(actor.pos.x, actor.pos.Y + 0.01, actor.pos.z)
						
						actor.currentWaypoint += 1
						if actor.currentWaypoint > #actor.path then
							actor.path = nil
							actor.currentWaypoint = nil
							break
						end
					else
						actor.currentWaypoint += 1
						if actor.currentWaypoint > #actor.path then
							actor.path = nil
							actor.currentWaypoint = nil
							break
						end
					end
				else
					local moveDir = dirXZ.Unit
					local moveStep = math.min(remainingMove, distXZ)
					local newPosXZ = actor.pos + moveDir * moveStep

					local newPos = Vector3.new(newPosXZ.X, getGroundY(newPosXZ), newPosXZ.Z)
					local yaw = math.atan2(-moveDir.x, -moveDir.z)

					actor.pos = newPos
					actor.rotation += (yaw - actor.rotation) * 0.35
					actor.positionChanged = true

					remainingMove -= moveStep
				end
			end
		end
	end
end


iam trying to figure it out i need time i will keep updating you about it

@bIorbee I was helping on this but I don’t have time to continue. Leaving the refs I used:

Hope this helps. I may revisit later.

i managed to get it to jump it and not phase through the floor

but its still making zig zag movements instead of going straight to the target which makes no sense

External Media

the zig zag is less extreme if i dont put the path on a 45 degree angle but its still there

its not as important, so it works for now

local function updateActor(actor: Actor, dt: number)
	local function updateVertical()
		if not actor.isAirborne then
			actor.isAirborne = raycastDownShort(actor.pos, 3) == nil
		end
		
		-- vertical integration
		if actor.isAirborne then
			actor.vy -= GRAVITY * dt
			actor.pos = Vector3.new(actor.pos.X, actor.pos.Y + actor.vy * dt, actor.pos.Z)
			actor.positionChanged = true

			local landHit = raycastDownShort(actor.pos, 3)
			if landHit and actor.vy <= 0 then
				local groundY = landHit.Position.Y
				if actor.pos.Y <= groundY + 0.5 then
					actor.pos = Vector3.new(actor.pos.x, groundY, actor.pos.z)
					actor.vy = 0
					actor.isAirborne = false
				end
			end
		end
	end
	
	local function toNextWaypoint()
		if actor.path == nil then
			return
		end
		
		actor.currentWaypoint += 1
		local node = actor.path[actor.currentWaypoint]
		if node == nil then
			return
		end
		
		actor.targetPosition = node.pos
		if node.action == Enum.PathWaypointAction.Jump or node.label == "GapJump" then
			actor.isAirborne = true
			actor.vy = actor.jumpVelocity
			actor.pos = Vector3.new(actor.pos.X, actor.pos.Y + 0.01, actor.pos.Z)
		end
	end
	
	updateVertical()
	if actor.targetPosition == nil and actor.path ~= nil and actor.currentWaypoint ~= nil then
		toNextWaypoint()
	end
	if actor.targetPosition == nil then
		return
	end
	
	local remainingMove = actor.moveSpeed * dt
	local targetXZ = Vector3.new(actor.targetPosition.X, 0, actor.targetPosition.Z)
	local posXZ = Vector3.new(actor.pos.X, 0, actor.pos.Z)
	local toTargetXZ = targetXZ - posXZ
	local distXZ = toTargetXZ.Magnitude
	
	if distXZ < 0.5 then
		actor.targetPosition = nil
		return
	end

	local moveDir = toTargetXZ.Unit
	local horizStep = math.min(actor.moveSpeed * dt, distXZ)
	local yaw = math.atan2(-moveDir.X, -moveDir.Z)

	actor.pos += moveDir * horizStep
	actor.rotation += (yaw - actor.rotation) * 0.35
	actor.positionChanged = true
end

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