Pathfinding: NPC does not know how to jump/fall

For some reason, the NPC is able to walk only on land. It cannot seem to jump onto or off anything. The pathfinding works and generates a reasonable path, but for some reason it just does not move to the waypoints. (Each set of waypoints has a different colour)

I’m sure I set AgentCanJump to true for this NPC. In fact, sometimes the NPC is unable to follow paths even if it does not consist of jumping – it can fail to follow paths if an obstacle is between two waypoints (which should not happen in the first place?)

Full Code

(This is very long, apologies; there’s a simplified algorithm below)

local Players = game:GetService("Players")
local TweenService = game:GetService("TweenService")
local PathfindingService = game:GetService("PathfindingService")
local SoundLibrary = require(game.ReplicatedStorage.WeaponSoundService)
local Enumerations = require(game.ReplicatedStorage.Enumerations)

local guard = script.Parent.Parent
local animations = require(script.Parent.Parent.Animations.AnimationHandler)
local firearm_data = require(guard.Firearm.WeaponData)

local isSuspicious = false
local isFiring = false
local firingCoro: coroutine
local facingCoro: coroutine
local suspicion = guard.Behaviour.Suspicion  -- decimal, percentage: 0 is nothing, 1 is full
local lastKnownPos: Vector3 = nil

local MAX_RANGE = 40
local PHYSICAL_RANGE = 5
local GUARD_FOV = 70

local state = Enumerations.GuardState.PEACEFUL

local path = Enumerations.getGuardPath(guard.Behaviour.GuardType.Value)
local waypoints
local nextWaypointIndex = 2
local reachedConnection
local blockedConnection

local function followPath(character, path, destination: Vector3)	
	local success, errorMessage = pcall(function()
		path:ComputeAsync(character.HumanoidRootPart.Position, destination)
	end)

	if success and path.Status == Enum.PathStatus.Success then
		waypoints = path:GetWaypoints()

		-- Detect if path becomes blocked
		blockedConnection = path.Blocked:Connect(function(blockedWaypointIndex)
			if blockedWaypointIndex >= nextWaypointIndex then
				blockedConnection:Disconnect()
				followPath(character, path, character.HumanoidRootPart.Position)
			end
		end)

		local humanoid = character:FindFirstChild("Humanoid")
		-- Detect when movement to next waypoint is complete
		if not reachedConnection then
			reachedConnection = humanoid.MoveToFinished:Connect(function(reached)
				if reached and nextWaypointIndex < #waypoints then
					-- Increase waypoint index and move to next waypoint
					nextWaypointIndex += 1
					humanoid:MoveTo(waypoints[nextWaypointIndex].Position)
				else
					reachedConnection:Disconnect()
					blockedConnection:Disconnect()
				end
			end)
		end

		-- Initially move to second waypoint (first waypoint is path start; skip it)
		nextWaypointIndex = 2
		humanoid:MoveTo(waypoints[nextWaypointIndex].Position)
		
		local c = Color3.new(math.random(0, 255), math.random(0, 255), math.random(0, 255))
		for _, waypoint in waypoints do
			local part = Instance.new("Part", workspace)
			part.Size = Vector3.new(1, 1, 1)
			part.Anchored = true
			part.CanCollide = false
			part.Transparency = 0
			part.Position = waypoint.Position
			part.Material = Enum.Material.Neon
			part.Color = c
			task.delay(10, function() part:Destroy() end)
		end
	else
		warn("Path not computed!", errorMessage)
	end
end

local function raycastToPlayer(player)
	local distance: Vector3 = player.Character.HumanoidRootPart.CFrame.Position - guard.HumanoidRootPart.CFrame.Position
	local relPos = guard.HumanoidRootPart.CFrame:PointToObjectSpace(player.Character.HumanoidRootPart.CFrame.Position)
	local raycast_origin = guard.Head.CFrame.Position
	local raycast_direction = (player.Character.Head.CFrame.Position - raycast_origin).Unit

	local raycast_params = RaycastParams.new()
	raycast_params.FilterDescendantsInstances = {player.Character, guard, workspace.BulletHoles}
	raycast_params.FilterType = Enum.RaycastFilterType.Exclude
	raycast_params.IgnoreWater = true

	return workspace:Raycast(raycast_origin, raycast_direction * distance.Magnitude, raycast_params), distance
