@daneren2005/shared-memory-physics 1.3.0 → 1.5.0

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
@@ -362,6 +362,39 @@ A no-op for an entity with no velocity or no interpolation, or while the system
362
362
  by hand rather than through `PhysicsSystem`? Call the underlying `startSpawnInterpolation(entity, { stepMs,
363
363
  stepFraction, tick })` directly.
364
364
 
365
+ ### A moving death: `dieAtImpact`
366
+
367
+ The same lag runs the other way at the end of a life. A fast entity moves a whole step at a time and, with
368
+ `continuousCollisionDetection` on, is killed the run its *swept path* first crosses what it hits - which can be a
369
+ near-full stride before its step actually lands there. Remove it on the spot and the render, itself a step behind,
370
+ draws it blinking out well short of the target. A bullet dies in mid-air.
371
+
372
+ `dieAtImpact` fixes it as the mirror of the spawn seeding: instead of `callbacks.entityDied`, a collision callback
373
+ calls it to **end the dying entity's current segment on the exact point it struck** (`CollisionBroadphase.contactPoint`,
374
+ the same entry fraction the sweep already refines) and **defer the kill by one run** - the one extra step the render
375
+ needs to play that last stride onto the impact before the entity is gone:
376
+
377
+ ```ts
378
+ onCollision(world, self, other, queries, callbacks) {
379
+ // `self` (a fast sensor bullet) just swept through `other`. Draw it reaching `other`, then die - rather than
380
+ // vanishing a stride short.
381
+ world.dieAtImpact(self, other);
382
+
383
+ // Gameplay still happens now: the hit lands this run, only the visual removal waits.
384
+ callbacks.emitEntityEvent(other.entityId, 'hit');
385
+ }
386
+ ```
387
+
388
+ Either side of the pair can be the one to kill - `dieAtImpact(dying, other)` takes whichever carried the fatal
389
+ role, working out that entity's own approach from its velocity. The struck entity is marked with a runtime `dying`
390
+ bit (`BODY_DYING_FLAG`) that leaves it out of the broadphase at once, so nothing runs into it again while it plays
391
+ out, and it is removed at the top of its next update. The bit lives in shared memory, so any worker stepping the
392
+ entity honours it. Nothing clears it - a dying entity is gone a run later.
393
+
394
+ `dieAtImpact` is set on the world every run by `createPhysicsUpdate`; keep a `callbacks.entityDied` fallback if you
395
+ call `onCollision` by hand without a `preRun`. It moves position only - raise your own hit or score event on the
396
+ spot, as above.
397
+
365
398
  ### The cost
366
399
 
367
400
  32 bytes per interpolated entity, three extra writes per entity per physics step, and a per-frame pass of one
