300 lines
6.7 KiB
Markdown
300 lines
6.7 KiB
Markdown
|
|
# Pathfinding System
|
|||
|
|
|
|||
|
|
`@esengine/pathfinding` provides a complete 2D pathfinding solution including A* algorithm, grid maps, navigation meshes, and path smoothing.
|
|||
|
|
|
|||
|
|
## Installation
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
npm install @esengine/pathfinding
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Quick Start
|
|||
|
|
|
|||
|
|
### Grid Map Pathfinding
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
import { createGridMap, createAStarPathfinder } from '@esengine/pathfinding';
|
|||
|
|
|
|||
|
|
// Create 20x20 grid
|
|||
|
|
const grid = createGridMap(20, 20);
|
|||
|
|
|
|||
|
|
// Set obstacles
|
|||
|
|
grid.setWalkable(5, 5, false);
|
|||
|
|
grid.setWalkable(5, 6, false);
|
|||
|
|
|
|||
|
|
// Create pathfinder
|
|||
|
|
const pathfinder = createAStarPathfinder(grid);
|
|||
|
|
|
|||
|
|
// Find path
|
|||
|
|
const result = pathfinder.findPath(0, 0, 15, 15);
|
|||
|
|
|
|||
|
|
if (result.found) {
|
|||
|
|
console.log('Path found!');
|
|||
|
|
console.log('Path:', result.path);
|
|||
|
|
console.log('Cost:', result.cost);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### NavMesh Pathfinding
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
import { createNavMesh } from '@esengine/pathfinding';
|
|||
|
|
|
|||
|
|
const navmesh = createNavMesh();
|
|||
|
|
|
|||
|
|
// Add polygon areas
|
|||
|
|
navmesh.addPolygon([
|
|||
|
|
{ x: 0, y: 0 }, { x: 10, y: 0 },
|
|||
|
|
{ x: 10, y: 10 }, { x: 0, y: 10 }
|
|||
|
|
]);
|
|||
|
|
|
|||
|
|
navmesh.addPolygon([
|
|||
|
|
{ x: 10, y: 0 }, { x: 20, y: 0 },
|
|||
|
|
{ x: 20, y: 10 }, { x: 10, y: 10 }
|
|||
|
|
]);
|
|||
|
|
|
|||
|
|
// Auto-build connections
|
|||
|
|
navmesh.build();
|
|||
|
|
|
|||
|
|
// Find path
|
|||
|
|
const result = navmesh.findPath(1, 1, 18, 8);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Core Concepts
|
|||
|
|
|
|||
|
|
### IPathResult
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
interface IPathResult {
|
|||
|
|
readonly found: boolean; // Path found
|
|||
|
|
readonly path: readonly IPoint[];// Path points
|
|||
|
|
readonly cost: number; // Total cost
|
|||
|
|
readonly nodesSearched: number; // Nodes searched
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### IPathfindingOptions
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
interface IPathfindingOptions {
|
|||
|
|
maxNodes?: number; // Max search nodes (default 10000)
|
|||
|
|
heuristicWeight?: number; // Heuristic weight (>1 faster but may be suboptimal)
|
|||
|
|
allowDiagonal?: boolean; // Allow diagonal movement (default true)
|
|||
|
|
avoidCorners?: boolean; // Avoid corner cutting (default true)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Heuristic Functions
|
|||
|
|
|
|||
|
|
| Function | Use Case | Description |
|
|||
|
|
|----------|----------|-------------|
|
|||
|
|
| `manhattanDistance` | 4-directional | Manhattan distance |
|
|||
|
|
| `euclideanDistance` | Any direction | Euclidean distance |
|
|||
|
|
| `chebyshevDistance` | 8-directional | Diagonal cost = 1 |
|
|||
|
|
| `octileDistance` | 8-directional | Diagonal cost = √2 (default) |
|
|||
|
|
|
|||
|
|
## Grid Map API
|
|||
|
|
|
|||
|
|
### createGridMap
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
function createGridMap(
|
|||
|
|
width: number,
|
|||
|
|
height: number,
|
|||
|
|
options?: IGridMapOptions
|
|||
|
|
): GridMap
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Options:**
|
|||
|
|
|
|||
|
|
| Property | Type | Default | Description |
|
|||
|
|
|----------|------|---------|-------------|
|
|||
|
|
| `allowDiagonal` | `boolean` | `true` | Allow diagonal movement |
|
|||
|
|
| `diagonalCost` | `number` | `√2` | Diagonal movement cost |
|
|||
|
|
| `avoidCorners` | `boolean` | `true` | Avoid corner cutting |
|
|||
|
|
| `heuristic` | `HeuristicFunction` | `octileDistance` | Heuristic function |
|
|||
|
|
|
|||
|
|
### Map Operations
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// Check/set walkability
|
|||
|
|
grid.isWalkable(x, y);
|
|||
|
|
grid.setWalkable(x, y, false);
|
|||
|
|
|
|||
|
|
// Set movement cost (e.g., swamp, sand)
|
|||
|
|
grid.setCost(x, y, 2);
|
|||
|
|
|
|||
|
|
// Set rectangle region
|
|||
|
|
grid.setRectWalkable(0, 0, 5, 5, false);
|
|||
|
|
|
|||
|
|
// Load from array (0=walkable, non-0=blocked)
|
|||
|
|
grid.loadFromArray([
|
|||
|
|
[0, 0, 0, 1, 0],
|
|||
|
|
[0, 1, 0, 1, 0]
|
|||
|
|
]);
|
|||
|
|
|
|||
|
|
// Load from string (.=walkable, #=blocked)
|
|||
|
|
grid.loadFromString(`
|
|||
|
|
.....
|
|||
|
|
.#.#.
|
|||
|
|
`);
|
|||
|
|
|
|||
|
|
// Export and reset
|
|||
|
|
console.log(grid.toString());
|
|||
|
|
grid.reset();
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## A* Pathfinder API
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
const pathfinder = createAStarPathfinder(grid);
|
|||
|
|
|
|||
|
|
const result = pathfinder.findPath(
|
|||
|
|
startX, startY,
|
|||
|
|
endX, endY,
|
|||
|
|
{ maxNodes: 5000, heuristicWeight: 1.5 }
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// Pathfinder is reusable
|
|||
|
|
pathfinder.findPath(0, 0, 10, 10);
|
|||
|
|
pathfinder.findPath(5, 5, 15, 15);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## NavMesh API
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
const navmesh = createNavMesh();
|
|||
|
|
|
|||
|
|
// Add convex polygons
|
|||
|
|
const id1 = navmesh.addPolygon(vertices1);
|
|||
|
|
const id2 = navmesh.addPolygon(vertices2);
|
|||
|
|
|
|||
|
|
// Auto-detect shared edges
|
|||
|
|
navmesh.build();
|
|||
|
|
|
|||
|
|
// Or manually set connections
|
|||
|
|
navmesh.setConnection(id1, id2, {
|
|||
|
|
left: { x: 10, y: 0 },
|
|||
|
|
right: { x: 10, y: 10 }
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Query and pathfind
|
|||
|
|
const polygon = navmesh.findPolygonAt(5, 5);
|
|||
|
|
navmesh.isWalkable(5, 5);
|
|||
|
|
const result = navmesh.findPath(1, 1, 18, 8);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Path Smoothing
|
|||
|
|
|
|||
|
|
### Line of Sight Smoothing
|
|||
|
|
|
|||
|
|
Remove unnecessary waypoints:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
import { createLineOfSightSmoother } from '@esengine/pathfinding';
|
|||
|
|
|
|||
|
|
const smoother = createLineOfSightSmoother();
|
|||
|
|
const smoothedPath = smoother.smooth(result.path, grid);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Curve Smoothing
|
|||
|
|
|
|||
|
|
Catmull-Rom spline:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
import { createCatmullRomSmoother } from '@esengine/pathfinding';
|
|||
|
|
|
|||
|
|
const smoother = createCatmullRomSmoother(5, 0.5);
|
|||
|
|
const curvedPath = smoother.smooth(result.path, grid);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Combined Smoothing
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
import { createCombinedSmoother } from '@esengine/pathfinding';
|
|||
|
|
|
|||
|
|
const smoother = createCombinedSmoother(5, 0.5);
|
|||
|
|
const finalPath = smoother.smooth(result.path, grid);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Line of Sight Functions
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
import { bresenhamLineOfSight, raycastLineOfSight } from '@esengine/pathfinding';
|
|||
|
|
|
|||
|
|
const hasLOS = bresenhamLineOfSight(x1, y1, x2, y2, grid);
|
|||
|
|
const hasLOS2 = raycastLineOfSight(x1, y1, x2, y2, grid, 0.5);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Practical Examples
|
|||
|
|
|
|||
|
|
### Dynamic Obstacles
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
class DynamicPathfinding {
|
|||
|
|
private grid: GridMap;
|
|||
|
|
private pathfinder: AStarPathfinder;
|
|||
|
|
private dynamicObstacles: Set<string> = new Set();
|
|||
|
|
|
|||
|
|
addDynamicObstacle(x: number, y: number): void {
|
|||
|
|
this.dynamicObstacles.add(`${x},${y}`);
|
|||
|
|
this.grid.setWalkable(x, y, false);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
removeDynamicObstacle(x: number, y: number): void {
|
|||
|
|
this.dynamicObstacles.delete(`${x},${y}`);
|
|||
|
|
this.grid.setWalkable(x, y, true);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Terrain Costs
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
const grid = createGridMap(50, 50);
|
|||
|
|
|
|||
|
|
// Normal ground - cost 1 (default)
|
|||
|
|
// Sand - cost 2
|
|||
|
|
for (let y = 10; y < 20; y++) {
|
|||
|
|
for (let x = 0; x < 50; x++) {
|
|||
|
|
grid.setCost(x, y, 2);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Swamp - cost 4
|
|||
|
|
for (let y = 30; y < 35; y++) {
|
|||
|
|
for (let x = 20; x < 30; x++) {
|
|||
|
|
grid.setCost(x, y, 4);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Blueprint Nodes
|
|||
|
|
|
|||
|
|
- `FindPath` - Find path
|
|||
|
|
- `FindPathSmooth` - Find and smooth path
|
|||
|
|
- `IsWalkable` - Check walkability
|
|||
|
|
- `GetPathLength` - Get path point count
|
|||
|
|
- `GetPathDistance` - Get total path distance
|
|||
|
|
- `GetPathPoint` - Get specific path point
|
|||
|
|
- `MoveAlongPath` - Move along path
|
|||
|
|
- `HasLineOfSight` - Check line of sight
|
|||
|
|
|
|||
|
|
## Performance Tips
|
|||
|
|
|
|||
|
|
1. **Limit search range**: `{ maxNodes: 1000 }`
|
|||
|
|
2. **Use heuristic weight**: `{ heuristicWeight: 1.5 }` (faster but may not be optimal)
|
|||
|
|
3. **Reuse pathfinder instances**
|
|||
|
|
4. **Use NavMesh for complex terrain**
|
|||
|
|
5. **Choose appropriate heuristic for movement type**
|
|||
|
|
|
|||
|
|
## Grid vs NavMesh
|
|||
|
|
|
|||
|
|
| Feature | GridMap | NavMesh |
|
|||
|
|
|---------|---------|---------|
|
|||
|
|
| Use Case | Regular tile maps | Complex polygon terrain |
|
|||
|
|
| Memory | Higher (width × height) | Lower (polygon count) |
|
|||
|
|
| Precision | Grid-aligned | Continuous coordinates |
|
|||
|
|
| Dynamic Updates | Easy | Requires rebuild |
|
|||
|
|
| Setup Complexity | Simple | More complex |
|