end

local function withinFOV(distance: Vector3)
	return guard.Head.CFrame.LookVector:Dot(distance.Unit) > math.cos(math.rad(GUARD_FOV))
end

local function findAngle(positionA, positionB)
	local cf = CFrame.new(positionA, positionB)
	local targetX, targetY, targetZ = cf:ToEulerAnglesXYZ()
	return CFrame.Angles(targetX, targetY, targetZ)
end

function play_sound(sound: string)
	local sound: Sound = SoundLibrary.get_sound(firearm_data.metadata.identifier, sound)
	sound.Parent = guard.Firearm.MuzzleFlash
	sound.Name = "NPCSound"
	sound.RollOffMinDistance = 5
	sound.RollOffMaxDistance = 60
	sound.Ended:Connect(function() sound:Destroy() end)
	sound:Play()
end

function fire()
	animations.animation_table.fire:Play()
	guard.Firearm.MuzzleFlash.ParticleEmitter:Emit(1)
	guard.Firearm.MuzzleFlash.PointLight.Enabled = true
	play_sound("fire")
	task.delay(0.025, function() guard.Firearm.MuzzleFlash.PointLight.Enabled = false end)
end

-- Periodically check for physical contact
while wait(0.25) and state == Enumerations.GuardState.PEACEFUL do
	for _, player in Players:GetPlayers() do
		local result, distance = raycastToPlayer(player)
		
		if not result then
			-- No intersections / guard can see player
			if distance.Magnitude <= PHYSICAL_RANGE then
				isSuspicious = true
				suspicion.Value = 1
			elseif withinFOV(distance) then
				if distance.Magnitude <= MAX_RANGE then
					isSuspicious = true
					suspicion.Value += 0.5 * math.clamp(1 - distance.Magnitude / MAX_RANGE, 0, 1)
				end
			end
		end
		
		suspicion.Value = math.clamp(suspicion.Value, 0, 1)
		
		if suspicion.Value == 1 then
			lastKnownPos = player.Character.Head.CFrame.Position
			followPath(guard, path, lastKnownPos)
			
			animations.animation_table.aim:Play()
			state = Enumerations.GuardState.SEARCH
			
			task.delay(animations.animation_table.aim.Length, function()
				animations:stop_animations()
				animations.animation_table.aim_hold:Play()
			end)
			break
		end
	end
	
	if not isSuspicious then
		suspicion.Value -= 0.05
	end
	
	isSuspicious = false
end

isSuspicious = true

while wait(0.25) do	
	if state == Enumerations.GuardState.SEARCH then
		local result: RaycastResult
		local distance: Vector3

		for _, player in Players:GetPlayers() do
			result, distance = raycastToPlayer(player)

			if not result and distance.Magnitude <= MAX_RANGE then
				if distance.Magnitude <= PHYSICAL_RANGE then
					lastKnownPos = player.Character.Head.CFrame.Position
					isSuspicious = true
					suspicion.Value = 1
					break
				elseif withinFOV(distance) then
					lastKnownPos = player.Character.Head.CFrame.Position
					isSuspicious = true
					suspicion.Value += math.clamp(1 - distance.Magnitude / MAX_RANGE, 0.35, 1)
					break
				end
			end
		end

		if not isSuspicious then
			suspicion.Value -= 0.05
		end

		suspicion.Value = math.clamp(suspicion.Value, 0, 1)

		if suspicion.Value >= 1 then
			state = Enumerations.GuardState.ATTACK
		end

		isSuspicious = false
		
	elseif state == Enumerations.GuardState.ATTACK then
		local result: RaycastResult
		local distance: Vector3
		
		for _, player in Players:GetPlayers() do
			result, distance = raycastToPlayer(player)
			
			if not result and ((withinFOV(distance) and distance.Magnitude <= MAX_RANGE)
				or distance.Magnitude <= PHYSICAL_RANGE) then
				
				lastKnownPos = player.Character.Head.CFrame.Position
				
				if not isFiring then
					isFiring = true
					firingCoro = task.spawn(function()
						while wait(60 / firearm_data.statistics.rpm) do
							fire()
							guard.Behaviour.TargetPosition.Value = player.Character.Head.CFrame.Position
						end
					end)
					guard.AlignOrientation.Attachment1 = player.Character.Torso.NeckAttachment
					guard.Behaviour.LineOfSight.Value = true
				end
				break
			end
		end
			
		if result or not withinFOV(distance) or distance.Magnitude > MAX_RANGE then
			isFiring = false
			if firingCoro then task.cancel(firingCoro) end
			guard.AlignOrientation.Attachment1 = nil
			guard.Behaviour.LineOfSight.Value = false
			
			if lastKnownPos then
				followPath(guard, path, lastKnownPos)
			end
		end
	end
