@daneren2005/shared-memory-physics 1.8.0 → 1.9.1

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/README.md CHANGED
@@ -7,7 +7,7 @@ reads positions straight off its entities.
7
7
 
8
8
  Examples at https://daneren2005.github.io/shared-memory-physics/
9
9
 
10
- The ECS (`>=1.1.1`) and the shared-memory primitives it is built on are **peer dependencies**: a game must
10
+ The ECS (`>=1.10.0`) and the shared-memory primitives it is built on are **peer dependencies**: a game must
11
11
  register these components against the same copy of the ECS it builds its world with.
12
12
 
13
13
  ```sh
@@ -23,6 +23,7 @@ npm install @daneren2005/shared-memory-physics @daneren2005/shared-memory-ecs @d
23
23
  | `velocity` | – | `velocityX?`, `velocityY?`, `damping?` | `Float32Array` – world units per second |
24
24
  | `body` | `shape?`, `collideCategory?`, `collideMask?`, `sensor?`, `continuousCollisionDetection?`, `blocksPath?` | – | `Uint32Array` – what shape, what collides with |
25
25
  | `interpolation` | `interpolate` | – | `Float32Array` – where to *draw* it |
26
+ | `navigation` | `navigate` | – | `Int32Array` – a goal and the path to it ([Pathfinding](#pathfinding)) |
26
27
 
27
28
  Entity configs are flat and shared across every component, so velocity is keyed as `velocityX` / `velocityY`
28
29
  rather than `x` / `y` (which already belong to the transform). Either velocity axis on its own is enough to
@@ -838,6 +839,11 @@ const clicked = world.findNearestSpatial(mouseX, mouseY, 0);
838
839
  const nearby = world.searchSpatialAround(x, y, 100, 100);
839
840
  ```
840
841
 
842
+ The map defaults to `type: Float64Array`. Entity ids are never reused, so a `Float32` map, which only holds ids
843
+ below 2^24 (~16.7M), runs out after that many entities have *ever* been created. `Float64` covers the whole
844
+ uint32 eid range for about 1.4x the memory per entity. Pass `spatial: { type: Float32Array }` only if a world
845
+ will create fewer entities than that over its lifetime.
846
+
841
847
  `searchSpatial`, `searchSpatialAround`, `findNearestSpatial`, and `findNearbySpatial` return live entities and
842
848
  accept entity filters. Shape-aware axis-aligned bounds are stored: rotated rectangles and capsules occupy the
843
849
  box around their actual outline. A direct transform/body edit outside the physics update must be followed by
@@ -910,9 +916,111 @@ ask it directly rather than building the sort yourself - it settles as soon as t
910
916
  measured to an entity's **box**, not to its centre, so a large target is as near as its nearest edge and
911
917
  `maxDistance` reads as a range past the hull.
912
918
 
