Defold Graph Pathfinder Extension - High-performance A* pathfinding library for real-time games and simulations.
- Initialization
- Node Management
- Edge Management
- Pathfinding
- Path Smoothing
- Game Object Nodes
- Spatial Index
- Statistics
- Enumerations
- Data Types
Initialize the pathfinding system. Must be called before any other pathfinding operations.
Syntax:
pathfinder.init(max_nodes, [max_gameobject_nodes], max_edges_per_node, heap_pool_block_size, max_cache_path_length)Parameters:
max_nodes(number): Maximum number of nodes in the graph. Minimum value is 32.max_gameobject_nodes(number|nil)[optional, default: 0]: Maximum number of game object nodesmax_edges_per_node(number): Maximum edges per nodeheap_pool_block_size(number): Size of heap pool blocks for A* algorithmmax_cache_path_length(number): Maximum length of cached paths
Important
The heap pool capacity equals max_nodes. If heap_pool_block_size > max_nodes, it will be automatically clamped to max_nodes to prevent heap allocation failures.
Recommended: Use heap_pool_block_size = 32 (default) and ensure max_nodes >= 32.
Important
If you plan to use projected pathfinding, consider increasing the value of max_nodes. Each entry point is treated as a new node.
Example: If your graph has 100 nodes and 10 agents are constantly finding projected paths, you'll have up to 10 additional entry points — resulting in a total of 110 nodes.
More info and QA about heap_pool_block_size and max_nodes
Example:
-- Initialize with 100 nodes, 10 game object nodes, 4 edges per node
pathfinder.init(100, 10, 4, 32, 32)Shutdown the pathfinding system and free all resources.
Syntax:
pathfinder.shutdown()Example:
function final(self)
pathfinder.shutdown()
endAdd a single node to the pathfinding graph.
Syntax:
local node_id = pathfinder.add_node(x, y)Parameters:
x(number): X coordinate of the nodey(number): Y coordinate of the node
Returns:
node_id(number): Unique identifier for the created node
Example:
local node1 = pathfinder.add_node(100, 200)
local node2 = pathfinder.add_node(300, 400)Add multiple nodes to the pathfinding graph in batch.
Syntax:
local node_ids = pathfinder.add_nodes(node_positions)Parameters:
node_positions(PathNode[]): Array of node positions with x and y coordinates
Returns:
node_ids(number[]): Array of created node IDs
Example:
local nodes = pathfinder.add_nodes({
{ x = 100, y = 200 },
{ x = 200, y = 300 },
{ x = 300, y = 400 }
})
-- nodes = {0, 1, 2}Remove a node from the pathfinding graph.
Syntax:
pathfinder.remove_node(node_id)Parameters:
node_id(number): ID of the node to remove
Example:
pathfinder.remove_node(node_id)Move an existing node to a new position.
Syntax:
pathfinder.move_node(node_id, x, y)Parameters:
node_id(number): ID of the node to movex(number): New X coordinatey(number): New Y coordinate
Example:
pathfinder.move_node(node1, 150, 250)Get the position of a node.
Syntax:
local position = pathfinder.get_node_position(node_id)Parameters:
node_id(number): ID of the node
Returns:
position(PathNode): Table with x and y coordinates
Example:
local pos = pathfinder.get_node_position(node1)
print("Node position:", pos.x, pos.y)Get edges for a specific node with bidirectionality information.
Syntax:
local edges = pathfinder.get_node_edges(node_id, [bidirectional], [include_incoming])Parameters:
node_id(number): ID of the nodebidirectional(boolean) [optional, default: true]: If true, returns all edges. If false, returns only unidirectional edges.include_incoming(boolean) [optional, default: false]: If true, includes incoming edges. If false, includes only outgoing edges.
Returns:
edges(PathEdge[]): Array of edge definitions
Example:
local edges = pathfinder.get_node_edges(node1,true,true)
pprint(edges)Add a single edge between two nodes.
Syntax:
pathfinder.add_edge(from_node_id, to_node_id, bidirectional, cost)Parameters:
from_node_id(number): Source node IDto_node_id(number): Target node IDbidirectional(boolean): If true, creates edges in both directionscost(number|nil)[optional, default: Euclidean distance between nodes]: Edge cost
Example:
-- Create a bidirectional edge with default cost
pathfinder.add_edge(node1, node2, true)
-- Create a one-way edge with custom cost
pathfinder.add_edge(node2, node3, false, 100)Add multiple edges to the pathfinding graph in batch.
Syntax:
pathfinder.add_edges(edges)Parameters:
edges(PathEdge[]): Array of edge definitions
Example:
pathfinder.add_edges({
{ from_node_id = node1, to_node_id = node2, bidirectional = true },
{ from_node_id = node2, to_node_id = node3, bidirectional = true, cost = 150 },
{ from_node_id = node2, to_node_id = node3 }
})Remove an edge between two nodes.
Syntax:
pathfinder.remove_edge(from_node_id, to_node_id, [bidirectional])Parameters:
from_node_id(number): Source node IDto_node_id(number): Target node IDbidirectional(boolean)[optional, default: false]: If true, removes edges in both directions
Example:
pathfinder.remove_edge(node1, node2, true)Find a path between two nodes using A* algorithm.
Syntax:
local path_length, status, status_text, path = pathfinder.find_node_to_node(start_node_id, goal_node_id, max_path_length, [smooth_id])Parameters:
start_node_id(number): Starting node IDgoal_node_id(number): Goal node IDmax_path_length(number): Maximum path length to searchsmooth_id(number|nil) [optional, default: 0 = no smoothing]: Optional smoothing configuration ID
Returns:
path_length(number): Number of waypoints in the pathstatus(number): PathStatus code indicating success or errorstatus_text(string): Human-readable status messagepath(PathNode[]): Array of waypoints (positions with optional node IDs)
Example:
local path_length, status, status_text, path = pathfinder.find_node_to_node(start_id, goal_id, 128)
if status == pathfinder.PathStatus.SUCCESS then
print("Path found with", path_length, "waypoints")
for i, waypoint in ipairs(path) do
print("Waypoint", i, ":", waypoint.x, waypoint.y, "Node ID:", waypoint.id)
end
else
print("Pathfinding failed:", status_text)
endFind a path from an arbitrary position (not on graph) to a goal node. Projects the start position onto the nearest graph edge and pathfinds from there.
Syntax:
local path_length, status, status_text, entry_point, path = pathfinder.find_projected_to_node(x, y, goal_node_id, max_path_length, [smooth_id])Parameters:
x(number): X coordinate of start positiony(number): Y coordinate of start positiongoal_node_id(number): Goal node IDmax_path_length(number): Maximum path length to searchsmooth_id(number|nil) [optional, default: 0 = no smoothing]: Optional smoothing configuration ID
Returns:
path_length(number): Number of waypoints in the pathstatus(number): PathStatus code indicating success or errorstatus_text(string): Human-readable status messageentry_point(vector3): Position where the path enters the graphpath(PathNode[]): Array of waypoints (positions with optional node IDs)
Example:
local mouse_x, mouse_y = 150, 250
local path_length, status, status_text, entry_point, path = pathfinder.find_projected_to_node(mouse_x, mouse_y, goal_id, 128)
if status == pathfinder.PathStatus.SUCCESS then
print("Entry point:", entry_point.x, entry_point.y)
-- Draw line from mouse position to entry point
-- Then follow the path
endFind a path from a start node to an arbitrary position (not on graph). Projects the target position onto the nearest graph edge and pathfinds to there.
Syntax:
local path_length, status, status_text, exit_point, path = pathfinder.find_node_to_projected(start_node_id, x, y, max_path_length, [smooth_id])Parameters:
start_node_id(number): Starting node IDx(number): X coordinate of target positiony(number): Y coordinate of target positionmax_path_length(number): Maximum path length to searchsmooth_id(number|nil) [optional, default: 0 = no smoothing]: Optional smoothing configuration ID
Returns:
path_length(number): Number of waypoints in the pathstatus(number): PathStatus code indicating success or errorstatus_text(string): Human-readable status messageexit_point(vector3): Position where the path exits the graphpath(PathNode[]): Array of waypoints (positions with optional node IDs)
Example:
local target_x, target_y = 150, 250
local path_length, status, status_text, exit_point, path = pathfinder.find_node_to_projected(start_id, target_x, target_y, 128)
if status == pathfinder.PathStatus.SUCCESS then
print("Exit point:", exit_point.x, exit_point.y)
-- Follow the path, then draw line from exit point to target position
endFind a path from an arbitrary position (not on graph) to another arbitrary position (not on graph). Projects both start and target positions onto the nearest graph edges and pathfinds between them.
Syntax:
local path_length, status, status_text, entry_point, exit_point, path = pathfinder.find_projected_to_projected(start_x, start_y, target_x, target_y, max_path_length, [smooth_id])Parameters:
start_x(number): X coordinate of start positionstart_y(number): Y coordinate of start positiontarget_x(number): X coordinate of target positiontarget_y(number): Y coordinate of target positionmax_path_length(number): Maximum path length to searchsmooth_id(number|nil) [optional, default: 0 = no smoothing]: Optional smoothing configuration ID
Returns:
path_length(number): Number of waypoints in the pathstatus(number): PathStatus code indicating success or errorstatus_text(string): Human-readable status messageentry_point(vector3): Position where the path enters the graphexit_point(vector3): Position where the path exits the graphpath(PathNode[]): Array of waypoints (positions with optional node IDs)
Example:
local start_x, start_y = 50, 50
local target_x, target_y = 350, 350
local path_length, status, status_text, entry_point, exit_point, path = pathfinder.find_projected_to_projected(start_x, start_y, target_x, target_y, 128)
if status == pathfinder.PathStatus.SUCCESS then
print("Entry point:", entry_point.x, entry_point.y)
print("Exit point:", exit_point.x, exit_point.y)
-- Draw line from start position to entry point
-- Follow the path
-- Draw line from exit point to target position
endCreate a path smoothing configuration. A maximum of 64 configurations is allowed.
Syntax:
local smooth_id = pathfinder.add_path_smoothing(config)Parameters:
config(PathSmoothConfig): Smoothing configuration table
Returns:
smooth_id(number): Unique identifier for the smoothing configuration
Example:
local smooth_config = {
style = pathfinder.PathSmoothStyle.BEZIER_QUADRATIC,
bezier_sample_segment = 8,
bezier_curve_radius = 0.8
}
local smooth_id = pathfinder.add_path_smoothing(smooth_config)
-- Use in pathfinding
local path_length, status, status_text, path = pathfinder.find_node_to_node(start_id, goal_id, 128, smooth_id)Update a path smoothing configuration.
Syntax:
pathfinder.update_path_smoothing(smooth_id, config)Parameters:
smooth_id(number): Unique identifier for the smoothing configurationconfig(PathSmoothConfig): Smoothing configuration table
Example:
local smooth_config = {
style = pathfinder.PathSmoothStyle.BEZIER_QUADRATIC,
bezier_sample_segment = 8,
bezier_curve_radius = 0.8
}
local smooth_id = pathfinder.add_path_smoothing(smooth_config)
smooth_config = {
style = pathfinder.PathSmoothStyle.BEZIER_QUADRATIC,
bezier_sample_segment = 4,
bezier_curve_radius = 0.3
}
pathfinder.update_path_smoothing(smooth_id, smooth_config)
-- Use in pathfinding
local path_length, status, status_text, path = pathfinder.find_node_to_node(start_id, goal_id, 128, smooth_id)Apply path smoothing to a set of waypoints.
Syntax:
local smoothed_length, smoothed_path = pathfinder.smooth_path(smooth_id, waypoints)Parameters:
smooth_id(number): Smoothing configuration ID (from add_path_smoothing)waypoints(PathNode[]): Array of waypoint positions
Returns:
smoothed_length(number): Number of points in smoothed pathsmoothed_path(PathNode[]): Array of smoothed positions
Example:
local waypoints = {
{ x = 100, y = 100 },
{ x = 200, y = 200 },
{ x = 300, y = 150 }
}
local smoothed_length, smoothed_path = pathfinder.smooth_path(smooth_id, waypoints)Add a game object node that automatically tracks the game object's position.
Syntax:
local node_id = pathfinder.add_gameobject_node(game_object_instance, [use_world_position])Parameters:
game_object_instance(userdata): Game object instanceuse_world_position(boolean)[optional, default: false]: Whether to use world or local position
Returns:
node_id(number): Unique identifier for the created node
Example:
local go_url = msg.url("/enemy")
local node_id = pathfinder.add_gameobject_node(go_url, true)Add multiple game object nodes that automatically track their game objects' positions.
Syntax:
local node_ids = pathfinder.add_gameobject_nodes(game_object_nodes)Parameters:
game_object_nodes(GameObjectNodeConfig[]): Array of game object node configurations
Returns:
node_ids(number[]): Array of created node IDs
Example:
local node_gos = {
{ msg.url("/enemy1"), true }, -- use world position
{ msg.url("/enemy2"), false }, -- use local position
{ msg.url("/enemy3") } -- default to local position (false)
}
local go_node_ids = pathfinder.add_gameobject_nodes(node_gos)Convert an existing node to a game object node.
Syntax:
pathfinder.convert_gameobject_node(node_id, game_object_instance, use_world_position)Parameters:
node_id(number): Existing node ID to convertgame_object_instance(userdata): Game object instance to trackuse_world_position(boolean): Whether to use world or local position
Example:
local go_url = msg.url("/player")
pathfinder.convert_gameobject_node(existing_node_id, go_url, true)Remove a game object node from tracking and the pathfinding graph.
Syntax:
pathfinder.remove_gameobject_node(node_id)Parameters:
node_id(number): ID of the game object node to remove
Example:
pathfinder.remove_gameobject_node(node_id)Pause automatic updates for a game object node.
Syntax:
pathfinder.pause_gameobject_node(node_id)Parameters:
node_id(number): ID of the game object node to pause
Example:
pathfinder.pause_gameobject_node(node_id)Resume automatic updates for a game object node.
Syntax:
pathfinder.resume_gameobject_node(node_id)Parameters:
node_id(number): ID of the game object node to resume
Example:
pathfinder.resume_gameobject_node(node_id)Enable or disable automatic game object node position updates.
Syntax:
pathfinder.gameobject_update(enabled)Parameters:
enabled(boolean): True to enable automatic updates, false to disable
Example:
pathfinder.gameobject_update(true) -- Enable automatic updatesSet the update frequency for game object node position updates. It is possible to set an independent update frequency for the game object position update iteration. The default value is taken from the display.frequency setting in the game.project file. The update loop follows the same structure as in the Defold source.
Syntax:
pathfinder.set_update_frequency(frequency)Parameters:
frequency(number): Update frequency in Hz
Example:
pathfinder.set_update_frequency(60) -- Update at 60 HzInitialize the spatial index with custom configuration for accelerating projected pathfinding queries.
Syntax:
pathfinder.set_spatial_index(max_grid_size, min_cell_size, max_cell_size, max_cell_search_radius)Parameters:
max_grid_size(number): Maximum grid dimension (recommended: 1000)min_cell_size(number): Minimum cell size (recommended: 10.0)max_cell_size(number): Maximum cell size (recommended: 500.0)max_cell_search_radius(number): Search radius in cells (1 = 3×3 grid, 2 = 5×5 grid)
Description:
The spatial index is a grid-based acceleration structure for finding nearest edges during projected pathfinding.
When to use:
- Graph has >500 nodes
- Frequent projected pathfinding queries (>20 per frame)
- Need 10-100× speedup for large graphs
Behavior:
- Must be called AFTER
pathfinder.init() - If not called, spatial index remains disabled
- If called, spatial index is built lazily on first projected path query
- Automatically updates when nodes move
Example:
function init(self)
-- Initialize pathfinder
pathfinder.init(1000, nil, 4, 32, 32)
-- Enable spatial index for large graph
pathfinder.set_spatial_index(1000, 10.0, 500.0, 1)
-- Add nodes and edges...
-- Spatial index will be built on first projected path query
endGet spatial index grid data for debug visualization.
Syntax:
local grid = pathfinder.get_spatial_index()Returns:
grid(table): Table withverticalandhorizontalarrays, each containing line data withstart_positionandend_position(vector3)
Description:
Returns the spatial index grid structure for rendering debug overlays. Useful for visualizing the spatial partitioning used to accelerate projected pathfinding.
Example:
function update(self, dt)
local grid = pathfinder.get_spatial_index()
if grid.vertical then
-- Draw vertical grid lines
for _, line in ipairs(grid.vertical) do
msg.post("@render:", "draw_line", {
start_point = line.start_position,
end_point = line.end_position,
color = vmath.vector4(1, 0, 0, 0.3)
})
end
end
if grid.horizontal then
-- Draw horizontal grid lines
for _, line in ipairs(grid.horizontal) do
msg.post("@render:", "draw_line", {
start_point = line.start_position,
end_point = line.end_position,
color = vmath.vector4(1, 0, 0, 0.3)
})
end
end
endCheck if the spatial index has been initialized and built.
Syntax:
local is_initialized = pathfinder.spatial_index_initialized()Returns:
is_initialized(boolean): True if spatial index is active, false otherwise
Description:
Returns whether the spatial index has been configured via set_spatial_index() and built. The spatial index is built lazily on the first projected path query after calling set_spatial_index().
Example:
function update(self, dt)
if pathfinder.spatial_index_initialized() then
-- Spatial index is active, projected queries will be fast
label.set_text("#status", "Spatial Index: ACTIVE")
else
-- No spatial index, projected queries use full scan
label.set_text("#status", "Spatial Index: INACTIVE")
end
endGet comprehensive statistics about pathfinding caches and spatial index.
Syntax:
local stats = pathfinder.get_stats()Returns:
stats(table): Table containing cache and spatial index statistics
Fields:
path_cache(table): Path cache statisticscache_entries(number): Current number of cached pathscache_capacity(number): Maximum cache capacitycache_hit_rate(number): Cache hit rate percentage (0-100)
distance_cache(table): Distance cache statisticscurrent_size(number): Current number of cached distanceshit_count(number): Number of cache hitsmiss_count(number): Number of cache misseshit_rate(number): Cache hit rate percentage (0-100)
spatial_index(table): Spatial index statisticscell_count(number): Number of grid cellsedge_count(number): Total edges indexedavg_edges_per_cell(number): Average edges per cellmax_edges_per_cell(number): Maximum edges in any cell
Description:
Provides detailed performance metrics for monitoring and optimizing pathfinding operations. Use this data to:
- Tune cache sizes
- Monitor spatial index efficiency
- Identify performance bottlenecks
- Validate optimization strategies
Example:
function update(self, dt)
local stats = pathfinder.get_stats()
-- Monitor path cache efficiency
print("Path Cache: " .. stats.path_cache.cache_entries .. "/" .. stats.path_cache.cache_capacity)
print("Path Cache Hit Rate: " .. stats.path_cache.cache_hit_rate .. "%")
-- Monitor distance cache
print("Distance Cache Size: " .. stats.distance_cache.current_size)
print("Distance Cache Hit Rate: " .. stats.distance_cache.hit_rate .. "%")
-- Monitor spatial index
if stats.spatial_index.cell_count > 0 then
print("Spatial Index Cells: " .. stats.spatial_index.cell_count)
print("Avg Edges per Cell: " .. string.format("%.2f", stats.spatial_index.avg_edges_per_cell))
end
endStatus codes for pathfinding operations.
| Constant | Value | Description |
|---|---|---|
SUCCESS |
0 | Operation completed successfully |
SUCCESS_START_FALLBACK |
1 | Success, but start position used fallback to nearest cell (navmesh only) |
SUCCESS_GOAL_FALLBACK |
2 | Success, but goal position used fallback to nearest cell (navmesh only) |
ERROR_NO_PATH |
-1 | No valid path found between start and goal nodes |
ERROR_START_GOAL_NODE_SAME |
-12 | Start node ID and goal node ID are the same |
ERROR_START_NODE_INVALID |
-2 | Invalid or inactive start node ID |
ERROR_GOAL_NODE_INVALID |
-3 | Invalid or inactive goal node ID |
ERROR_START_NOT_IN_CELL |
-13 | Start position not in any cell, fallback disabled (navmesh only) |
ERROR_GOAL_NOT_IN_CELL |
-14 | Goal position not in any cell, fallback disabled (navmesh only) |
ERROR_NODE_FULL |
-4 | Node capacity reached, cannot add more nodes |
ERROR_EDGE_FULL |
-5 | Edge capacity reached, cannot add more edges |
ERROR_HEAP_FULL |
-6 | Heap pool exhausted during pathfinding |
ERROR_PATH_TOO_LONG |
-7 | Path exceeds maximum allowed length |
ERROR_GRAPH_CHANGED |
-8 | Graph modified during pathfinding (retrying) |
ERROR_GRAPH_CHANGED_TOO_OFTEN |
-11 | Graph changed too often during pathfinding |
ERROR_NO_PROJECTION |
-9 | Cannot project point onto graph (no edges exist) |
ERROR_VIRTUAL_NODE_FAILED |
-10 | Failed to create or connect virtual node |
Usage:
if status == pathfinder.PathStatus.SUCCESS then
-- Path found successfully
elseif status == pathfinder.PathStatus.SUCCESS_START_FALLBACK then
-- Path found, but start was outside navmesh (moved to nearest cell)
elseif status == pathfinder.PathStatus.ERROR_NO_PATH then
-- No path exists
endPath smoothing algorithms.
| Constant | Value | Description | Best For |
|---|---|---|---|
NONE |
0 | No smoothing (angular paths, fastest) | Grid-based movement |
CATMULL_ROM |
1 | Passes through all waypoints with smooth curves | Precise waypoint following |
BEZIER_CUBIC |
2 | Very smooth curves with two control points | Cinematic cameras, UI |
BEZIER_QUADRATIC |
3 | Corner-only smoothing (recommended) | Character movement, vehicles |
BEZIER_ADAPTIVE |
4 | Adaptive corner smoothing (highly customizable) | Dynamic paths, varying turn angles |
CIRCULAR_ARC |
5 | Perfect circular arcs (best for tile-based games) | Railroads, tower defense |
BEZIER_QUADRATIC offers good balance between quality and performance.
Usage:
local config = {
style = pathfinder.PathSmoothStyle.BEZIER_QUADRATIC,
bezier_sample_segment = 8,
bezier_curve_radius = 0.8
}Represents a node position in the pathfinding graph.
Fields:
x(number): X coordinate of the node positiony(number): Y coordinate of the node position
Represents an edge between two nodes.
Fields:
from_node_id(number): Source node IDto_node_id(number): Target node IDbidirectional(boolean)[optional]: Whether the edge is bidirectionalcost(number|nil)[optional]: Optional edge cost (default: Euclidean distance)
Configuration for path smoothing.
Fields:
style(number): Path smoothing style (use pathfinder.PathSmoothStyle constants)bezier_sample_segment(number): Number of samples per segment for Bezier curves (default: 8)bezier_control_point_offset(number)[0.0-1.0, default: 0.4]: Control point offset for BEZIER_CUBIC stylebezier_curve_radius(number) [0.0-1.0, default: 0.8]: Curve radius for BEZIER_QUADRATIC stylebezier_adaptive_tightness(number)[default: 0.5]: Tightness for BEZIER_ADAPTIVE stylebezier_adaptive_roundness(number)[default: 0.5]: Roundness for BEZIER_ADAPTIVE stylebezier_adaptive_max_corner_distance(number][default: 50.0]: Maximum corner distance for BEZIER_ADAPTIVEbezier_arc_radius(number)[default: 60.0]: Arc radius for CIRCULAR_ARC style
Configuration for a game object node (used in add_gameobject_nodes).
Fields:
[1](msg.url): Game object instance[2](boolean|nil)[optional, default: false if omitted]: Whether to use world position
function init(self)
-- Initialize pathfinder
pathfinder.init(32, nil, 4, 32, 4)
-- Create nodes
local nodes = pathfinder.add_nodes({
{ x = 100, y = 100 },
{ x = 200, y = 200 },
{ x = 300, y = 200 },
{ x = 400, y = 100 }
})
-- Create edges
pathfinder.add_edges({
{ from_node_id = nodes[1], to_node_id = nodes[2], bidirectional = true },
{ from_node_id = nodes[2], to_node_id = nodes[3], bidirectional = true },
{ from_node_id = nodes[3], to_node_id = nodes[4], bidirectional = true }
})
-- Create smoothing configuration
local smooth_config = {
style = pathfinder.PathSmoothStyle.BEZIER_QUADRATIC,
bezier_sample_segment = 8,
bezier_curve_radius = 0.8
}
local smooth_id = pathfinder.add_path_smoothing(smooth_config)
-- Find path with smoothing
local start_id = nodes[1]
local goal_id = nodes[4]
local path_length, status, status_text, path = pathfinder.find_node_to_node(start_id, goal_id, 128, smooth_id)
if status == pathfinder.PathStatus.SUCCESS then
print("Path found with", path_length, "waypoints")
for i, waypoint in ipairs(path) do
print(string.format("Waypoint %d: (%.1f, %.1f)", i, waypoint.x, waypoint.y))
end
else
print("Pathfinding failed:", status_text)
end
end
function final(self)
pathfinder.shutdown()
end
The heap_pool_block_size parameter defines the maximum size of the A algorithm's open set (priority queue)* during a single pathfinding operation.
During pathfinding, A* maintains an "open set" of nodes to explore:
- Start: Open set contains only the start node
- Expansion: Pop lowest-cost node, add its unexplored neighbors to open set
- Growth: Open set grows as the search expands outward through the graph
- Peak: Maximum size reached when search is widest (before finding goal)
- Goal Found: Open set collapses as path is reconstructed
For 90% of use cases, pool_block_size = 32 is sufficient:
pathfinder.init(
max_nodes, -- Your graph size (e.g., 150)
nil,
max_edges_per_node, -- Your max edges (e.g., 10)
32, --Default - works for most graphs
8 -- Cache length
);Works well for:
- Small to medium graphs (< 500 nodes)
- Sparse graphs (average degree < 6)
- Short to medium paths (< 20 hops)
For dense graphs or long paths, estimate based on graph characteristics:
// Formula: heap_pool_block_size ≈ sqrt(nodes_likely_to_explore)
// Sparse graph (grid, few connections):
heap_pool_block_size = 32; // Open set rarely exceeds 32 nodes
// Medium density (navigation graph):
heap_pool_block_size = sqrt(total_nodes) / 2; // e.g., sqrt(1000) / 2 ≈ 16
// Dense graph (many connections):
heap_pool_block_size = sqrt(total_nodes); // e.g., sqrt(1000) ≈ 32
// Worst case (might explore entire graph):
heap_pool_block_size = total_nodes; // Maximum possible
The library uses a single-buffer pooling strategy for performance:
At Init Time:
// Allocate ONE contiguous buffer
heap_pool_buffer = malloc(max_nodes * sizeof(HeapNode));
heap_pool_capacity = max_nodes; // Total capacityDuring find_path():
// Each pathfinding operation requests a slice
heap_slice_start = heap_pool_buffer[current_offset];
heap_slice_size = pool_block_size;
// Check if slice fits in pool
if (current_offset + pool_block_size > heap_pool_capacity) {
return ERROR_HEAP_FULL; // Not enough space!
}
current_offset += pool_block_size; // Reserve this sliceThe Constraint:
- Pool capacity =
max_nodes - Request size =
pool_block_size - If
pool_block_size > max_nodes: The first allocation fails because you're requesting more than the entire pool
Why This Design?
- Performance: Single allocation at init time (no runtime malloc)
- Predictability: Memory usage known upfront
- Cache-friendly: Contiguous buffer, good memory locality
- Zero-copy: Slices are just pointers into the buffer
Trade-off: max_nodes must be large enough to accommodate pool_block_size
This behavior is intentional:
- Memory Safety: Prevents buffer overruns
- Resource Limits: Enforces declared capacity limits
- Predictability: Fails fast rather than corrupting memory
pathfinder.init(
400, -- Graph has 400 nodes,
nil,
4, -- Each node has max 4 edges
32, -- Default: sufficient for grid searches
8 -- Typical path length on grid
);pathfinder.init(
150, -- Graph has 150 nodes,
nil,
10, -- Dense: up to 10 connected edges per node
32, -- Default: works for most nav mesh queries
10 -- Nav mesh paths can be longer
);pathfinder.init(
5000, -- Graph has 5000 nodes,
nil,
8, -- Moderate connectivity
64, -- Larger: anticipating complex searches
12 -- Longer paths in large world
);There are two kinds of caches involved in pathfinding.
Responsible for caching frequently reused paths.
Responsible for caching the distances between nodes to speed up calculations.
- Moving or removing nodes breaks the cache, but only for paths that include the moved or removed node and its related edges — not the entire cache.
- Removing edges breaks the cache, but only for paths that include the removed edge, related nodes, and edges — not the entire cache.
- Projected paths are cached, but they are retrieved from cache only if the start point and/or end point are exactly the same.
- If a path includes a moving node (and its edges), it cannot be retrieved from the cache.
- Smoothed paths are not cached.
A Spatial Index is a grid-based structure used to find the nearest edge from a projected point. It provides significant performance improvements for projected pathfinding in large graphs.
The spatial index is no longer enabled automatically. You must explicitly configure it by calling pathfinder.set_spatial_index() after initialization.
When to enable:
- Graph has >500 nodes
- Frequent projected pathfinding queries (>20 per frame)
- Expected speedup: 10-100× for large graphs
Configuration Steps:
-- 1. Initialize pathfinder
pathfinder.init(1000, nil, 4, 32, 32)
-- 2. Configure spatial index (required for acceleration)
pathfinder.set_spatial_index(
1000, -- max_grid_size
10.0, -- min_cell_size
500.0, -- max_cell_size
1 -- max_cell_search_radius (1 = 3×3 grid)
)
-- 3. Add nodes and edges
-- Spatial index will be built lazily on first projected path queryBehavior:
- Lazy initialization: Built on first projected pathfinding query after
set_spatial_index()is called - Cell size automatically calculated from average edge length (clamped to min/max)
- Incrementally updates when nodes move
- Integrates with cache invalidation
- Check status with
pathfinder.spatial_index_initialized()
Debug Visualization:
-- Get spatial index grid for rendering
local grid = pathfinder.get_spatial_index()
if grid.vertical then
for _, line in ipairs(grid.vertical) do
-- Draw vertical grid lines
msg.post("@render:", "draw_line", {
start_point = line.start_position,
end_point = line.end_position,
color = vmath.vector4(1, 0, 0, 0.3)
})
end
end