end

Pathfinding Parameters

...
		return PathfindingService:CreatePath({
			AgentRadius = 1,
			AgentHeight = 6,
			AgentCanJump = true,
			AgentCanClimb = true,
			Costs = {
				Climb = 5
			}
		})

Guard Logic

  • Guard starts in peaceful mode, and once alerted, it switches to search mode
  • If alerted in search mode by a player, they switch to attack mode and start attacking the player
  • If a player is in sight (raycast + FOV checks) the guard attacks
  • If a player is not in sight, the guard goes to the player’s last known position
  • Guard should stop walking, even when in the middle of a path, to attack if they see a player

those params alone won’t make your NPC actually jump; they just tell the solver it’s allowed to plan jumps/climbs. I recommend just turning on hum.autojump its the easiest. For the falling the pathfinding doesn’t plan paths that require jumping down so you will have to probably could just do a hum:move() to force it to move then continue your path finding you can also do a raycast to check if it is too high of a fall and the npc will stay there to be little bit more real.

Roblox does not automatically make the Humanoid jump or step over obstacles. MoveTo alone will fail if the waypoint requires a jump.

Detect a vertical difference between waypoints and trigger humanoid.Jump = true before moving, or Use Humanoid:MoveTo() along with a loop that handles being stuck or unable to reach the next waypoint.

A minimal example..

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

local npc = workspace.NPC
local humanoid = npc:WaitForChild("Humanoid")
local root = npc:WaitForChild("HumanoidRootPart")

local waypoints = {Vector3.new(0,0,0), Vector3.new(10,0,10), Vector3.new(20,5,0)}

for i = 1, #waypoints-1 do
    local path = PathfindingService:CreatePath({
        AgentRadius = 2,
        AgentHeight = 5,
        AgentCanJump = true,
        AgentJumpHeight = 10,
        AgentMaxSlope = 45
    })
    path:ComputeAsync(root.Position, waypoints[i+1])
    local points = path:GetWaypoints()
    for _, point in ipairs(points) do
        if point.Action == Enum.PathWaypointAction.Jump then
            humanoid.Jump = true
        end
        humanoid:MoveTo(point.Position)
        humanoid.MoveToFinished:Wait()
    end
end

This next chunk is a bit advanced so you’ll need to test and adjust your script.

for i, point in ipairs(points) do
    humanoid:MoveTo(point.Position)
    humanoid.MoveToFinished:Wait()
    if i > 1 then
        local heightDiff = point.Position.Y - points[i-1].Position.Y
        if heightDiff > 1 or point.Action == Enum.PathWaypointAction.Jump then
            humanoid.Jump = true
        end
    end
end

You could also send that information with the waypoint with some crafty scripting and placement.
Auto jump can help randomly, but then they jump for no reason at times. Most just set Auto jump and hope for the best, it’s sloppy but it can work out.

1 Like

you can just set Humanoid.AutoJump to true

1 Like

I do have Humanoid.AutoJumpEnabled set to true. I’ve modified my code, and it seems occasionally (around 5%), but it still refuses to jump sometimes.