919
+ ## Pathfinding
920
+
921
+ Grid pathfinding for movers that need to walk around walls and buildings: an A* over tiles, run on a worker,
922
+ answering requests written into a `navigation` component. Bodies flagged `blocksPath` are the walls. The physics
923
+ still does the moving; the game reads the path back and steers velocity along it.
924
+
925
+ Register `navigationRegistry` beside `physicsRegistry`, and give movers a `navigate: true` tag:
926
+
927
+ ```ts
928
+ import { navigationRegistry, physicsRegistry, PathfindingSystem } from '@daneren2005/shared-memory-physics';
929
+
930
+ const world = new PhysicalWorld({ ...physicsRegistry, ...navigationRegistry, ...gameRegistry });
931
+ world.addSystem(new PathfindingSystem(world, {
932
+ getInitData: () => ({ tilesWide: 128, tilesHigh: 128, cellSize: 32 }),
933
+ getWorker: () => new Worker(new URL('./pathfinding.worker.ts', import.meta.url), { type: 'module' }),
934
+ }));
935
+
936
+ world.loadEntity({ x: 48, y: 48, width: 32, height: 32, blocksPath: true }); // a wall
937
+ const villager = world.loadEntity({ x: 16, y: 16, width: 24, height: 24, navigate: true });
938
+ villager.components.navigation!.navigateTo(400, 300);
939
+ ```
940
+
941
+ The grid's `(0, 0)` is the world's top-left corner and anything off it is blocked. A worker file is the same two
942
+ lines as physics', handing `createEntitySystemWorker` the update the system was given (`pathfindingUpdate` by default).
943
+
944
+ ### The navigation block
945
+
946
+ Everything is in the mover's `navigation` block (an `Int32Array`), so any thread can set a goal or read a path:
947
+
948
+ | Call | Does |
949
+ | --- | --- |
950
+ | `navigateTo(block, x, y)` | Sets the goal, marks the mover active and asks for a path |
951
+ | `setNavigationGoal(block, x, y)` | Moves the goal without searching again; the last leg bends to it |
952
+ | `requestPath(block)` | Searches again for the current goal |
953
+ | `stopNavigating(block)` | Marks the mover inactive, so it is neither searched for nor rechecked |
954
+ | `isPathPending(block)` / `isUnreachable(block)` | Whether an answer is still coming / settled as no way there |
955
+ | `followPath(block, x, y, arriveDistance, target)` | Follower bookkeeping, below |
956
+
957
+ A request is pending while its request id is ahead of its result id; the update writes waypoints and outcome
958
+ first and the result id last, so a settled path is never read half written. Up to `NAVIGATION_MAX_WAYPOINTS` (32)
959
+ waypoints fit; a longer path is cut short and marked `PathStatus.Incomplete`. Goals and paths are transient: whoever
960
+ set a goal sets it again after a load.
961
+
962
+ `followPath` is what a movement system calls each step. It skips waypoints within `arriveDistance`, writes the next
963
+ point to head for into `target` (no allocation) and says what kind it is:
964
+
965
+ ```ts
966
+ const step = followPath(navigation, x, y, 1, target);
967
+ if(step === FollowStep.Waypoint || step === FollowStep.Goal) {
968
+ steerToward(target);
969
+ } else {
970
+ halt(); // Waiting for a path, or Stuck at the closest reachable spot
971
+ }
972
+ ```
973
+
974
+ At the end of an incomplete path it asks for the rest itself and reports `Waiting`.
975
+
976
+ ### What the update does
977
+
978
+ - Only **active** movers with a pending request are searched. Searching is budgeted per run in A* tiles
979
+ (`iterationBudget`, default `DEFAULT_PATH_SEARCH_ITERATION_BUDGET`), not wall-clock time, so a run plays out the
980
+ same on any machine; the first search of a run always goes ahead and the rest wait for the next run. The
981
+ budget is fixed when the update is built, but `PathfindingSystem`'s `iterationBudget` (a config option and a live
982
+ field) overrides it from the next run on - say, a larger one when the system is on a worker, where searching
983
+ costs no frame time. In-process, every tile of budget is main-thread time inside `world.update`.
984
+ - Blockers are assumed never to move. Each is rasterised into the grid when it joins the `pathBlockers` query and
985
+ cleared when it leaves, and a new one re-paths only movers whose remaining path it crosses.
986
+ - A mover standing inside a blocker (a bed built around a sleeper) searches as if that blocker were not there, so it
987
+ walks out rather than being relocated past a wall.
988
+ - Paths are smoothed (string pulled) where the straight line is clear and no slower. A mover is placed by its
989
+ footprint, so one bigger than a tile only fits gaps its whole box fits.
990
+
991
+ ### Configuring it
992
+
993
+ `createPathfindingUpdate` builds the update both backends run. Hooks are baked in because functions do not
994
+ survive `postMessage`:
995
+
996
+ ```ts
997
+ export const pathfindingUpdate = createPathfindingUpdate<Components, PathfindingUpdateComponents, GameWorld>({
998
+ search: new GridAStar({ diagonal: true }),
999
+ // Bodies in these categories stop only movers whose collide mask takes them in, e.g. doors animals cannot open.
1000
+ gateCategories: DOOR,
1001
+ init(navigation, data, queries) {
1002
+ // Feed terrain in: 1.5 for a road, 0.5 for mud.
1003
+ setNavigationTerrainSpeed(navigation, 3, 4, 1.5);
1004
+ },
1005
+ queryChanged(world, queryName, delta) { /* the game's extra queries */ },
1006
+ preRun(world) { /* per-run upkeep */ },
1007
+ });
1008
+ ```
1009
+
1010
+ Pass it to `PathfindingSystem` as `updateFunction`, along with `blockers: { not: ['construction'] }` (or a `filter`)
1011
+ to narrow what counts as a blocker, `queries` for the hooks' own data, and `optional` for extra mover components.
1012
+ Another system wanting the same view of the world (say, finding enclosed pens) can build matching queries with
1013
+ `pathBlockerQueries` and keep its own `NavGrid` with `applyRectChanges`.
1014
+
1015
+ Gates get a grid per set of gate categories a mover's mask takes in, cloned from the blockers grid the first time
1016
+ such a mover searches, so gates nobody collides with cost nothing.
1017
+
1018
+ The search itself is swappable: anything implementing `PathSearch` (`findPath(grid, request)`) can be passed as
1019
+ `search`. `NavGrid`, `GridAStar`, `sweepCost` and `smoothPath` are exported for use on their own too.
1020
+
913
1021
  ## Examples