@@ -458,7 +491,9 @@ value another thread also touches with the atomics from `@daneren2005/shared-mem
458
491
  the two entities in front of it - to credit a third for a kill, say - a callback can walk
459
492
  `queries[COLLIDABLE_QUERY]`, the full collidable list. Anything that has to happen on the main thread goes
460
493
  through `callbacks`: `entityDied`, `entityComponentChanged`, `createEntity`, and `emitEntityEvent` for an
461
- event of your own - the same one the move above reports itself through.
494
+ event of your own - the same one the move above reports itself through. To kill a *fast* mover on impact and
495
+ still have it drawn reaching what it hit, call `world.dieAtImpact(dying, other)` in place of `entityDied` - see
496
+ [`dieAtImpact`](#a-moving-death-dieatimpact).
462
497
 
463
498
  **What collides.** Everything with a transform and a `body`, not only the entities the system moves, so a
464
499
  ship can run into a station that has no velocity of its own. Shapes that only just touch do not count as
@@ -632,14 +667,44 @@ body just swept, while the two-argument form (and every non-continuous body) tes
632
667
 
633
668
  ## Spatial queries
634
669
 
635
- `SpatialIndex` answers the other kind of question about where things are - who is in this area, and what is
636
- the closest thing to this point - for the systems that are not about collisions at all: targeting, aggro
637
- range, spawning somewhere clear, area effects. It is the same R-tree the broadphase is built on, without the
638
- collide categories or the shape tests.
670
+ `PhysicalWorld` keeps every transformed entity in a live, shared `SharedSpatialMap`. The physics worker updates
671
+ the entry immediately after it writes a final position, while entity add/remove and transform/body component
672
+ changes are tracked on the main thread. That makes the same index available for occasional main-thread checks
673
+ such as picking an entity under the mouse, and for worker systems that would otherwise rebuild an index for only
674
+ a few queries:
675
+
676
+ ```ts
677
+ import { PhysicalWorld, physicsRegistry } from '@daneren2005/shared-memory-physics';
678
+
679
+ const world = new PhysicalWorld(physicsRegistry, {
680
+ spatial: { gridSize: 50, maxEntities: 100_000 },
681
+ });
682
+
683
+ const clicked = world.findNearestSpatial(mouseX, mouseY, 0);
684
+ const nearby = world.searchSpatialAround(x, y, 100, 100);
685
+ ```
639
686
 
640
- Build one from anything shaped like a query result and it reads the transform (and the body, to know which
641
- outline that transform describes) off each entity. Only the transform is needed, so it indexes anything with
642
- a place in the world rather than only what collides:
687
+ `searchSpatial`, `searchSpatialAround`, `findNearestSpatial`, and `findNearbySpatial` return live entities and
688
+ accept entity filters. Shape-aware axis-aligned bounds are stored: rotated rectangles and capsules occupy the
689
+ box around their actual outline. A direct transform/body edit outside the physics update must be followed by
690
+ `world.updateSpatialEntity(entity)`. A custom physics wrapper that changes a transform after calling the library
691
+ update can call `updateSpatialMap(workerWorld, entityId, components)` instead.
692
+
693
+ `PhysicsSystem` requires a `PhysicalWorld` (or another `BaseWorld` that implements `PhysicalWorldSource`) and
694
+ automatically shares its map with the physics worker. Another worker system can receive the same map by calling
695
+ `addPhysicalWorldData(sourceWorld, runWorld)` from its system's `addDataToWorld`, then `getSpatialMap(runWorld)` in
696
+ its update. The worker world must come from `@daneren2005/shared-memory-ecs` 1.5.1 or newer so it carries the
697
+ shared heap used to reconstruct the map handle.
698
+
699
+ The live map is intentionally not the collision or bulk-query hot path. `CollisionBroadphase` and
700
+ `SpatialIndex` still use Flatbush snapshots: packing one immutable R-tree per run is faster when that run will
701
+ make a large number of queries. Use the live map for continuous availability and sparse queries; use
702
+ `SpatialIndex` for query-heavy systems such as targeting every entity every tick.
703
+
704
+ `SpatialIndex` is the same R-tree the collision broadphase is built on, without collide categories or shape
705
+ tests. Build one from anything shaped like a query result and it reads the transform (and the body, to know which
706
+ outline that transform describes) off each entity. Only the transform is needed, so it indexes anything with a
707
+ place in the world rather than only what collides:
643
708
 
644
709
  ```ts
645
710
  import { SpatialIndex } from '@daneren2005/shared-memory-physics';
@@ -30,8 +30,11 @@ export declare const BODY_SIZE = 3;
30
30
  export declare const BODY_SHAPE_MASK = 3;
31
31
  export declare const BODY_SENSOR_FLAG = 4;
32
32
  export declare const BODY_CCD_FLAG = 8;
33
+ export declare const BODY_DYING_FLAG = 16;
33
34
  export declare function bodyShape(body: Uint32Array): number;
34
35
  export declare function isSensor(body: Uint32Array): boolean;
35
36
  export declare function isContinuous(body: Uint32Array): boolean;
37
+ export declare function isDying(body: Uint32Array): boolean;
38
+ export declare function markDying(body: Uint32Array): void;
36
39
  export declare const bodyDefinition: ComponentDefinition<BodyComponent, Uint32Array, BodyConfig>;
37
40
  export default bodyDefinition;
package/dist/index.d.ts CHANGED
@@ -4,8 +4,8 @@ export type { TransformComponent, TransformConfig, TransformSerialization, } fro
4
4
  export { default as velocityDefinition } from './components/velocity-component';
5
5
  export { VELOCITY_X_INDEX, VELOCITY_Y_INDEX, VELOCITY_SIZE, } from './components/velocity-component';
6
6
  export type { VelocityComponent, VelocityConfig, VelocitySerialization, } from './components/velocity-component';
