Grid type

Grid represents a 2D grid where 0 cells are walkable and 1 are blocked.

Type Definition:

[][]int

Poly struct

Poly is a polygon node in a NavMesh.

Fields:

  • ID (int)
  • Center (m.Vec2)
  • Neighbors ([]int)

GridPathResult struct

Fields:

  • Path ([]m.Vec2)
  • Found (bool)
  • Err (error)

MeshPathResult struct

Fields:

  • Path ([]int)
  • Found (bool)
  • Err (error)

Pathfinder struct

Fields:

  • mu (sync.Mutex)
  • nextID (int)
  • cancels (map[int]context.CancelFunc)

Methods:

RequestGridPath

RequestGridPath starts an asynchronous A* search on a grid.

It returns a request ID and a channel that will receive the result.


Parameters:
  • ctx context.Context
  • grid Grid
  • start m.Vec2
  • goal m.Vec2

Returns:
  • int
  • <-chan GridPathResult

References:


Show/Hide Method Body
{
	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

RequestNavMeshPath starts an asynchronous A* search on a navmesh.

It returns a request ID and a channel that will receive the result.


Parameters:
  • ctx context.Context
  • mesh NavMesh
  • start int
  • goal int

Returns:
  • int
  • <-chan MeshPathResult

References:


Show/Hide Method Body
{
	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

Cancel aborts a pending request.


Parameters:
  • id int

Show/Hide Method Body
{
	p.mu.Lock()
	if c, ok := p.cancels[id]; ok {
		c()
		delete(p.cancels, id)
	}
	p.mu.Unlock()
}

prepare

prepare registers a cancelable context for a new request.


Parameters:
  • ctx context.Context

Returns:
  • int
  • context.Context

Show/Hide Method Body
{
	p.mu.Lock()
	p.nextID++
	id := p.nextID
	ctx, cancel := context.WithCancel(ctx)
	p.cancels[id] = cancel
	p.mu.Unlock()
	return id, ctx
}

finish

finish cleans up tracking for a completed/canceled request.


Parameters:
  • id int

Show/Hide Method Body
{ p.Cancel(id) }

New function

New returns a new Pathfinder.

Returns:

  • *Pathfinder
Show/Hide Function Body
{
	return &Pathfinder{cancels: make(map[int]context.CancelFunc)}
}

gridNode struct

Fields:

  • pos (m.Vec2)
  • g (float32)
  • f (float32)
  • parent (*gridNode)
  • index (int)

priorityQueue type

Type Definition:

[]*gridNode

inBounds function

inBounds checks if (x,y) is inside the grid.

Parameters:

  • grid Grid
  • x int
  • y int

Returns:

  • bool

References:

  • Grid (v1/ai/pathfinding)
Show/Hide Function Body
{
	return y >= 0 && y < len(grid) && x >= 0 && x < len(grid[0])
}

gridAStar function

gridAStar performs A* on a 4-neighborhood grid with unit costs and Manhattan heuristic.

Parameters:

  • ctx context.Context
  • grid Grid
  • start m.Vec2
  • goal m.Vec2

Returns:

  • []m.Vec2
  • bool
  • error

References:

  • Grid (v1/ai/pathfinding)
Show/Hide Function Body
{
	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 function

reconstructGrid rebuilds the path from the goal node by following parents.

Parameters:

  • n *gridNode

Returns:

  • []m.Vec2
Show/Hide Function Body
{
	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
}

priorityQueueNav type

Type Definition:

[]*navNode

reconstructNav function

reconstructNav rebuilds the polygon path by following parents.

Parameters:

  • n *navNode

Returns:

  • []int
Show/Hide Function Body
{
	var path []int
	for n != nil {
		path = append(path, n.id)
		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
}

TestRequestGridPathFindsPath function

Parameters:

  • t *testing.T
Show/Hide Function Body
{
	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)
	}
}

TestRequestNavMeshPathFindsPath function

Parameters:

  • t *testing.T
Show/Hide Function Body
{
	mesh := NavMesh{Polygons: map[int]Poly{
		1: {ID: 1, Center: m.Vec2{0, 0}, Neighbors: []int{2}},
		2: {ID: 2, Center: m.Vec2{1, 0}, Neighbors: []int{1, 3}},
		3: {ID: 3, Center: m.Vec2{2, 0}, Neighbors: []int{2}},
	}}
	pf := New()
	_, ch := pf.RequestNavMeshPath(context.Background(), mesh, 1, 3)
	res := <-ch
	if !res.Found {
		t.Fatalf("expected path, got err %v", res.Err)
	}
	if len(res.Path) != 3 || res.Path[0] != 1 || res.Path[2] != 3 {
		t.Fatalf("unexpected path %v", res.Path)
	}
}

TestCancelGridPath function

Parameters:

  • t *testing.T
Show/Hide Function Body
{
	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)
	}
}

container/heap import

Import example:

import "container/heap"

context import

Import example:

import "context"

errors import

Import example:

import "errors"

math import

Import example:

import "math"

sync import

Import example:

import "sync"

github.com/rfwlab/rfw/v1/math import

Import example:

import "github.com/rfwlab/rfw/v1/math"

Imported as:

m

context import

Import example:

import "context"

testing import

Import example:

import "testing"

github.com/rfwlab/rfw/v1/math import

Import example:

import "github.com/rfwlab/rfw/v1/math"

Imported as:

m