Builtin Library
readme.md
pathfinding system
Generic, content-only pathfinding library for the Zero engine. A* over a
caller-provided graph, with volumetric cost zones (PathfindingModifier)
and off-mesh links (PathfindingLink) composed in.
The package is intentionally engine-light: it never reaches into renderer state, never assumes a baked navmesh exists, and never depends on unimplemented primitives. Instead it accepts whatever graph the caller supplies (grid, navmesh polygons, hand-authored waypoint network) and runs A* with modifier/link composition on top.
This addresses two issues that previously had no shared substrate:
- #2714 PathfindingLink — off-mesh link components (ladders, ziplines, jumps, teleporters) with bidirectional, predicate, and label-cost semantics matching Roblox's PathfindingLink.
- #2715 PathfindingModifier — volumetric cost zones (lava, roads, crosswalks, fog) with multiplicative, max, or first-wins composition.
Layout
systems/pathfinding.package/
pathfinding.module/ -- public API (require this)
init.luau -- Pathfinding.path / addLink / addModifier / ...
astar.luau -- generic A* over an adjacency callback
graph.luau -- GridGraph helper (3D grid -> graph adapter)
modifiers.luau -- volumetric modifier registry + cost lookup
links.luau -- off-mesh link registry + adjacency overlay
path.luau -- Path / PathWaypoint constructors
README.md -- design reference
PathfindingLink.component/ -- attach to entities to register a link
PathfindingModifier.component/ -- attach to entities to register a cost zone
demo/
pathfinding_demo.scene/ -- showcase: lava + road + crosswalk + ladder
Quick start
local Pathfinding = require("@builtin::systems.pathfinding.pathfinding")
-- Register a cost zone programmatically (or attach a PathfindingModifier
-- component to any entity for the same effect).
local lavaId = Pathfinding.addModifier({
aabb = { min = {-5, -1, -5}, max = {5, 1, 5} },
label = "Danger",
})
-- Register an off-mesh link (or attach a PathfindingLink component).
local ladderId = Pathfinding.addLink({
from = {0, 0, 0},
to = {0, 4, 0},
label = "ClimbLadder",
cost = 5.0,
isBidirectional = true,
})
-- Build a 2D grid graph (32x32 cells of 1 metre each) from a walkability
-- predicate. The predicate is called once per cell during build.
local grid = Pathfinding.gridGraph({
origin = {0, 0, 0},
size = {32, 1, 32},
voxelSize = 1.0,
walkable = function(x, y, z) return true end,
})
-- Compute a path. The agent's `costs` table is consulted for every
-- modifier whose AABB contains an edge midpoint.
local path = Pathfinding.path({
graph = grid,
start = {0, 0, 0},
goal = {20, 0, 20},
agent = {
radius = 0.4,
canSwim = false,
costs = {
Danger = math.huge,
Road = 0.5,
Crosswalk = 0.01,
ClimbLadder = 1.0,
},
},
})
for i, wp in ipairs(path.waypoints) do
-- wp.position = { x, y, z }
-- wp.action = "Walk" | "Custom"
-- wp.label = link.label (when action = "Custom")
print(i, wp.action, wp.label or "-", wp.position.x, wp.position.y, wp.position.z)
end
Conflict resolution
When multiple modifiers overlap, costs combine per
Pathfinding.modifierConflict:
"multiply"(default) —cost = base * c1 * c2 * .... Composable; a road overlaid with a crosswalk amplifies the road's preference."max"—cost = base * max(c1, c2, ...). Conservative; the worst zone wins. Useful for danger-overlaps-road semantics."first"—cost = base * c_first. Legacy mode; whichever modifier was registered first wins.
math.huge from any modifier blocks the edge regardless of mode.
Predicate links (Zero leapfrog)
Pathfinding.addLink({
from = {0, 0, 0}, to = {10, 0, 0},
label = "SwimAcross",
condition = function(agent)
return agent.canSwim and not _G.world.isStormy
end,
})
The predicate runs once per Pathfinding.path call when the link's
cost is queried, so dynamic gating doesn't require manual enabled
toggling.
Documentation
pathfinding.module/README.md— algorithm + module-by-module referencePathfindingLink.component/— component schemaPathfindingModifier.component/— component schemademo/pathfinding_demo.scene/— runnable showcase
Status vs the open pathfinding constellation
This PR sits in the middle of an 18-issue pathfinding stack. What this package does and does not cover:
Closes
Closes #2714— PathfindingLink off-mesh component.Closes #2715— PathfindingModifier volumetric cost-zone component.
Addresses (Luau-side; Rust binding still pending)
#2710—pathfinding.*namespace. This package matches the documented surface (path/registerMaterialCost/findNearestPoint/ lineOfSight). The issue specifically asks for a Rust binding incrates/zero_scripting/src/ffi/bindings/pathfinding.rs; until that lands callers canrequire("@builtin::systems.pathfinding.pathfinding")or callPathfinding.installGlobal()once at world boot to publish the table to_G.pathfinding. The Rust binding will shadow it cleanly when shipped.#2712— Path object with:computeAsync/:waypoints/:setTarget/.status/.blockedevent. Implemented as a Luau OO type with the same shape as the planned binding.#2713— PathWaypoint withaction ∈ {Walk, Jump, Custom}+ label dispatch.Customis emitted today by off-mesh links;Jumpis carried through the type so future graphs (#2711) can emit it without API churn.#2716— material-based costs.registerMaterialCost/getMaterialCost/materialCostsuse the same registry as modifier labels, so per-Path agent overrides shadow either uniformly.#2722(partial) — 3D agent profiles.gridGraph { connectivity = 26 }is full 3D + diagonals;connectivity = 6is 3D axis-aligned. Climbing is handled by off-mesh links today rather than vertical navmesh polys.
Out of scope (separately filed, not touched here)
#2711Recast-style navmesh bake (Rust).#2717NavMeshAgent component with autopilot (depends oncharacterController:moveTo, #2725).#2718ORCA/RVO local avoidance + crowd manager.#2719time-sliced / progressive path computation.#2720hierarchical pathfinding (HPA*).#2721streaming-aware navmesh tile lifecycle.#2724debug visualization + navmesh editor (depends on Visualization Options popup, #2643).Pathfinding.diagnostics.*is plumbed as a no-op hook so the future overlay slots in without touching call sites.#2725characterController:moveToand friends.
The package is intentionally engine-light. When the Rust substrate (#1400) and Recast bake (#2711) ship, the graph adapter and per-edge cost composition are the natural seams to swap; the cost-zone, link, Path object, and waypoint-action types stay put.