Log inGet started
World

Builtin Library

Branchmain
Branchesmainpublish
Commit61a33237e0

README.md

pathfinding.module

Public API for the pathfinding package. Caller-facing entry point lives in init.luau; sibling modules implement individual concerns.

Modules

FilePurpose
init.luauPublic surface (path, addLink, addModifier, registries, costs).
astar.luauGeneric A* over an adjacent(node) -> { Edge } callback. Binary-heap frontier, opaque node identity via nodeKey.
graph.luauTwo concrete graphs: grid (uniform 3D voxel grid) and waypoint (hand-authored network). Both expose the same interface.
modifiers.luauVolumetric cost-zone registry. AABB containment, OBB hooks reserved for future engine OBB primitive.
links.luauOff-mesh link registry + per-query baking onto a graph.
path.luauWaypoint / Path constructors. Mirrors Roblox PathWaypoint shape.

Algorithm at a glance

  1. nodeAt(start) / nodeAt(goal) — snap world points onto graph nodes.
  2. links.bake(graph, agent) — snap every enabled, condition-satisfying link's endpoints onto graph node-keys; bucket adjacency lookups by from-key. One pass per path call, not per A* expansion.
  3. A* expands each node, calls the closure adjacent(node). The closure:
    • asks the graph for native neighbours,
    • for each neighbour, queries modifiers.costAt(midpoint, costs, mode),
    • skips edges with math.huge multipliers,
    • appends link-overlay edges from linkBuckets[nodeKey].
  4. On reach: Path.fromAStar(result, graph, edgeMeta) produces a list of {position, action, label, link, payload} waypoints. Walk edges become action = "Walk"; link edges become action = "Custom" with label = link.label.

Edge-cost composition

Per edge e: a -> b:

edge_cost = base_cost(a, b) * compose({ modifier.cost-at-mid(a, b) }, mode)

Composition mode:

  • multiply: cost *= Π c_i (default; composable preferences amplify).
  • max: cost *= max(c_i) (worst-zone wins; conservative).
  • first: cost *= c_first (registration order; legacy).

math.huge from any modifier blocks the edge regardless of mode. The per-edge midpoint sample matches Roblox's PathfindingModifier semantics exactly — multiple overlapping modifiers compose.

Identity & determinism

  • A* node identity is by string key (graph.nodeKey(node)), not table identity, so transient nodes built by nodeAt snap onto the same key as graph-internal nodes.
  • The frontier is a binary min-heap; ties on f are broken by insertion order. With identical inputs the same path is returned — important for tests and replay.

Extension points

  • graph.luau exposes a clear interface (nodeAt, nodeKey, position, adjacent, heuristic, edgeMidpoint). Future navmesh-polygon graphs implement the same shape; nothing in the rest of the package cares.
  • modifiers.luau already exposes a basis field for oriented bounding boxes; the AABB-containment fast path is the current default. When an OBB primitive lands (#2677-style attachment basis or a future renderer-side OBB) the basis-aware branch in modifiers.contains is the single place to extend.
  • links.luau::bake builds the per-query overlay; if predicate links start showing up in profiles, this is the cache point.

Boundaries

  • This module performs no engine I/O: no physics raycasts, no entity spawning, no renderer state, no time. Pure data in, path out.
  • That means the package never breaks the engine, and tests can exercise it without booting the engine — see astar.luau::search
    • graph.grid for the canonical setup.
  • The components (PathfindingLink.component, PathfindingModifier.component) own the entity-side wiring and call _addLinkFromComponent / _addModifierFromComponent in awake. They're a thin, optional adapter on top of the module.