Grid represents a 2D grid where 0 cells are walkable and 1 are blocked.
[][]int
Poly is a polygon node in a NavMesh.
RequestGridPath starts an asynchronous A* search on a grid.
It returns a request ID and a channel that will receive the result.
{
id, ctx := p.prepare(ctx)
ch := make(chan GridPathResult, 1)
go func() {
defer close(ch)
path, found, err := gridAStar(ctx, grid, start, goal)
// If cancellation happened right before sending, surface it explicitly.
if ctx.Err() != nil && err == nil {
err = context.Canceled
found = false
path = nil
}
ch <- GridPathResult{Path: path, Found: found, Err: err}
p.finish(id)
}()
return id, ch
}
RequestNavMeshPath starts an asynchronous A* search on a navmesh.
It returns a request ID and a channel that will receive the result.
{
id, ctx := p.prepare(ctx)
out := make(chan MeshPathResult, 1)
go func() {
defer close(out)
path, found, err := navMeshAStar(ctx, mesh, start, goal)
if ctx.Err() != nil && err == nil {
err = context.Canceled
found = false
path = nil
}
out <- MeshPathResult{Path: path, Found: found, Err: err}
p.finish(id)
}()
return id, out
}
Cancel aborts a pending request.
{
p.mu.Lock()
if c, ok := p.cancels[id]; ok {
c()
delete(p.cancels, id)
}
p.mu.Unlock()
}
prepare registers a cancelable context for a new request.
{
p.mu.Lock()
p.nextID++
id := p.nextID
ctx, cancel := context.WithCancel(ctx)
p.cancels[id] = cancel
p.mu.Unlock()
return id, ctx
}
finish cleans up tracking for a completed/canceled request.
{ p.Cancel(id) }
New returns a new Pathfinder.
{
return &Pathfinder{cancels: make(map[int]context.CancelFunc)}
}
[]*gridNode
inBounds checks if (x,y) is inside the grid.
{
return y >= 0 && y < len(grid) && x >= 0 && x < len(grid[0])
}
gridAStar performs A* on a 4-neighborhood grid with unit costs and Manhattan heuristic.
{
if len(grid) == 0 || len(grid[0]) == 0 {
return nil, false, ErrOutOfBounds
}
sx, sy := int(start.X), int(start.Y)
gx, gy := int(goal.X), int(goal.Y)
if !inBounds(grid, sx, sy) || !inBounds(grid, gx, gy) {
return nil, false, ErrOutOfBounds
}
if grid[sy][sx] == 1 || grid[gy][gx] == 1 {
return nil, false, ErrBlocked
}
if sx == gx && sy == gy {
return []m.Vec2{{float32(sx), float32(sy)}}, true, nil
}
// Manhattan heuristic (admissible in 4-dir grid with unit costs).
h := func(x, y int) float32 { return float32(math.Abs(float64(x-gx)) + math.Abs(float64(y-gy))) }
startNode := &gridNode{pos: m.Vec2{float32(sx), float32(sy)}, g: 0, f: h(sx, sy)}
open := priorityQueue{startNode}
heap.Init(&open)
visited := map[[2]int]*gridNode{{sx, sy}: startNode}
for open.Len() > 0 {
select {
case <-ctx.Done():
return nil, false, context.Canceled
default:
}
current := heap.Pop(&open).(*gridNode)
x, y := int(current.pos.X), int(current.pos.Y)
if x == gx && y == gy {
return reconstructGrid(current), true, nil
}
// 4-neighbors
for _, n := range [][2]int{{x + 1, y}, {x - 1, y}, {x, y + 1}, {x, y - 1}} {
nx, ny := n[0], n[1]
if !inBounds(grid, nx, ny) || grid[ny][nx] == 1 {
continue
}
gScore := current.g + 1
key := [2]int{nx, ny}
if v, ok := visited[key]; ok && gScore >= v.g {
continue
}
node := &gridNode{
pos: m.Vec2{float32(nx), float32(ny)},
g: gScore,
f: gScore + h(nx, ny),
parent: current,
}
visited[key] = node
heap.Push(&open, node)
}
}
// Exhausted without reaching goal: not found.
return nil, false, nil
}
reconstructGrid rebuilds the path from the goal node by following parents.
{
var path []m.Vec2
for n != nil {
path = append(path, n.pos)
n = n.parent
}
// reverse in-place
for i := 0; i < len(path)/2; i++ {
path[i], path[len(path)-1-i] = path[len(path)-1-i], path[i]
}
return path
}
{
pf := New()
grid := Grid{
{0, 0, 0},
{1, 1, 0},
{0, 0, 0},
}
start := m.Vec2{0, 0}
goal := m.Vec2{2, 2}
_, ch := pf.RequestGridPath(context.Background(), grid, start, goal)
res := <-ch
if !res.Found {
t.Fatalf("expected path, got err %v", res.Err)
}
if len(res.Path) == 0 || res.Path[0] != start || res.Path[len(res.Path)-1] != goal {
t.Fatalf("unexpected path %v", res.Path)
}
}
{
pf := New()
// Large grid to give time for cancellation.
grid := make(Grid, 100)
for i := range grid {
grid[i] = make([]int, 100)
}
start := m.Vec2{0, 0}
goal := m.Vec2{99, 99}
id, ch := pf.RequestGridPath(context.Background(), grid, start, goal)
pf.Cancel(id)
res := <-ch
if res.Err != context.Canceled {
t.Fatalf("expected context.Canceled, got %v", res.Err)
}
}
import "container/heap"
import "context"
import "errors"
import "math"
import "sync"
import "github.com/rfwlab/rfw/v1/math"
m
import "context"
import "testing"
import "github.com/rfwlab/rfw/v1/math"
m