7
- export { default as bodyDefinition, canCollide, isSensor, isContinuous, bodyShape } from './components/body-component';
8
- export { SHAPE_RECTANGLE, SHAPE_CIRCLE, SHAPE_CAPSULE, DEFAULT_COLLIDE_CATEGORY, DEFAULT_COLLIDE_MASK, BODY_FLAGS_INDEX, BODY_SHAPE_MASK, BODY_SENSOR_FLAG, BODY_CCD_FLAG, BODY_CATEGORY_INDEX, BODY_MASK_INDEX, BODY_SIZE, } from './components/body-component';
7
+ export { default as bodyDefinition, canCollide, isSensor, isContinuous, isDying, markDying, bodyShape } from './components/body-component';
8
+ export { SHAPE_RECTANGLE, SHAPE_CIRCLE, SHAPE_CAPSULE, DEFAULT_COLLIDE_CATEGORY, DEFAULT_COLLIDE_MASK, BODY_FLAGS_INDEX, BODY_SHAPE_MASK, BODY_SENSOR_FLAG, BODY_CCD_FLAG, BODY_DYING_FLAG, BODY_CATEGORY_INDEX, BODY_MASK_INDEX, BODY_SIZE, } from './components/body-component';
9
9
  export type { BodyComponent, BodyConfig, } from './components/body-component';
10
10
  export { default as bouncinessDefinition } from './components/bounciness-component';
11
11
  export { BOUNCINESS_INDEX, BOUNCINESS_SIZE, } from './components/bounciness-component';
@@ -15,14 +15,16 @@ export { INTERPOLATION_X_INDEX, INTERPOLATION_Y_INDEX, INTERPOLATION_PREV_X_INDE
15
15
  export type { InterpolationComponent, InterpolationConfig, SnappableEntity, SpawnableEntity, SpawnInterpolationOptions, } from './components/interpolation-component';
16
16
  export { physicsRegistry } from './components/registry';
17
17
  export type { PhysicsComponents, PhysicsUpdateComponents, InterpolationComponents, InterpolationUpdateComponents, } from './components/registry';
18
+ export { default as PhysicalWorld, addPhysicalWorldData, getSpatialMap } from './world';
19
+ export type { PhysicalSystemWorld, PhysicalWorldData, PhysicalWorldSource, PhysicalWorldEntity, PhysicalWorldFilter, PhysicalWorldOptions, } from './world';
18
20
  export { shapesOverlap, contactNormal, shapeHalfWidth, shapeHalfHeight, shapeIsEmpty, shapeRadius, capsuleHalfLength, boundsHalfWidth, boundsHalfHeight, orientedBoxesOverlap, segmentSegmentDistanceSquared, segmentBoxDistanceSquared, pointSegmentDistanceSquared, } from './math/shapes';
19
21
  export type { Vector } from './math/shapes';
20
22
  export { default as CollisionBroadphase, COLLIDABLE_QUERY } from './systems/collision';
21
23
  export type { CollisionComponents, CollisionEntity, CollisionFunction, MoveResult, MovingEntity, SweepResult } from './systems/collision';
22
24
  export { default as SpatialIndex } from './systems/spatial-index';
23
25
  export type { SpatialComponents, SpatialEntity, SpatialFilter } from './systems/spatial-index';
24
- export { default as physicsUpdate, createPhysicsUpdate, POSITION_UPDATED_EVENT } from './systems/physics-update';
25
- export type { PhysicsUpdateFunction, PhysicsUpdateMetadata, PhysicsUpdateOptions, PhysicsWorld } from './systems/physics-update';
26
+ export { default as physicsUpdate, createPhysicsUpdate, POSITION_UPDATED_EVENT, updateSpatialMap } from './systems/physics-update';
27
+ export type { DeathInterpolationEntity, PhysicsGroupConfig, PhysicsUpdateFunction, PhysicsUpdateMetadata, PhysicsUpdateOptions, PhysicsWorld } from './systems/physics-update';
26
28
  export { default as PhysicsSystem, DEFAULT_PHYSICS_STEP_MS } from './systems/physics-system';
27
29
  export type { PhysicsSystemConfig } from './systems/physics-system';
28
30
  export { default as interpolationUpdate } from './systems/interpolation-update';