@daneren2005/shared-memory-physics 1.2.2 → 1.4.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
@@ -18,9 +18,9 @@ npm install @daneren2005/shared-memory-physics @daneren2005/shared-memory-ecs @d
18
18
 
19
19
  | Component | Config | Serialization | Block |
20
20
  | --------------- | -------------------------------------------- | -------------------------- | ---------------------------------------------- |
21
- | `transform` | `width`, `height`, `angle?` / `radius` | `x`, `y` | `Float32Array` – where, how big, facing |
21
+ | `transform` | `width`, `height`, `angle?` / `radius` | `x`, `y`, `angle` | `Float32Array` – where, how big, facing |
22
22
  | `velocity` | – | `velocityX?`, `velocityY?` | `Float32Array` – world units per second |
23
- | `body` | `shape?`, `collideCategory?`, `collideMask?` | – | `Uint32Array` – what shape, what collides with |
23
+ | `body` | `shape?`, `collideCategory?`, `collideMask?`, `sensor?`, `continuousCollisionDetection?` | – | `Uint32Array` – what shape, what collides with |
24
24
  | `interpolation` | `interpolate` | – | `Float32Array` – where to *draw* it |
25
25
 
26
26
  Entity configs are flat and shared across every component, so velocity is keyed as `velocityX` / `velocityY`
@@ -30,12 +30,13 @@ load one, and the missing one defaults to `0`.
30
30
  The transform is split in two. **Config** is what your entity template says - `width` and `height` are
31
31
  required, `angle` defaults to `0` - and a size is what loads the component at all: a config with neither a
32
32
  width nor a height does not get a transform. **Serialization** is the live state that changes as the world
33
- runs, so `save()` returns only `x` / `y` and a reloaded entity gets its size and starting facing back from
34
- your own template rather than the save.
33
+ runs, so `save()` returns `x` / `y` and `angle`, while a reloaded entity gets its size back from your own
34
+ template rather than the save.
35
35
 
36
36
  `x` / `y` are the **centre** of the box, not a corner: the box is rotated about that point and collision
37
37
  projects out from it in both directions. `angle` is in **radians** counter-clockwise from the +x axis, and
38
- nothing here writes it - it is yours to set, usually from the heading, and collision reads it. `width` /
38
+ the physics never writes it - it is yours to set, usually from the heading, and collision reads it - but it
39
+ round-trips through `save` so a turned entity comes back facing the same way. `width` /
39
40
  `height` are the unrotated size; a box with no area never overlaps anything, not even another box sharing its
40
41
  exact position.
41
42
 
@@ -308,8 +309,9 @@ happened rather than the one after, which is one frame of latency and nothing el
308
309
 
309
310
  - **Pause and `timeScale`** are free. `BaseWorld#runUpdate` skips every system while paused and scales the
310
311
  elapsed time it hands them, so the pacing stops and slows with the simulation on its own.