914
1022
 
915
- Three runnable examples live in [`examples/`](./examples), rendered with [Phaser](https://phaser.io) (a dev
1023
+ Runnable examples live in [`examples/`](./examples), rendered with [Phaser](https://phaser.io) (a dev
916
1024
  dependency - it is not part of the library) and driven by plain HTML controls beside the canvas:
917
1025
 
918
1026
  ```sh
@@ -927,6 +1035,11 @@ npm start # http://127.0.0.1:8080
927
1035
  them in turn. It starts on a deliberately long 100ms physics step, so turning **interpolate rendering** off is
928
1036
  a direct before-and-after on the same running scene: the same simulation, drawn six frames at a time instead
929
1037
  of one.
1038
+ - **Pathfinding** - click to send a square through a scene built to show each pathfinding case: a U-shaped trap,
1039
+ a gap only small movers fit, mud and road terrain, round and angled blockers, and a door only some movers can
1040
+ open. Toggle diagonal movement and smoothing, resize the mover, and add walls or paint terrain while it walks.
1041
+ - **Pathfinding stress** - thousands of movers wandering between random goals through hundreds of obstacles, with
1042
+ obstacle churn re-pathing whoever a new wall crosses, and per-worker timings in the stats panel.
930
1043
 
931
1044
  Every page has a **physics step** slider (starting at the library's own 50ms) and an **interpolate rendering**
932
1045
  toggle, so the trade the [interpolation section](#interpolation) describes can be looked at rather than
@@ -0,0 +1,11 @@
1
+ export declare const NAVIGATION_REQUEST_INDEX = 0;
2
+ export declare const NAVIGATION_RESULT_INDEX = 1;
3
+ export declare const NAVIGATION_OUTCOME_INDEX = 2;
4
+ export declare const NAVIGATION_WAYPOINT_INDEX = 3;
5
+ export declare const NAVIGATION_WAYPOINT_COUNT_INDEX = 4;
6
+ export declare const NAVIGATION_ACTIVE_INDEX = 5;
7
+ export declare const NAVIGATION_GOAL_X_INDEX = 6;
8
+ export declare const NAVIGATION_GOAL_Y_INDEX = 7;
9
+ export declare const NAVIGATION_WAYPOINTS_INDEX = 8;
10
+ export declare const NAVIGATION_MAX_WAYPOINTS = 32;
11
+ export declare const NAVIGATION_SIZE: number;
@@ -0,0 +1,31 @@
1
+ import type { ComponentDefinition } from '@daneren2005/shared-memory-ecs';
2
+ import { type Point } from '../pathfinding/path-search';
3
+ export interface NavigationComponent {
4
+ index: number;
5
+ readonly active: boolean;
6
+ readonly goalX: number;
7
+ readonly goalY: number;
8
+ readonly pending: boolean;
9
+ readonly outcome: number;
10
+ readonly waypointIndex: number;
11
+ readonly waypoints: Array<Point>;
12
+ navigateTo(x: number, y: number): void;
13
+ stop(): void;
14
+ }
15
+ export interface NavigationConfig {
16
+ navigate: boolean;
17
+ }
18
+ export declare const navigationDefinition: ComponentDefinition<NavigationComponent, Int32Array, NavigationConfig>;
19
+ export declare function navigateTo(navigation: Int32Array, x: number, y: number): void;
20
+ export declare function setNavigationGoal(navigation: Int32Array, x: number, y: number): void;
21
+ export declare function stopNavigating(navigation: Int32Array): void;
22
+ export declare function isNavigating(navigation: Int32Array): boolean;
23
+ export declare function requestPath(navigation: Int32Array): void;
24
+ export declare function isPathPending(navigation: Int32Array): boolean;
25
+ export declare function isUnreachable(navigation: Int32Array): boolean;
26
+ export declare function navigationGoal(navigation: Int32Array): Point;
27
+ export declare function waypoint(navigation: Int32Array, i: number): Point;
28
+ export declare function waypointX(navigation: Int32Array, i: number): number;
29
+ export declare function waypointY(navigation: Int32Array, i: number): number;
30
+ export declare function writePath(navigation: Int32Array, status: number, points: ReadonlyArray<Point>): void;
31
+ export default navigationDefinition;
@@ -2,6 +2,7 @@ import { type BodyComponent } from './body-component';
2
2
  import { type BouncinessComponent } from './bounciness-component';
3
3
  import { type DynamicsComponent } from './dynamics-component';
4
4
  import { type InterpolationComponent } from './interpolation-component';
5
+ import { type NavigationComponent } from './navigation-component';
5
6
  import { type PolygonComponent } from './polygon-component';
6
7
  import { type TransformComponent } from './transform-component';
7
8
  import { type VelocityComponent } from './velocity-component';
@@ -14,6 +15,9 @@ export declare const physicsRegistry: {
14
15
  dynamics: import("@daneren2005/shared-memory-ecs").ComponentDefinition<DynamicsComponent, Float32Array<ArrayBufferLike>, import("./dynamics-component").DynamicsConfig, import("./dynamics-component").DynamicsConfig>;
15
16
  interpolation: import("@daneren2005/shared-memory-ecs").ComponentDefinition<InterpolationComponent, Float32Array<ArrayBufferLike>, import("./interpolation-component").InterpolationConfig, object>;
16
17
  };
18
+ export declare const navigationRegistry: {
19
+ navigation: import("@daneren2005/shared-memory-ecs").ComponentDefinition<NavigationComponent, Int32Array<ArrayBufferLike>, import("./navigation-component").NavigationConfig, object>;
20
+ };
17
21
  export type PhysicsComponents = {
18
22
  transform: TransformComponent;
19
23
  velocity: VelocityComponent;
@@ -27,6 +31,11 @@ export type InterpolationComponents = {
27
31
  transform: TransformComponent;
28
32
  interpolation: InterpolationComponent;
29
33
  };
34
+ export type NavigationComponents = {
35
+ navigation: NavigationComponent;
36
+ transform: TransformComponent;
37
+ body: BodyComponent;
38
+ };
30
39
  export type PhysicsUpdateComponents = {
31
40
  transform: Float32Array;
32
41
  velocity: Float32Array;
package/dist/index.d.ts CHANGED
@@ -20,8 +20,11 @@ export type { BouncinessComponent, BouncinessConfig, } from './components/bounci
20
20
  export { default as interpolationDefinition } from './components/interpolation-component';
21
21
  export { INTERPOLATION_X_INDEX, INTERPOLATION_Y_INDEX, INTERPOLATION_SYNCED_TICK_INDEX, INTERPOLATION_DURATION_INDEX, INTERPOLATION_TICK_INDEX, INTERPOLATION_TOTAL_DURATION_INDEX, INTERPOLATION_SYNCED_DURATION_INDEX, INTERPOLATION_REMAINING_DURATION_INDEX, INTERPOLATION_TARGET_X_INDEX, INTERPOLATION_TARGET_Y_INDEX, INTERPOLATION_SYNCED_STEP_DURATION_INDEX, INTERPOLATION_CHANNEL_INDEX, INTERPOLATION_PREVIOUS_X_INDEX, INTERPOLATION_PREVIOUS_Y_INDEX, INTERPOLATION_PREVIOUS_DURATION_INDEX, INTERPOLATION_PREVIOUS_TOTAL_DURATION_INDEX, INTERPOLATION_PREVIOUS_TICK_INDEX, INTERPOLATION_SIZE, } from './components/interpolation-component-constants';
22
22
  export type { InterpolationComponent, InterpolationConfig, } from './components/interpolation-component';
23
- export { physicsRegistry } from './components/registry';
24
- export type { PhysicsComponents, PhysicsUpdateComponents, InterpolationComponents, InterpolationUpdateComponents, } from './components/registry';
23
+ export { default as navigationDefinition, isNavigating, isPathPending, isUnreachable, navigateTo, navigationGoal, requestPath, setNavigationGoal, stopNavigating, waypoint, waypointX, waypointY, writePath, } from './components/navigation-component';
24
+ export { NAVIGATION_REQUEST_INDEX, NAVIGATION_RESULT_INDEX, NAVIGATION_OUTCOME_INDEX, NAVIGATION_WAYPOINT_INDEX, NAVIGATION_WAYPOINT_COUNT_INDEX, NAVIGATION_ACTIVE_INDEX, NAVIGATION_GOAL_X_INDEX, NAVIGATION_GOAL_Y_INDEX, NAVIGATION_WAYPOINTS_INDEX, NAVIGATION_MAX_WAYPOINTS, NAVIGATION_SIZE, } from './components/navigation-component-constants';
25
+ export type { NavigationComponent, NavigationConfig } from './components/navigation-component';
26
+ export { physicsRegistry, navigationRegistry } from './components/registry';
27
+ export type { NavigationComponents, PhysicsComponents, PhysicsUpdateComponents, InterpolationComponents, InterpolationUpdateComponents, } from './components/registry';
25
28
  export { default as PhysicalWorld } from './world';
26
29
  export type { PhysicalSystemWorld, PhysicalWorldData, PhysicalWorldSource, PhysicalWorldEntity, PhysicalWorldFilter, PhysicalWorldOptions, } from './world';
27
30
  export { shapesOverlap, contactNormal, shapeHalfWidth, shapeHalfHeight, shapeIsEmpty, shapeRadius, capsuleHalfLength, capsuleCoreAngle, boundsHalfWidth, boundsHalfHeight, orientedBoxesOverlap, segmentSegmentDistanceSquared, segmentBoxDistanceSquared, pointSegmentDistanceSquared, } from './math/shapes';
@@ -45,6 +48,25 @@ export { default as physicsUpdate, createPhysicsUpdate } from './systems/physics
45
48
  export type { DeathInterpolationEntity, PhysicsCallbackWorld, PhysicsGroupConfig, PhysicsUpdateFunction, PhysicsUpdateMetadata, PhysicsUpdateOptions, PhysicsWorld } from './systems/physics-update';
46
49
  export { default as PhysicsSystem } from './systems/physics-system';
47
50
  export type { PhysicsSystemConfig, VelocityAssignment } from './systems/physics-system';
51
+ export { default as NavGrid } from './pathfinding/nav-grid';
52
+ export type { Rect, TileRange } from './pathfinding/nav-grid';
53
+ export { default as GridAStar } from './pathfinding/grid-a-star';
54
+ export type { GridAStarOptions } from './pathfinding/grid-a-star';
55
+ export { default as MinHeap } from './pathfinding/min-heap';
56
+ export { PathStatus } from './pathfinding/path-search';
57
+ export type { PathRequest, PathResult, PathSearch, PathStatusValue, Point } from './pathfinding/path-search';
58
+ export { sweepCost } from './pathfinding/line-of-sight';
59
+ export { smoothPath } from './pathfinding/smooth-path';
60
+ export { FollowStep, followPath } from './pathfinding/follow-path';
61
+ export type { FollowStepValue } from './pathfinding/follow-path';
62
+ export { PATH_BLOCKERS_QUERY, PATH_GATES_QUERY, applyRectChanges, blockerRect, pathBlockerQueries, } from './pathfinding/path-blockers';
63
+ export type { PathBlockerComponents, PathBlockerDelta, PathBlockerQueryOptions } from './pathfinding/path-blockers';
64
+ export { applyPathBlockerChanges, applyPathGateChanges, blockingRects, createNavigationState, navigationGridFor, setNavigationTerrainSpeed, } from './pathfinding/navigation-state';
65
+ export type { NavigationState } from './pathfinding/navigation-state';
66
+ export { DEFAULT_PATH_SEARCH_ITERATION_BUDGET, createPathfindingUpdate, pathfindingUpdate } from './pathfinding/pathfinding-update';
67
+ export type { PathfindingInitData, PathfindingUpdateComponents, PathfindingUpdateFunction, PathfindingUpdateMetadata, PathfindingUpdateOptions, PathfindingWorld, } from './pathfinding/pathfinding-update';
68
+ export { default as PathfindingSystem } from './pathfinding/pathfinding-system';
69
+ export type { PathfindingSystemConfig } from './pathfinding/pathfinding-system';
48
70
  export { default as interpolationUpdate } from './systems/interpolation-update';
49
71
  export type { InterpolationWorld } from './systems/interpolation-update';
50
72
  export { default as InterpolationSystem } from './systems/interpolation-system';