omath::pathfinding::Astar — Pathfinding over a navigation mesh
Header: your project’s
pathfinding/astar.hppNamespace:omath::pathfindingInputs: start/end asVector3<float>, aNavigationMeshOutput: ordered list of waypointsstd::vector<Vector3<float>>
Astar exposes a single public function, find_path, that computes a path of 3D waypoints on a navigation mesh. Internally it reconstructs the result with reconstruct_final_path from a closed set keyed by Vector3<float>.
API
namespace omath::pathfinding {
struct PathNode; // holds per-node search data (see "Expected PathNode fields")
class Astar final {
public:
[[nodiscard]]
static std::vector<Vector3<float>>
find_path(const Vector3<float>& start,
const Vector3<float>& end,
const NavigationMesh& nav_mesh) noexcept;
private:
[[nodiscard]]
static std::vector<Vector3<float>>
reconstruct_final_path(
const std::unordered_map<Vector3<float>, PathNode>& closed_list,
const Vector3<float>& current) noexcept;
};
} // namespace omath::pathfinding
Semantics
- Returns a polyline of 3D points from
starttoend. - If no path exists, the function typically returns an empty vector (behavior depends on implementation details; keep this contract in unit tests).
What NavigationMesh is expected to provide
The header doesn’t constrain NavigationMesh, but for A* it commonly needs:
- Neighborhood queries: given a position or node key → iterable neighbors.
- Traversal cost:
g(u,v)(often Euclidean distance or edge weight). - Heuristic:
h(x,end)(commonly straight-line distance on the mesh). - Projection / snap: the ability to map
start/endto valid nodes/points on the mesh (if they are off-mesh).
If your
NavigationMeshdoesn’t directly expose these,Astar::find_pathlikely does the adapter work (snapping to the nearest convex polygon/portal nodes and expanding across adjacency).
Expected PathNode fields
Although not visible here, PathNode typically carries:
Vector3<float> parent;— predecessor position or key for backtrackingfloat g;— cost fromstartfloat h;— heuristic toendfloat f;—g + h
reconstruct_final_path(closed_list, current) walks parent links from current back to the start, reverses the chain, and returns the path.
Heuristic & optimality
- Use an admissible heuristic (never overestimates true cost) to keep A optimal. The usual choice is Euclidean distance* in 3D:
cpp
h(x, goal) = (goal - x).length();
* For best performance, make it consistent (triangle inequality holds). Euclidean distance is consistent on standard navmeshes.
Complexity
Let V be explored vertices (or portal nodes) and E the traversed edges.
- With a binary heap open list: O(E log V) time, O(V) memory.
- With a d-ary heap or pairing heap you may reduce practical constants.
Typical usage
#include "omath/pathfinding/astar.hpp"
#include "omath/pathfinding/navigation_mesh.hpp"
using omath::Vector3;
using omath::pathfinding::Astar;
NavigationMesh nav = /* ... load/build mesh ... */;
Vector3<float> start{2.5f, 0.0f, -1.0f};
Vector3<float> goal {40.0f, 0.0f, 12.0f};
auto path = Astar::find_path(start, goal, nav);
if (!path.empty()) {
// feed to your agent/renderer
for (const auto& p : path) {
// draw waypoint p or push to steering
}
} else {
// handle "no path" (e.g., unreachable or disconnected mesh)
}
Notes & recommendations
- Start/end snapping: If
startorendare outside the mesh, decide whether to snap to the nearest polygon/portal or fail early. Keep this behavior consistent and document it whereNavigationMeshis defined. - Numerical stability: Prefer squared distances when only comparing (
dist2) to avoid unnecessarysqrt. - Tie-breaking: When
fties are common (grid-like graphs), bias toward largergor smallerhto reduce zig-zagging. -
Smoothing: A* returns a polyline that may hug polygon edges. Consider:
- String pulling / Funnel algorithm over the corridor of polygons to get a straightened path.
- Raycast smoothing (visibility checks) to remove redundant interior points.
- Hashing
Vector3<float>: Your repo definesstd::hash<omath::Vector3<float>>. Ensure equality/precision rules for using float keys are acceptable (or use discrete node IDs instead).
Testing checklist
- Start/end on the same polygon → direct path of 2 points.
- Disconnected components → empty result.
- Narrow corridors → path stays inside.
- Obstacles blocking → no path.
- Floating-point noise → still reconstructs a valid chain from parents.
Minimal pseudo-implementation outline (for reference)
// Pseudocode only — matches the header’s intent
std::vector<Vec3> find_path(start, goal, mesh) {
auto [snode, gnode] = mesh.snap_to_nodes(start, goal);
OpenSet open; // min-heap by f
std::unordered_map<Vec3, PathNode> closed;
open.push({snode, g=0, h=heuristic(snode, gnode)});
parents.clear();
while (!open.empty()) {
auto current = open.pop_min(); // node with lowest f
if (current.pos == gnode.pos)
return reconstruct_final_path(closed, current.pos);
for (auto [nbr, cost] : mesh.neighbors(current.pos)) {
float tentative_g = current.g + cost;
if (auto it = closed.find(nbr); it == closed.end() || tentative_g < it->second.g) {
closed[nbr] = { .parent = current.pos,
.g = tentative_g,
.h = heuristic(nbr, gnode.pos),
.f = tentative_g + heuristic(nbr, gnode.pos) };
open.push(closed[nbr]);
}
}
}
return {}; // no path
}
FAQ
-
Why return
std::vector<Vector3<float>>and not polygon IDs? Waypoints are directly usable by agents/steering and for rendering. If you also need the corridor (polygon chain), extend the API orPathNodeto store it. -
Does
find_pathmodify the mesh? No; it should be a read-only search overNavigationMesh.
Last updated: 31 Oct 2025