311
- - **Spawns** are free. The block is seeded from the entity's config, so something added between steps is drawn
312
- standing where it was put rather than sliding in from the origin.
312
+ - **Spawns at rest** are free. The block is seeded from the entity's config, so something added between steps is
313
+ drawn standing where it was put rather than sliding in from the origin. A spawn *already moving* is the one case
314
+ that needs a hand - see [`startInterpolation`](#a-moving-spawn-startinterpolation) below.
313
315
  - **Velocity changes, collisions and coming to rest** are all just steps the simulation took, and are drawn as
314
316
  they happened.
315
317
 
@@ -325,6 +327,41 @@ entity.components.transform!.x = 500;
325
327
  snapEntity(entity); // drawn at 500 on the very next frame
326
328
  ```
327
329
 
330
+ ### A moving spawn: `startInterpolation`
331
+
332
+ Blending has only one position for a brand-new entity - where it spawned - so it holds there until the first
333
+ physics step gives it a second, up to a whole step later. For anything at rest that is exactly right (the bullet
334
+ above stands where it was put). For something spawned *already moving* - a bullet leaving a barrel - that step of
335
+ stillness reads as the shot hanging in the air before it goes.
336
+
337
+ `startInterpolation` fixes it by **opening the render at the spawn point** and moving it out along the entity's own
338
+ velocity at its true speed, **jumping the transform forward** to where that motion reaches by the time the first
339
+ real step lands. So the entity is drawn leaving the spawn in its true direction from the very next frame, and the
340
+ seeded segment hands straight over to the first step with no seam. How far it jumps - and how long the seeded
341
+ segment lasts - is whatever is left of the current step, since that is when the first run lands:
342
+
343
+ ```ts
344
+ const bullet = world.loadEntity({
345
+ x: 0, y: 0, velocityX: 400, velocityY: 0, radius: 3, interpolate: true,
346
+ });
347
+ physics.startInterpolation(bullet); // drawn from the spawn point and moving out, not parked there until the first step
348
+ ```
349
+
350
+ It reads the step and the accumulator off the system, so there is nothing to pass.
351
+
352
+ **The jump skips a collision sweep.** It is a real move of the transform, applied without sweeping the span it
353
+ covers, so the entity skips forward up to a step: it can **tunnel** through anything within that span of the spawn,
354
+ and it runs that far ahead of where its velocity alone would have put it from then on. This is a deliberate trade
355
+ for **spawned projectiles**, where leaving the muzzle instantly matters more than that first sweep - a shot fired
356
+ *into* a target still lands inside it and is caught and killed by the next run's overlap test; only something thin
357
+ enough to sit entirely within that jumped span is passed through uncaught. Reach for it on bullets and the like,
358
+ not on something that must never skip a step of collision. Because the transform is jumped to the segment's end,
359
+ the render is still never drawn ahead of the simulation.
360
+
361
+ A no-op for an entity with no velocity or no interpolation, or while the system steps every frame. Driving physics
362
+ by hand rather than through `PhysicsSystem`? Call the underlying `startSpawnInterpolation(entity, { stepMs,
363
+ stepFraction, tick })` directly.
364
+
328
365
  ### The cost
329
366
 
330
367
  32 bytes per interpolated entity, three extra writes per entity per physics step, and a per-frame pass of one
@@ -469,6 +506,17 @@ thing in the way is thick, so at any normal frame rate it does not arise - a shi
469
506
  moves 1.6 of them per frame at 60fps. A fixed `deltaBetweenRuns` puts a ceiling on it if your game has anything
470
507
  fast enough to care.
471
508
 
509
+ **Continuous collision detection** lifts that ceiling for the bodies that need it, without slowing down the
510
+ ones that do not. Set `continuousCollisionDetection: true` on a body and its own move is tested along the whole
511
+ path it swept this run rather than only where the step ends - so a bullet that steps clean past a target still
512
+ comes to rest against it (or, if it is a sensor, still reports the pass through it). It is one extra swept-shape
513
+ test per candidate and only for that body: every other body stays on the single end-of-move test. Each shape
514
+ sweeps to a single covering shape - a rectangle to an oriented box spanning start to end, a circle or capsule to
515
+ a capsule down its path - chosen to be a superset of the ground actually covered, so a contact is reported early
516
+ at worst, never missed. A continuous body comes straight to rest at its first contact and does not corner-slide,
517
+ which is the behaviour a fast thing wants anyway. Reach for it on the small, fast things that tunnel - bullets,
518
+ thrown weapons - and leave it off everything else.
519
+
472
520
  ### Collision filtering
473
521
 
474
522
  Every body collides **as** a `collideCategory` and **with** a `collideMask`, both plain 32-bit fields. The
@@ -524,6 +572,26 @@ than off both. The pair is never missed, just attributed to the side that moved
524
572
  the entity that moves first gets the clear road: it takes its whole move, and the second one is the one that
525
573
  comes to rest against it.
526
574
 
575
+ ### Scoping a system to a group
576
+
577
+ Categories decide which *kinds* of body collide; `scope` decides which *world* they collide in. A game that
578
+ runs several independent groups at once - solar systems around the same origin, floors of a dungeon - gives
579
+ each group its own `PhysicsSystem` with a `scope`, and each one then only ever sees, moves, or collides against
580
+ the entities that pass it:
581
+
582
+ ```ts
583
+ world.addSystem(new PhysicsSystem<Components>(world, {
584
+ updateFunction: gamePhysicsUpdate,
585
+ scope: entity => entity.components.solarSystem?.solarSystemId === solId,
586
+ }));
587
+ ```
588
+
589
+ Unlike a category rule - which every body in the world is still weighed against - a scoped-out body never
590
+ enters this system's broadphase at all, so two groups whose boxes overlap around a shared origin cannot collide
591
+ across the boundary, and the tree each run builds is only as big as its own group. `scope` is separate from the
592
+ sharding `filter` (which narrows only *which* movers a worker steps, still sweeping the whole world so one
593
+ simulation can spread across cores); an entity handed both has to pass each to be moved.
594
+
527
595
  ## Using physicsUpdate in your own system
528
596
 
529
597
  `physicsUpdate` is a plain `EntityUpdateFunction`, so a game that already has its own movement system can call
@@ -557,6 +625,11 @@ transform[TRANSFORM_Y_INDEX] += moveY * fraction;
557
625
  `blocking` is what the entity came to rest *against*, which `forEachOverlapping` will not report from that
558
626
  resting place: the two are touching rather than through each other.
559
627
 
628
+ Both honour a body's `continuousCollisionDetection` flag. `sweep` picks it up on its own - a continuous body's
629
+ move is swept along its whole path. `forEachOverlapping` needs the move handed to it, since it runs after the
630
+ entity has already been put down: `forEachOverlapping(self, handle, moveX, moveY)` tests the path a continuous
631
+ body just swept, while the two-argument form (and every non-continuous body) tests only where it now stands.
632
+
560
633
  ## Spatial queries
561
634
 
562
635
  `SpatialIndex` answers the other kind of question about where things are - who is in this area, and what is
@@ -5,6 +5,7 @@ export interface BodyComponent {
5
5
  collideCategory: number;
6
6
  collideMask: number;
7
7
  sensor: boolean;
8
+ continuousCollisionDetection: boolean;
8
9
  }
9
10
  export declare const SHAPE_RECTANGLE = 1;
10
11
  export declare const SHAPE_CIRCLE = 2;
@@ -20,12 +21,17 @@ export interface BodyConfig {
20
21
  collideCategory?: number;
21
22
  collideMask?: number;
22
23
  sensor?: boolean;
24
+ continuousCollisionDetection?: boolean;
23
25
  }
24
- export declare const BODY_SHAPE_INDEX = 0;
26
+ export declare const BODY_FLAGS_INDEX = 0;
25
27
  export declare const BODY_CATEGORY_INDEX = 1;
26
28
  export declare const BODY_MASK_INDEX = 2;
27
- export declare const BODY_SENSOR_INDEX = 3;
28
- export declare const BODY_SIZE = 4;
29
+ export declare const BODY_SIZE = 3;
30
+ export declare const BODY_SHAPE_MASK = 3;
31
+ export declare const BODY_SENSOR_FLAG = 4;
32
+ export declare const BODY_CCD_FLAG = 8;
33
+ export declare function bodyShape(body: Uint32Array): number;
29
34
  export declare function isSensor(body: Uint32Array): boolean;
35
+ export declare function isContinuous(body: Uint32Array): boolean;
30
36
  export declare const bodyDefinition: ComponentDefinition<BodyComponent, Uint32Array, BodyConfig>;
31
37
  export default bodyDefinition;
@@ -1,11 +1,16 @@
1
1
  import type { ComponentDefinition } from '@daneren2005/shared-memory-ecs';
2
2
  import type { TransformComponent } from './transform-component';
3
+ import type { VelocityComponent } from './velocity-component';
3
4
  export interface InterpolationComponent {
4
5
  index: number;
5
6
  x: number;
6
7
  y: number;
7
8
  prevX: number;
8
9
  prevY: number;
10
+ progress: number;
11
+ syncedTick: number;
12
+ duration: number;
13
+ tick: number;
9
14
  }
10
15
  export interface InterpolationConfig {
11
16
  interpolate?: boolean;
@@ -29,4 +34,17 @@ export interface SnappableEntity {
29
34
  };
30
35
  }
31
36
  export declare function snapEntity(entity: SnappableEntity): void;
37
+ export interface SpawnableEntity {
38
+ components: {
39
+ transform?: TransformComponent;
40
+ velocity?: VelocityComponent;
41
+ interpolation?: InterpolationComponent;
42
+ };
43
+ }
44
+ export interface SpawnInterpolationOptions {
45
+ stepMs: number;
46
+ stepFraction: number;
47
+ tick: number;
48
+ }
49
+ export declare function startSpawnInterpolation(entity: SpawnableEntity, options: SpawnInterpolationOptions): void;
32
50
  export default interpolationDefinition;
@@ -16,6 +16,7 @@ export interface TransformConfig {
16
16
  export interface TransformSerialization {
17
17
  x: number;
18
18
  y: number;
19
+ angle: number;
19
20
  }
20
21
  export declare const TRANSFORM_X_INDEX = 0;
21
22
  export declare const TRANSFORM_Y_INDEX = 1;
package/dist/index.d.ts CHANGED
@@ -4,15 +4,15 @@ 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 } from './components/body-component';
8
- export { SHAPE_RECTANGLE, SHAPE_CIRCLE, SHAPE_CAPSULE, DEFAULT_COLLIDE_CATEGORY, DEFAULT_COLLIDE_MASK, BODY_SHAPE_INDEX, BODY_CATEGORY_INDEX, BODY_MASK_INDEX, BODY_SENSOR_INDEX, BODY_SIZE, } from './components/body-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';
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';
12
12
  export type { BouncinessComponent, BouncinessConfig, } from './components/bounciness-component';
13
- export { default as interpolationDefinition, snapEntity } from './components/interpolation-component';
13
+ export { default as interpolationDefinition, snapEntity, startSpawnInterpolation } from './components/interpolation-component';
14
14
  export { INTERPOLATION_X_INDEX, INTERPOLATION_Y_INDEX, INTERPOLATION_PREV_X_INDEX, INTERPOLATION_PREV_Y_INDEX, INTERPOLATION_PROGRESS_INDEX, INTERPOLATION_SYNCED_TICK_INDEX, INTERPOLATION_DURATION_INDEX, INTERPOLATION_TICK_INDEX, INTERPOLATION_SIZE, } from './components/interpolation-component';
15
- export type { InterpolationComponent, InterpolationConfig, SnappableEntity, } from './components/interpolation-component';
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
18
  export { shapesOverlap, contactNormal, shapeHalfWidth, shapeHalfHeight, shapeIsEmpty, shapeRadius, capsuleHalfLength, boundsHalfWidth, boundsHalfHeight, orientedBoxesOverlap, segmentSegmentDistanceSquared, segmentBoxDistanceSquared, pointSegmentDistanceSquared, } from './math/shapes';
@@ -22,7 +22,7 @@ export type { CollisionComponents, CollisionEntity, CollisionFunction, MoveResul
22
22
  export { default as SpatialIndex } from './systems/spatial-index';
23
23
  export type { SpatialComponents, SpatialEntity, SpatialFilter } from './systems/spatial-index';
24
24
  export { default as physicsUpdate, createPhysicsUpdate, POSITION_UPDATED_EVENT } from './systems/physics-update';
25
- export type { PhysicsUpdateFunction, PhysicsUpdateMetadata, PhysicsUpdateOptions, PhysicsWorld } from './systems/physics-update';
25
+ export type { PhysicsGroupConfig, PhysicsUpdateFunction, PhysicsUpdateMetadata, PhysicsUpdateOptions, PhysicsWorld } from './systems/physics-update';
26
26
  export { default as PhysicsSystem, DEFAULT_PHYSICS_STEP_MS } from './systems/physics-system';
27
27
  export type { PhysicsSystemConfig } from './systems/physics-system';
28
28
  export { default as interpolationUpdate } from './systems/interpolation-update';