local function followPath(character, path, destination: Vector3)	
	local success, errorMessage = pcall(function()
		path:ComputeAsync(character.HumanoidRootPart.Position, destination)
	end)

	if success and path.Status == Enum.PathStatus.Success then
		waypoints = path:GetWaypoints()

		-- Detect if path becomes blocked
		blockedConnection = path.Blocked:Connect(function(blockedWaypointIndex)
			if blockedWaypointIndex >= nextWaypointIndex then
				blockedConnection:Disconnect()
				followPath(character, path, character.HumanoidRootPart.Position)
			end
		end)

		local humanoid = character:FindFirstChild("Humanoid")
		-- Detect when movement to next waypoint is complete
		if not reachedConnection then
			reachedConnection = humanoid.MoveToFinished:Connect(function(reached)
				if reached and nextWaypointIndex < #waypoints then
					-- Increase waypoint index and move to next waypoint
					nextWaypointIndex += 1
					
					local heightDiff = waypoints[nextWaypointIndex].Position.Y - character.HumanoidRootPart.Position.Y
					if math.abs(heightDiff) > 0 or waypoints[nextWaypointIndex].Action == Enum.PathWaypointAction.Jump then
						humanoid.Jump = true
					end
					humanoid:MoveTo(waypoints[nextWaypointIndex].Position)
				else
					reachedConnection:Disconnect()
					blockedConnection:Disconnect()
				end
			end)
		end

		-- Initially move to second waypoint (first waypoint is path start; skip it)
		nextWaypointIndex = 2
		humanoid:MoveTo(waypoints[nextWaypointIndex].Position)
		
		local c = Color3.new(math.random(0, 255), math.random(0, 255), math.random(0, 255))
		for _, waypoint in waypoints do
			local part = Instance.new("Part", workspace)
			part.Size = Vector3.new(1, 1, 1)
			part.Anchored = true
			part.CanCollide = false
			part.Transparency = 0
			part.Position = waypoint.Position
			part.Material = Enum.Material.Neon
			part.Color = c
			task.delay(10, function() part:Destroy() end)
		end
	else
		warn("Path not computed!", errorMessage)
	end
end

Even if I take the absolute value of the height difference, the NPC still refuses to jump and the problem still persists.

That stuff is advanced and a bit hard to implement. You are using Pathfinding Parameters. So you could also add a material that they cannot walk on. When placed in key spots it will force the NPC to resort to the jump.

example:

local params = {
    AgentRadius = 2,
    AgentHeight = 5,
    AgentCanJump = true,
    MaterialWeights = {
        [Enum.Material.Concrete] = math.huge
    }
}

I’m aware of using material costs as pathfinding parameters, but this would very likely limit flexibility for world building. Are there community-created modules/libraries that serve as a workaround?

then I would do a raycast infront of the character and check if the heightdifference is low enough to jump on probably raycast at feet though so you don’t get stuck this is like a rough draft of it.

local npc = script.Parent
local humanoid: Humanoid = npc:WaitForChild("Humanoid")
local hrp: BasePart = npc:WaitForChild("HumanoidRootPart")

-- CONFIG
local forwardDistance = 6       -- how far ahead to look
local rayHeight = 3             -- where the ray starts above HRP
local jumpCheckHeight = 6       -- max obstacle height NPC can jump
local moveSpeed = 8             -- walk speed

humanoid.WalkSpeed = moveSpeed

local function isJumpable(obstaclePos: Vector3): boolean
	local baseY = hrp.Position.Y
	local heightDiff = obstaclePos.Y - baseY
	return heightDiff > 0 and heightDiff <= jumpCheckHeight
end

while task.wait(0.1) do
	if not hrp then break end

	-- Cast forward ray from HRP at chest height
	local origin = hrp.Position + Vector3.new(0, rayHeight, 0)
	local direction = hrp.CFrame.LookVector * forwardDistance
	local params = RaycastParams.new()
	params.FilterDescendantsInstances = {npc}
	params.FilterType = Enum.RaycastFilterType.Exclude

	local result = workspace:Raycast(origin, direction, params)

	if result then
		-- We hit something ahead
		if isJumpable(result.Position) then
			humanoid.Jump = true
		else
			-- Too tall → maybe stop or try pathfinding
			humanoid:Move(Vector3.zero)
			continue
		end
	end

	-- Always keep moving forward
	humanoid:Move(hrp.CFrame.LookVector)
end
1 Like