murow 0.0.42 → 0.0.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/fixed-ticker/fixed-ticker.d.ts +9 -1
- package/dist/core/fixed-ticker/fixed-ticker.js +10 -0
- package/dist/core/navmesh/navmesh-worker-pool.d.ts +53 -0
- package/dist/core/navmesh/navmesh-worker-pool.js +180 -0
- package/dist/core/navmesh/navmesh.d.ts +114 -3
- package/dist/core/navmesh/navmesh.js +136 -3
- package/dist/core/navmesh/navmesh.worker.d.ts +11 -0
- package/dist/core/navmesh/navmesh.worker.js +79 -0
- package/dist/core/prediction/prediction.d.ts +1 -0
- package/dist/core/prediction/prediction.js +3 -0
- package/dist/core.esm.js +1 -1
- package/dist/core.js +1 -1
- package/dist/ecs/component-store.d.ts +20 -35
- package/dist/ecs/component-store.js +84 -81
- package/dist/ecs/entity-handle.d.ts +209 -0
- package/dist/ecs/entity-handle.js +232 -0
- package/dist/ecs/index.d.ts +1 -0
- package/dist/ecs/index.js +1 -0
- package/dist/ecs/world.d.ts +73 -1
- package/dist/ecs/world.js +203 -64
- package/dist/net/adapters/browser-websocket.d.ts +2 -0
- package/dist/net/adapters/browser-websocket.js +8 -0
- package/dist/net/adapters/bun-websocket.d.ts +2 -0
- package/dist/net/adapters/bun-websocket.js +13 -0
- package/dist/net/client.d.ts +14 -8
- package/dist/net/client.js +50 -13
- package/dist/net/server.d.ts +10 -10
- package/dist/net/server.js +11 -10
- package/dist/net/types.d.ts +27 -0
- package/dist/protocol/rpc/define-rpc.d.ts +4 -4
- package/dist/protocol/rpc/define-rpc.js +3 -3
- package/dist/protocol/rpc/rpc-registry.d.ts +5 -5
- package/dist/protocol/rpc/rpc-registry.js +2 -2
- package/dist/protocol/rpc/rpc.d.ts +3 -3
- package/package.json +1 -1
- package/src/core/fixed-ticker/README.md +4 -4
- package/src/core/fixed-ticker/fixed-ticker.ts +12 -1
- package/src/core/navmesh/README.md +40 -0
- package/src/core/navmesh/navmesh-worker-pool.ts +236 -0
- package/src/core/navmesh/navmesh-workers.test.ts +356 -0
- package/src/core/navmesh/navmesh.ts +206 -9
- package/src/core/navmesh/navmesh.worker.ts +147 -0
- package/src/core/pooled-codec/pooled-codec.test.ts +176 -0
- package/src/core/prediction/prediction.ts +4 -0
- package/src/ecs/README.md +427 -354
- package/src/ecs/benchmark.test.ts +824 -15
- package/src/ecs/component-store.ts +87 -113
- package/src/ecs/entity-handle.test.ts +393 -0
- package/src/ecs/entity-handle.ts +245 -0
- package/src/ecs/index.ts +1 -0
- package/src/ecs/world.test.ts +7 -6
- package/src/ecs/world.ts +242 -62
- package/src/net/README.md +7 -3
- package/src/net/adapters/browser-websocket.ts +9 -1
- package/src/net/adapters/bun-websocket.ts +15 -0
- package/src/net/client.ts +60 -17
- package/src/net/server.ts +15 -14
- package/src/net/types.ts +27 -0
- package/src/protocol/README.md +3 -3
- package/src/protocol/rpc/define-rpc.test.ts +8 -8
- package/src/protocol/rpc/define-rpc.ts +5 -5
- package/src/protocol/rpc/rpc-registry.test.ts +3 -3
- package/src/protocol/rpc/rpc-registry.ts +5 -5
- package/src/protocol/rpc/rpc.ts +3 -3
|
@@ -25,7 +25,7 @@ export declare class FixedTicker {
|
|
|
25
25
|
* @description
|
|
26
26
|
* Interval in milliseconds per tick
|
|
27
27
|
*/
|
|
28
|
-
|
|
28
|
+
intervalMs: number;
|
|
29
29
|
/**
|
|
30
30
|
* @description
|
|
31
31
|
* Maximum amount of ticks to run per frame, to avoid
|
|
@@ -83,6 +83,14 @@ export declare class FixedTicker {
|
|
|
83
83
|
* @returns {number} Accumulated time in seconds
|
|
84
84
|
*/
|
|
85
85
|
get accumulatedTime(): number;
|
|
86
|
+
/**
|
|
87
|
+
* @description
|
|
88
|
+
* Returns the interpolation factor between 0 and 1 for smooth rendering between ticks.
|
|
89
|
+
* Clamped to prevent extrapolation when ticks are skipped.
|
|
90
|
+
*
|
|
91
|
+
* @returns {number} Alpha value between 0 and 1
|
|
92
|
+
*/
|
|
93
|
+
get alpha(): number;
|
|
86
94
|
}
|
|
87
95
|
interface FixedTickerProps {
|
|
88
96
|
/**
|
|
@@ -88,4 +88,14 @@ export class FixedTicker {
|
|
|
88
88
|
get accumulatedTime() {
|
|
89
89
|
return this.accumulator / 1000; // Convert to seconds
|
|
90
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* @description
|
|
93
|
+
* Returns the interpolation factor between 0 and 1 for smooth rendering between ticks.
|
|
94
|
+
* Clamped to prevent extrapolation when ticks are skipped.
|
|
95
|
+
*
|
|
96
|
+
* @returns {number} Alpha value between 0 and 1
|
|
97
|
+
*/
|
|
98
|
+
get alpha() {
|
|
99
|
+
return Math.min(this.accumulatedTime / (1 / this.rate), 1.0);
|
|
100
|
+
}
|
|
91
101
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NavMesh Worker Pool - Manages multiple workers for parallel pathfinding
|
|
3
|
+
*/
|
|
4
|
+
import { ObstacleInput } from './navmesh';
|
|
5
|
+
interface Vec2 {
|
|
6
|
+
x: number;
|
|
7
|
+
y: number;
|
|
8
|
+
}
|
|
9
|
+
export declare class NavMeshWorkerPool {
|
|
10
|
+
private poolSize;
|
|
11
|
+
private workerPath;
|
|
12
|
+
private navType;
|
|
13
|
+
private obstacles;
|
|
14
|
+
private workers;
|
|
15
|
+
private nextWorkerIndex;
|
|
16
|
+
private requestId;
|
|
17
|
+
private pendingRequests;
|
|
18
|
+
private initialized;
|
|
19
|
+
constructor(poolSize: number, workerPath: string, navType?: 'grid' | 'graph', obstacles?: ObstacleInput[]);
|
|
20
|
+
/**
|
|
21
|
+
* Initialize all workers in the pool
|
|
22
|
+
*/
|
|
23
|
+
init(): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Find a path using the next available worker (round-robin)
|
|
26
|
+
*/
|
|
27
|
+
findPath(from: Vec2, to: Vec2): Promise<Vec2[]>;
|
|
28
|
+
/**
|
|
29
|
+
* Add an obstacle to all workers
|
|
30
|
+
*/
|
|
31
|
+
addObstacle(obstacle: ObstacleInput): Promise<number>;
|
|
32
|
+
/**
|
|
33
|
+
* Remove an obstacle from all workers
|
|
34
|
+
*/
|
|
35
|
+
removeObstacle(obstacleId: number): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Move an obstacle in all workers
|
|
38
|
+
*/
|
|
39
|
+
moveObstacle(obstacleId: number, pos: Vec2): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Terminate all workers
|
|
42
|
+
*/
|
|
43
|
+
terminate(): void;
|
|
44
|
+
/**
|
|
45
|
+
* Get the number of pending requests
|
|
46
|
+
*/
|
|
47
|
+
get pendingCount(): number;
|
|
48
|
+
/**
|
|
49
|
+
* Get the pool size
|
|
50
|
+
*/
|
|
51
|
+
get size(): number;
|
|
52
|
+
}
|
|
53
|
+
export {};
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NavMesh Worker Pool - Manages multiple workers for parallel pathfinding
|
|
3
|
+
*/
|
|
4
|
+
export class NavMeshWorkerPool {
|
|
5
|
+
constructor(poolSize, workerPath, navType = 'grid', obstacles = []) {
|
|
6
|
+
this.poolSize = poolSize;
|
|
7
|
+
this.workerPath = workerPath;
|
|
8
|
+
this.navType = navType;
|
|
9
|
+
this.obstacles = obstacles;
|
|
10
|
+
this.workers = [];
|
|
11
|
+
this.nextWorkerIndex = 0;
|
|
12
|
+
this.requestId = 0;
|
|
13
|
+
this.pendingRequests = new Map();
|
|
14
|
+
this.initialized = false;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Initialize all workers in the pool
|
|
18
|
+
*/
|
|
19
|
+
async init() {
|
|
20
|
+
if (this.initialized)
|
|
21
|
+
return;
|
|
22
|
+
const initPromises = [];
|
|
23
|
+
for (let i = 0; i < this.poolSize; i++) {
|
|
24
|
+
const worker = new Worker(this.workerPath, { type: 'module' });
|
|
25
|
+
// Set up message handler
|
|
26
|
+
worker.onmessage = (e) => {
|
|
27
|
+
const msg = e.data;
|
|
28
|
+
if (msg.type === 'PATH_RESULT') {
|
|
29
|
+
const request = this.pendingRequests.get(msg.id);
|
|
30
|
+
if (request) {
|
|
31
|
+
request.resolve(msg.path);
|
|
32
|
+
this.pendingRequests.delete(msg.id);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
else if (msg.type === 'ERROR') {
|
|
36
|
+
// Reject all pending requests for this worker
|
|
37
|
+
for (const [id, request] of this.pendingRequests.entries()) {
|
|
38
|
+
request.reject(new Error(msg.error));
|
|
39
|
+
this.pendingRequests.delete(id);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
worker.onerror = (error) => {
|
|
44
|
+
console.error('Worker error:', error);
|
|
45
|
+
// Reject all pending requests for this worker
|
|
46
|
+
for (const [id, request] of this.pendingRequests.entries()) {
|
|
47
|
+
request.reject(new Error('Worker error'));
|
|
48
|
+
this.pendingRequests.delete(id);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
this.workers.push(worker);
|
|
52
|
+
// Initialize worker
|
|
53
|
+
const readyPromise = new Promise((resolve) => {
|
|
54
|
+
const onReady = (e) => {
|
|
55
|
+
if (e.data.type === 'READY') {
|
|
56
|
+
worker.removeEventListener('message', onReady);
|
|
57
|
+
resolve();
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
worker.addEventListener('message', onReady);
|
|
61
|
+
});
|
|
62
|
+
worker.postMessage({
|
|
63
|
+
type: 'INIT',
|
|
64
|
+
navType: this.navType,
|
|
65
|
+
obstacles: this.obstacles,
|
|
66
|
+
});
|
|
67
|
+
initPromises.push(readyPromise);
|
|
68
|
+
}
|
|
69
|
+
await Promise.all(initPromises);
|
|
70
|
+
this.initialized = true;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Find a path using the next available worker (round-robin)
|
|
74
|
+
*/
|
|
75
|
+
async findPath(from, to) {
|
|
76
|
+
if (!this.initialized) {
|
|
77
|
+
throw new Error('Worker pool not initialized. Call init() first.');
|
|
78
|
+
}
|
|
79
|
+
const id = this.requestId++;
|
|
80
|
+
const worker = this.workers[this.nextWorkerIndex];
|
|
81
|
+
this.nextWorkerIndex = (this.nextWorkerIndex + 1) % this.workers.length;
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
this.pendingRequests.set(id, { id, from, to, resolve, reject });
|
|
84
|
+
worker.postMessage({
|
|
85
|
+
type: 'FIND_PATH',
|
|
86
|
+
id,
|
|
87
|
+
from,
|
|
88
|
+
to,
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Add an obstacle to all workers
|
|
94
|
+
*/
|
|
95
|
+
async addObstacle(obstacle) {
|
|
96
|
+
if (!this.initialized) {
|
|
97
|
+
throw new Error('Worker pool not initialized. Call init() first.');
|
|
98
|
+
}
|
|
99
|
+
// Add to all workers
|
|
100
|
+
const promises = this.workers.map((worker) => {
|
|
101
|
+
return new Promise((resolve) => {
|
|
102
|
+
const onAdded = (e) => {
|
|
103
|
+
if (e.data.type === 'OBSTACLE_ADDED') {
|
|
104
|
+
worker.removeEventListener('message', onAdded);
|
|
105
|
+
resolve(e.data.obstacleId);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
worker.addEventListener('message', onAdded);
|
|
109
|
+
worker.postMessage({ type: 'ADD_OBSTACLE', obstacle });
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
const ids = await Promise.all(promises);
|
|
113
|
+
return ids[0]; // All workers should return the same ID
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Remove an obstacle from all workers
|
|
117
|
+
*/
|
|
118
|
+
async removeObstacle(obstacleId) {
|
|
119
|
+
if (!this.initialized) {
|
|
120
|
+
throw new Error('Worker pool not initialized. Call init() first.');
|
|
121
|
+
}
|
|
122
|
+
const promises = this.workers.map((worker) => {
|
|
123
|
+
return new Promise((resolve) => {
|
|
124
|
+
const onRemoved = (e) => {
|
|
125
|
+
if (e.data.type === 'OBSTACLE_REMOVED') {
|
|
126
|
+
worker.removeEventListener('message', onRemoved);
|
|
127
|
+
resolve();
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
worker.addEventListener('message', onRemoved);
|
|
131
|
+
worker.postMessage({ type: 'REMOVE_OBSTACLE', obstacleId });
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
await Promise.all(promises);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Move an obstacle in all workers
|
|
138
|
+
*/
|
|
139
|
+
async moveObstacle(obstacleId, pos) {
|
|
140
|
+
if (!this.initialized) {
|
|
141
|
+
throw new Error('Worker pool not initialized. Call init() first.');
|
|
142
|
+
}
|
|
143
|
+
const promises = this.workers.map((worker) => {
|
|
144
|
+
return new Promise((resolve) => {
|
|
145
|
+
const onMoved = (e) => {
|
|
146
|
+
if (e.data.type === 'OBSTACLE_MOVED') {
|
|
147
|
+
worker.removeEventListener('message', onMoved);
|
|
148
|
+
resolve();
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
worker.addEventListener('message', onMoved);
|
|
152
|
+
worker.postMessage({ type: 'MOVE_OBSTACLE', obstacleId, pos });
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
await Promise.all(promises);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Terminate all workers
|
|
159
|
+
*/
|
|
160
|
+
terminate() {
|
|
161
|
+
for (const worker of this.workers) {
|
|
162
|
+
worker.terminate();
|
|
163
|
+
}
|
|
164
|
+
this.workers = [];
|
|
165
|
+
this.pendingRequests.clear();
|
|
166
|
+
this.initialized = false;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Get the number of pending requests
|
|
170
|
+
*/
|
|
171
|
+
get pendingCount() {
|
|
172
|
+
return this.pendingRequests.size;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Get the pool size
|
|
176
|
+
*/
|
|
177
|
+
get size() {
|
|
178
|
+
return this.workers.length;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
type ObstacleId = number;
|
|
2
2
|
export type Obstacle = CircleObstacle | RectObstacle | PolygonObstacle;
|
|
3
|
+
/**
|
|
4
|
+
* NavMesh configuration options
|
|
5
|
+
*/
|
|
6
|
+
export interface NavMeshOptions<TWorkers extends boolean | 'auto' = false> {
|
|
7
|
+
/**
|
|
8
|
+
* Enable Web Workers for pathfinding
|
|
9
|
+
*
|
|
10
|
+
* - `false` (default): Synchronous pathfinding on main thread
|
|
11
|
+
* - `true`: Always use worker pool (4 workers)
|
|
12
|
+
* - `'auto'`: Automatically use workers when beneficial (>= 20 pending paths)
|
|
13
|
+
*
|
|
14
|
+
* @default false
|
|
15
|
+
*
|
|
16
|
+
* @remarks
|
|
17
|
+
* Workers provide 3-4.5x speedup for parallel pathfinding (20+ concurrent requests).
|
|
18
|
+
* For single/sequential pathfinding, sync is faster due to message passing overhead (~0.5ms).
|
|
19
|
+
*
|
|
20
|
+
* Use 'auto' for games where pathfinding load varies (e.g., RTS with unit groups).
|
|
21
|
+
*/
|
|
22
|
+
workers?: TWorkers;
|
|
23
|
+
/**
|
|
24
|
+
* Number of workers to spawn (only used when workers = true)
|
|
25
|
+
* @default 4
|
|
26
|
+
*/
|
|
27
|
+
workerPoolSize?: number;
|
|
28
|
+
/**
|
|
29
|
+
* Path to worker script (required if workers = true and running in browser)
|
|
30
|
+
* For Node.js/Bun, this is handled automatically
|
|
31
|
+
* @example './navmesh.worker.js'
|
|
32
|
+
*/
|
|
33
|
+
workerPath?: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Helper type to determine return type based on worker configuration
|
|
37
|
+
*/
|
|
38
|
+
type PathResult<TWorkers extends boolean | 'auto' | undefined> = TWorkers extends false | undefined ? Vec2[] : TWorkers extends true ? Promise<Vec2[]> : Vec2[] | Promise<Vec2[]>;
|
|
3
39
|
export type ObstacleInput = Omit<CircleObstacle, 'id'> | Omit<RectObstacle, 'id'> | Omit<PolygonObstacle, 'id'>;
|
|
4
40
|
interface Vec2 {
|
|
5
41
|
x: number;
|
|
@@ -62,25 +98,54 @@ type NavType = 'grid' | 'graph';
|
|
|
62
98
|
* - Version tracking: zero unnecessary rebuilds
|
|
63
99
|
* - Binary heap A*: handles 10k+ node searches
|
|
64
100
|
* - Simple full rebuild: correct and fast enough
|
|
101
|
+
* - Optional Web Workers: 3-4.5x speedup for parallel pathfinding
|
|
65
102
|
*
|
|
66
103
|
* Performance characteristics:
|
|
67
104
|
* - Obstacle query: O(1) average via spatial hash
|
|
68
105
|
* - Grid rebuild: O(n * area), < 1ms for typical games
|
|
69
106
|
* - Pathfinding: O(b^d * log n) with binary heap
|
|
107
|
+
* - Worker overhead: ~0.5ms per request (use for 20+ concurrent paths)
|
|
70
108
|
*
|
|
71
109
|
* Production ready for:
|
|
72
110
|
* - Grid-based games
|
|
73
111
|
* - RTS with < 1000 dynamic obstacles
|
|
74
112
|
* - Turn-based games
|
|
75
113
|
* - Moderate map sizes (< 1M cells)
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```ts
|
|
117
|
+
* // Simple usage (synchronous) - typed as Vec2[]
|
|
118
|
+
* const navmesh = new NavMesh('grid');
|
|
119
|
+
* const path = navmesh.findPath({ from: {x:0, y:0}, to: {x:10, y:10} });
|
|
120
|
+
*
|
|
121
|
+
* // With workers (automatic) - typed as Vec2[] | Promise<Vec2[]>
|
|
122
|
+
* const navmesh = new NavMesh('grid', { workers: 'auto' });
|
|
123
|
+
* const path = await navmesh.findPath({ from: {x:0, y:0}, to: {x:10, y:10} });
|
|
124
|
+
*
|
|
125
|
+
* // With workers (always) - typed as Promise<Vec2[]>
|
|
126
|
+
* const navmesh = new NavMesh('grid', { workers: true });
|
|
127
|
+
* const path = await navmesh.findPath({ from: {x:0, y:0}, to: {x:10, y:10} });
|
|
128
|
+
* ```
|
|
76
129
|
*/
|
|
77
|
-
export declare class NavMesh {
|
|
130
|
+
export declare class NavMesh<TWorkers extends boolean | 'auto' = false> {
|
|
78
131
|
private type;
|
|
79
132
|
private grid?;
|
|
80
133
|
private graph?;
|
|
81
134
|
private lastVersion;
|
|
82
135
|
obstacles: Obstacles;
|
|
83
|
-
|
|
136
|
+
private options;
|
|
137
|
+
private workerPool?;
|
|
138
|
+
private pendingPaths;
|
|
139
|
+
private readonly AUTO_WORKER_THRESHOLD;
|
|
140
|
+
constructor(type: NavType, options?: NavMeshOptions<TWorkers>);
|
|
141
|
+
/**
|
|
142
|
+
* Lazy initialize worker pool
|
|
143
|
+
*/
|
|
144
|
+
private initWorkerPool;
|
|
145
|
+
/**
|
|
146
|
+
* Check if we should use workers for this request
|
|
147
|
+
*/
|
|
148
|
+
private shouldUseWorkers;
|
|
84
149
|
/**
|
|
85
150
|
* Adds an obstacle and returns its unique ID.
|
|
86
151
|
* For polygons: ensure points are defined relative to (0,0).
|
|
@@ -102,15 +167,61 @@ export declare class NavMesh {
|
|
|
102
167
|
* Finds a path from start to goal.
|
|
103
168
|
* Automatically rebuilds navigation data if obstacles changed.
|
|
104
169
|
* Returns empty array if no path exists.
|
|
170
|
+
*
|
|
171
|
+
* @remarks
|
|
172
|
+
* - If workers are disabled (false): Returns Vec2[] synchronously
|
|
173
|
+
* - If workers are enabled (true): Returns Promise<Vec2[]>
|
|
174
|
+
* - If workers are 'auto': Returns Vec2[] | Promise<Vec2[]> based on load
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* ```ts
|
|
178
|
+
* // Synchronous (no workers) - typed as Vec2[]
|
|
179
|
+
* const navmesh = new NavMesh('grid');
|
|
180
|
+
* const path = navmesh.findPath({ from, to });
|
|
181
|
+
*
|
|
182
|
+
* // Asynchronous (with workers) - typed as Promise<Vec2[]>
|
|
183
|
+
* const navmesh = new NavMesh('grid', { workers: true });
|
|
184
|
+
* const path = await navmesh.findPath({ from, to });
|
|
185
|
+
*
|
|
186
|
+
* // Auto mode - typed as Vec2[] | Promise<Vec2[]>
|
|
187
|
+
* const navmesh = new NavMesh('grid', { workers: 'auto' });
|
|
188
|
+
* const result = navmesh.findPath({ from, to });
|
|
189
|
+
* const path = result instanceof Promise ? await result : result;
|
|
190
|
+
* ```
|
|
105
191
|
*/
|
|
106
192
|
findPath({ from, to }: {
|
|
107
193
|
from: Vec2;
|
|
108
194
|
to: Vec2;
|
|
109
|
-
}):
|
|
195
|
+
}): PathResult<TWorkers>;
|
|
196
|
+
/**
|
|
197
|
+
* Async pathfinding using worker pool
|
|
198
|
+
*/
|
|
199
|
+
private findPathAsync;
|
|
110
200
|
/**
|
|
111
201
|
* Smart rebuild - only rebuilds if obstacles changed.
|
|
112
202
|
* Version checking eliminates unnecessary work.
|
|
113
203
|
*/
|
|
114
204
|
rebuild(): void;
|
|
205
|
+
/**
|
|
206
|
+
* Cleanup resources (terminate worker pool if active)
|
|
207
|
+
* Call this when you're done with the NavMesh instance.
|
|
208
|
+
*
|
|
209
|
+
* @example
|
|
210
|
+
* ```ts
|
|
211
|
+
* const navmesh = new NavMesh('grid', { workers: true });
|
|
212
|
+
* // ... use navmesh ...
|
|
213
|
+
* navmesh.dispose(); // Cleanup workers
|
|
214
|
+
* ```
|
|
215
|
+
*/
|
|
216
|
+
dispose(): void;
|
|
217
|
+
/**
|
|
218
|
+
* Get current worker status for debugging/monitoring
|
|
219
|
+
*/
|
|
220
|
+
getWorkerStatus(): {
|
|
221
|
+
workersEnabled: TWorkers;
|
|
222
|
+
workerPoolActive: boolean;
|
|
223
|
+
pendingPaths: number;
|
|
224
|
+
usingWorkersNow: boolean;
|
|
225
|
+
};
|
|
115
226
|
}
|
|
116
227
|
export {};
|
|
@@ -524,27 +524,85 @@ class GraphNav {
|
|
|
524
524
|
* - Version tracking: zero unnecessary rebuilds
|
|
525
525
|
* - Binary heap A*: handles 10k+ node searches
|
|
526
526
|
* - Simple full rebuild: correct and fast enough
|
|
527
|
+
* - Optional Web Workers: 3-4.5x speedup for parallel pathfinding
|
|
527
528
|
*
|
|
528
529
|
* Performance characteristics:
|
|
529
530
|
* - Obstacle query: O(1) average via spatial hash
|
|
530
531
|
* - Grid rebuild: O(n * area), < 1ms for typical games
|
|
531
532
|
* - Pathfinding: O(b^d * log n) with binary heap
|
|
533
|
+
* - Worker overhead: ~0.5ms per request (use for 20+ concurrent paths)
|
|
532
534
|
*
|
|
533
535
|
* Production ready for:
|
|
534
536
|
* - Grid-based games
|
|
535
537
|
* - RTS with < 1000 dynamic obstacles
|
|
536
538
|
* - Turn-based games
|
|
537
539
|
* - Moderate map sizes (< 1M cells)
|
|
540
|
+
*
|
|
541
|
+
* @example
|
|
542
|
+
* ```ts
|
|
543
|
+
* // Simple usage (synchronous) - typed as Vec2[]
|
|
544
|
+
* const navmesh = new NavMesh('grid');
|
|
545
|
+
* const path = navmesh.findPath({ from: {x:0, y:0}, to: {x:10, y:10} });
|
|
546
|
+
*
|
|
547
|
+
* // With workers (automatic) - typed as Vec2[] | Promise<Vec2[]>
|
|
548
|
+
* const navmesh = new NavMesh('grid', { workers: 'auto' });
|
|
549
|
+
* const path = await navmesh.findPath({ from: {x:0, y:0}, to: {x:10, y:10} });
|
|
550
|
+
*
|
|
551
|
+
* // With workers (always) - typed as Promise<Vec2[]>
|
|
552
|
+
* const navmesh = new NavMesh('grid', { workers: true });
|
|
553
|
+
* const path = await navmesh.findPath({ from: {x:0, y:0}, to: {x:10, y:10} });
|
|
554
|
+
* ```
|
|
538
555
|
*/
|
|
539
556
|
export class NavMesh {
|
|
540
|
-
constructor(type) {
|
|
557
|
+
constructor(type, options) {
|
|
541
558
|
this.type = type;
|
|
542
559
|
this.lastVersion = -1;
|
|
560
|
+
this.pendingPaths = 0;
|
|
561
|
+
this.AUTO_WORKER_THRESHOLD = 20; // Use workers when >= 20 pending paths
|
|
543
562
|
this.obstacles = new Obstacles();
|
|
563
|
+
// Set defaults - cast to any to avoid complex type gymnastics
|
|
564
|
+
this.options = {
|
|
565
|
+
workers: (options?.workers ?? false),
|
|
566
|
+
workerPoolSize: options?.workerPoolSize ?? 4,
|
|
567
|
+
workerPath: options?.workerPath ?? './navmesh.worker.js',
|
|
568
|
+
};
|
|
569
|
+
// Initialize sync navigation
|
|
544
570
|
if (type === 'grid')
|
|
545
571
|
this.grid = new GridNav(this.obstacles);
|
|
546
572
|
if (type === 'graph')
|
|
547
573
|
this.graph = new GraphNav(this.obstacles);
|
|
574
|
+
// Initialize worker pool if workers = true
|
|
575
|
+
if (this.options.workers === true) {
|
|
576
|
+
this.initWorkerPool();
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Lazy initialize worker pool
|
|
581
|
+
*/
|
|
582
|
+
async initWorkerPool() {
|
|
583
|
+
if (this.workerPool)
|
|
584
|
+
return;
|
|
585
|
+
try {
|
|
586
|
+
// Dynamic import to avoid bundling worker pool if not needed
|
|
587
|
+
const { NavMeshWorkerPool } = await import('./navmesh-worker-pool');
|
|
588
|
+
this.workerPool = new NavMeshWorkerPool(this.options.workerPoolSize, this.options.workerPath, this.type, this.obstacles.values);
|
|
589
|
+
await this.workerPool.init();
|
|
590
|
+
}
|
|
591
|
+
catch (error) {
|
|
592
|
+
console.warn('Failed to initialize worker pool, falling back to sync:', error);
|
|
593
|
+
this.options.workers = false; // Disable workers on failure
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Check if we should use workers for this request
|
|
598
|
+
*/
|
|
599
|
+
shouldUseWorkers() {
|
|
600
|
+
if (this.options.workers === false)
|
|
601
|
+
return false;
|
|
602
|
+
if (this.options.workers === true)
|
|
603
|
+
return true;
|
|
604
|
+
// Auto mode: use workers if we have many pending paths
|
|
605
|
+
return this.pendingPaths >= this.AUTO_WORKER_THRESHOLD;
|
|
548
606
|
}
|
|
549
607
|
/**
|
|
550
608
|
* Adds an obstacle and returns its unique ID.
|
|
@@ -575,12 +633,59 @@ export class NavMesh {
|
|
|
575
633
|
* Finds a path from start to goal.
|
|
576
634
|
* Automatically rebuilds navigation data if obstacles changed.
|
|
577
635
|
* Returns empty array if no path exists.
|
|
636
|
+
*
|
|
637
|
+
* @remarks
|
|
638
|
+
* - If workers are disabled (false): Returns Vec2[] synchronously
|
|
639
|
+
* - If workers are enabled (true): Returns Promise<Vec2[]>
|
|
640
|
+
* - If workers are 'auto': Returns Vec2[] | Promise<Vec2[]> based on load
|
|
641
|
+
*
|
|
642
|
+
* @example
|
|
643
|
+
* ```ts
|
|
644
|
+
* // Synchronous (no workers) - typed as Vec2[]
|
|
645
|
+
* const navmesh = new NavMesh('grid');
|
|
646
|
+
* const path = navmesh.findPath({ from, to });
|
|
647
|
+
*
|
|
648
|
+
* // Asynchronous (with workers) - typed as Promise<Vec2[]>
|
|
649
|
+
* const navmesh = new NavMesh('grid', { workers: true });
|
|
650
|
+
* const path = await navmesh.findPath({ from, to });
|
|
651
|
+
*
|
|
652
|
+
* // Auto mode - typed as Vec2[] | Promise<Vec2[]>
|
|
653
|
+
* const navmesh = new NavMesh('grid', { workers: 'auto' });
|
|
654
|
+
* const result = navmesh.findPath({ from, to });
|
|
655
|
+
* const path = result instanceof Promise ? await result : result;
|
|
656
|
+
* ```
|
|
578
657
|
*/
|
|
579
658
|
findPath({ from, to }) {
|
|
659
|
+
// Check if we should use workers
|
|
660
|
+
if (this.shouldUseWorkers() && this.workerPool) {
|
|
661
|
+
// Async path (with workers)
|
|
662
|
+
this.pendingPaths++;
|
|
663
|
+
return this.findPathAsync(from, to).finally(() => {
|
|
664
|
+
this.pendingPaths--;
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
// Sync path (no workers)
|
|
580
668
|
this.rebuild();
|
|
581
|
-
return this.type === 'grid'
|
|
669
|
+
return (this.type === 'grid'
|
|
582
670
|
? this.grid.findPath(from, to)
|
|
583
|
-
: this.graph.findPath(from, to);
|
|
671
|
+
: this.graph.findPath(from, to));
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Async pathfinding using worker pool
|
|
675
|
+
*/
|
|
676
|
+
async findPathAsync(from, to) {
|
|
677
|
+
// Lazy init for 'auto' mode
|
|
678
|
+
if (this.options.workers === 'auto' && !this.workerPool) {
|
|
679
|
+
await this.initWorkerPool();
|
|
680
|
+
}
|
|
681
|
+
if (!this.workerPool) {
|
|
682
|
+
// Fallback to sync if workers failed to init
|
|
683
|
+
this.rebuild();
|
|
684
|
+
return this.type === 'grid'
|
|
685
|
+
? this.grid.findPath(from, to)
|
|
686
|
+
: this.graph.findPath(from, to);
|
|
687
|
+
}
|
|
688
|
+
return this.workerPool.findPath(from, to);
|
|
584
689
|
}
|
|
585
690
|
/**
|
|
586
691
|
* Smart rebuild - only rebuilds if obstacles changed.
|
|
@@ -593,6 +698,34 @@ export class NavMesh {
|
|
|
593
698
|
this.graph?.rebuild();
|
|
594
699
|
this.lastVersion = this.obstacles.version;
|
|
595
700
|
}
|
|
701
|
+
/**
|
|
702
|
+
* Cleanup resources (terminate worker pool if active)
|
|
703
|
+
* Call this when you're done with the NavMesh instance.
|
|
704
|
+
*
|
|
705
|
+
* @example
|
|
706
|
+
* ```ts
|
|
707
|
+
* const navmesh = new NavMesh('grid', { workers: true });
|
|
708
|
+
* // ... use navmesh ...
|
|
709
|
+
* navmesh.dispose(); // Cleanup workers
|
|
710
|
+
* ```
|
|
711
|
+
*/
|
|
712
|
+
dispose() {
|
|
713
|
+
if (this.workerPool) {
|
|
714
|
+
this.workerPool.terminate();
|
|
715
|
+
this.workerPool = undefined;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Get current worker status for debugging/monitoring
|
|
720
|
+
*/
|
|
721
|
+
getWorkerStatus() {
|
|
722
|
+
return {
|
|
723
|
+
workersEnabled: this.options.workers,
|
|
724
|
+
workerPoolActive: !!this.workerPool,
|
|
725
|
+
pendingPaths: this.pendingPaths,
|
|
726
|
+
usingWorkersNow: this.shouldUseWorkers(),
|
|
727
|
+
};
|
|
728
|
+
}
|
|
596
729
|
}
|
|
597
730
|
/* ---------------------------------- */
|
|
598
731
|
/* A* */
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NavMesh Worker - Offloads pathfinding to background thread
|
|
3
|
+
*
|
|
4
|
+
* Message Protocol:
|
|
5
|
+
* - INIT: Initialize NavMesh with obstacles
|
|
6
|
+
* - FIND_PATH: Request pathfinding (from, to)
|
|
7
|
+
* - ADD_OBSTACLE: Add obstacle dynamically
|
|
8
|
+
* - REMOVE_OBSTACLE: Remove obstacle dynamically
|
|
9
|
+
* - MOVE_OBSTACLE: Move obstacle
|
|
10
|
+
*/
|
|
11
|
+
export {};
|