@daneren2005/shared-memory-physics 1.0.0 → 1.1.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
@@ -14,11 +14,12 @@ npm install @daneren2005/shared-memory-physics @daneren2005/shared-memory-ecs @d
14
14
 
15
15
  ## Components
16
16
 
17
- | Component | Config | Serialization | Block |
18
- | ----------- | -------------------------------------------- | -------------------------- | ---------------------------------------------- |
19
- | `transform` | `width`, `height`, `angle?` / `radius` | `x`, `y` | `Float32Array` – where, how big, facing |
20
- | `velocity` | – | `velocityX?`, `velocityY?` | `Float32Array` – world units per second |
21
- | `body` | `shape?`, `collideCategory?`, `collideMask?` | – | `Uint32Array` – what shape, what collides with |
17
+ | Component | Config | Serialization | Block |
18
+ | --------------- | -------------------------------------------- | -------------------------- | ---------------------------------------------- |
19
+ | `transform` | `width`, `height`, `angle?` / `radius` | `x`, `y` | `Float32Array` – where, how big, facing |
20
+ | `velocity` | – | `velocityX?`, `velocityY?` | `Float32Array` – world units per second |
21
+ | `body` | `shape?`, `collideCategory?`, `collideMask?` | – | `Uint32Array` – what shape, what collides with |
22
+ | `interpolation` | `interpolate` | – | `Float32Array` – where to *draw* it |
22
23
 
23
24
  Entity configs are flat and shared across every component, so velocity is keyed as `velocityX` / `velocityY`
24
25
  rather than `x` / `y` (which already belong to the transform). Either velocity axis on its own is enough to
@@ -36,6 +37,10 @@ nothing here writes it - it is yours to set, usually from the heading, and colli
36
37
  `height` are the unrotated size; a box with no area never overlaps anything, not even another box sharing its
37
38
  exact position.
38
39
 
40
+ `interpolation` is where to **draw** an entity, which stops being the same thing as where it is the moment
41
+ physics runs less often than the screen refreshes. It is opt-in per entity (`interpolate: true`) and filled in
42
+ by [`InterpolationSystem`](#interpolation) - see that section for what it costs and what it buys.
43
+
39
44
  The `body` is what makes an entity collidable, and a size loads one just as it loads a transform - so
40
45
  everything with a place in the world collides with everything else until you say otherwise. All of it is
41
46
  defining config out of your own template, which is why `save` returns nothing for it: a reloaded entity gets
@@ -121,24 +126,77 @@ world.update(1000); // one second: the entity above is now at x = 10
121
126
  `elapsedTime` is in milliseconds (what `BaseWorld#update` is driven with) and velocity is in world units per
122
127
  **second**, so a run covering 16ms moves an entity 16/1000ths of its velocity.
123
128
 
124
- An entity that moved is reported back to the main thread as a single `position-updated` event carrying where
125
- it ended up, so anything you keep keyed off position can follow a move rather than having to poll the block:
129
+ ### The step
130
+
131
+ `PhysicsSystem` runs on a fixed step of **50ms** unless you say otherwise (`DEFAULT_PHYSICS_STEP_MS`). Collision
132
+ is by a distance the whole of it, so running it 20 times a second rather than 60 is two thirds less of the frame
133
+ spent on it for a simulation that is no less correct - only coarser. What that costs is smoothness, which is
134
+ what [`InterpolationSystem`](#interpolation) is for.
135
+
136
+ ```ts
137
+ new PhysicsSystem(world, { deltaBetweenRuns: 0 }); // back to every frame
138
+ ```
139
+
140
+ The step has one real ceiling: a move is swept where it *ends*, so **a longer step is a longer move, and a move
141
+ longer than the thing in its way is thick goes clean through it**. At 50ms an entity crossing 300 units a second
142
+ covers 15 of them per step, so anything it must be stopped by has to be thicker than that. A game with genuinely
143
+ fast entities either keeps the step short or makes its walls thick.
144
+
145
+ The system accumulates frame time and runs once it has a step's worth, so a 16ms frame runs physics on roughly
146
+ every third one and hands it the whole 50ms. Nothing is lost and nothing is run twice.
147
+
148
+ `PhysicsSystem` also stamps each run with a `tick`, which is what the interpolation block is published under.
149
+ A game that subclasses the system to attach per-run data of its own **must call `super.addDataToWorld(world)`**:
150
+
151
+ ```ts
152
+ class GamePhysicsSystem extends PhysicsSystem<Components, GameComponents, GameWorld> {
153
+ addDataToWorld(world: GameWorld): void {
154
+ super.addDataToWorld(world); // without this, nothing gets a tick
155
+ world.bounds = this.bounds;
156
+ }
157
+ }
158
+ ```
159
+
160
+ `GameWorld` extends `PhysicsWorld` (which is `ComponentSystemWorld` plus that `tick`) rather than
161
+ `ComponentSystemWorld` directly.
162
+
163
+ A run's moves are reported back to the main thread as a single `position-updated` event **on the system**,
164
+ carrying the ids of everything that moved, so anything you keep keyed off position can follow the moves
165
+ rather than having to poll every block:
126
166
 
127
167
  ```ts
128
168
  import { POSITION_UPDATED_EVENT } from '@daneren2005/shared-memory-physics';
129
169
 
130
- entity.on(POSITION_UPDATED_EVENT, (x, y) => {
131
- // 12.5, -3
170
+ const physics = world.addSystem(new PhysicsSystem(world));
171
+
172
+ physics.on(POSITION_UPDATED_EVENT, (entityIds: Array<number>) => {
173
+ for(const eid of entityIds) {
174
+ const transform = world.getEntityByEid(eid)?.components.transform;
175
+ // transform.x / transform.y are where it ended up
176
+ }
132
177
  });
133
178
  ```
134
179
 
135
- Both axes come with it even when only one of them moved, so a listener always has the whole position without
136
- having to remember the last one it was told - and a diagonal move costs one event across the worker boundary
137
- rather than the two a `component-property-updated` per property would. The move is not reported as a property
138
- change as well, so a listener on `component-property-updated` no longer hears about positions at all.
180
+ Nearly every entity moves on nearly every run, so this is the one thing a physics system must not report an
181
+ entity at a time: an event object apiece in the worker, cloned across the boundary, then a lookup and an emit
182
+ apiece on the main thread costs more, at a few thousand entities, than moving them did. One array of ids
183
+ costs one listener call for the whole run.
184
+
185
+ No position travels with the id, because there is no need: the transform is a `SharedArrayBuffer` block, so
186
+ the position the worker wrote is already on the main thread by the time the event lands - read it off the
187
+ entity (or straight off the block). The move is not reported as a property change either, so a listener on
188
+ `component-property-updated` never hears about positions.
189
+
190
+ An entity that did not move is not in the list, so a world of still entities stays quiet - and so does one
191
+ held against something it cannot move past.
139
192
 
140
- An entity that did not move says nothing, so a world of still entities stays quiet - and so does one held
141
- against something it cannot move past.
193
+ **A run only reports at all when something is listening.** The bill for this event is paid in the worker - an id
194
+ per moved entity pushed into the run's event array, and that array structured-cloned back across the boundary -
195
+ so `PhysicsSystem` checks per run whether it has a listener and tells the update not to bother when it does not.
196
+ That matters most for a game rendering from [`interpolation`](#interpolation), which wants nothing to do with an
197
+ event that fires at the physics rate: it is the rate that game is drawing *around*. Nothing has to be
198
+ configured for it, but `reportMoves: true` forces it on for a listener attached somewhere the system cannot see
199
+ it, and `reportMoves: false` turns it off regardless.
142
200
 
143
201
  The move is written with `addAtomicFloat32` rather than a plain `+=`, so a game system on another thread can
144
202
  add to the same position in the same instant without either move being lost.
@@ -167,13 +225,130 @@ world.addSystem(new PhysicsSystem(world, {
167
225
  Without a `getWorker` (or where Web Workers / `SharedArrayBuffer` are unavailable) the exact same
168
226
  `physicsUpdate` runs in-process on the main thread instead.
169
227
 
228
+ ## Interpolation
229
+
230
+ A 50ms physics step against a 16ms frame means the transform only changes on one frame in three, and an entity
231
+ drawn straight off it visibly stutters. `InterpolationSystem` fills in a **render position** that changes every
232
+ frame instead, by blending between the two positions physics published either side of its last step:
233
+
234
+ ```
235
+ render = prev + (current - prev) * alpha
236
+ ```
237
+
238
+ Every position it writes is therefore one the simulation actually produced - somewhere on the segment between
239
+ two real ones, never a guess past the end of one. There is nothing to tune and no case where it draws something
240
+ that did not happen: an entity that stopped against a wall is drawn easing into the wall and stopping, and one
241
+ that turned a corner is drawn turning the corner rather than carrying on into its old heading for a frame and
242
+ being snapped back. The cost is **one step of latency, always** - what is on screen is where the world was a
243
+ step ago.
244
+
245
+ ```ts
246
+ import { InterpolationSystem, PhysicsSystem } from '@daneren2005/shared-memory-physics';
247
+
248
+ world.addSystem(new PhysicsSystem<Components>(world, { getWorker }));
249
+ world.addSystem(new InterpolationSystem<Components>(world));
250
+
251
+ world.loadEntity({ x: 0, y: 0, width: 10, height: 10, velocityX: 100, interpolate: true });
252
+ ```
253
+
254
+ It takes no configuration and is not wired to the physics system. Everything it needs - the two positions, how
255
+ much simulated time lies between them, and whether that pair is new - is published into the block by whichever
256
+ run wrote it, so a game that retunes `deltaBetweenRuns` mid-flight is followed with nothing told to this system.
257
+
258
+ Then draw from `interpolation` instead of `transform`, keeping the size and facing where they have always been:
259
+
260
+ ```ts
261
+ const { x, y } = entity.components.interpolation ?? entity.components.transform!;
262
+ const { width, height, angle } = entity.components.transform!;
263
+ ```
264
+
265
+ Draw from it **every frame**, not from `position-updated`. That event fires once per physics step, which is
266
+ exactly the cadence interpolation exists to hide - a sprite moved by it would be as choppy as one reading the
267
+ transform. Once nothing is listening to it, `PhysicsSystem` stops reporting moves at all and the whole event
268
+ costs nothing; see [the note above](#the-step).
269
+
270
+ **Only rendering should read it.** Range checks, targeting, AI steering, "can I build here" and anything you
271
+ persist all want `transform` - the real position. The render position is a function of local frame timing, so
272
+ two machines drawing the same simulation at 144Hz and 30Hz hold *different* numbers in it at the same tick;
273
+ anything that branches on it stops being deterministic. Nothing in this library ever reads it back, which is
274
+ what keeps the simulation itself unaffected.
275
+
276
+ ### A slow physics run costs latency and nothing else
277
+
278
+ This is the property the whole thing is for, and it is worth being explicit about because it is easy to build
279
+ something that looks right and does not have it. **If a physics run takes longer than a frame, nothing about the
280
+ motion changes** - it is drawn a little further behind, at exactly the same speed. A worker that consistently
281
+ takes 30ms to come back costs 30ms of latency, which is not something an eye can see; only *variance* in how
282
+ late it is can be seen at all.
283
+
284
+ That works because the pacing is driven by the steps that have **landed** in the block, not by the physics
285
+ system's own accumulator. The accumulator resets when a run is *posted*, and on a worker thread that is not when
286
+ its results arrive - a run posted with 14ms of leftover and taking 30ms to come back leaves a whole frame where
287
+ the accumulator says "0.3 of the way into the new step" while the block still holds the one before it, so the
288
+ entity is drawn 0.3 along a segment it was drawn 0.96 along last frame. Backwards, then a lurch forward, on
289
+ every step.
290
+
291
+ For the same reason each run publishes **how much simulated time it covered** rather than that being assumed to
292
+ be the step: a run that comes back late leaves more than a step's worth banked, and the next one covers two at
293
+ once. Dividing that segment by the step would draw it at double speed.
294
+
295
+ Ordering against the physics system is only a preference - after it means a step is picked up on the frame it
296
+ happened rather than the one after, which is one frame of latency and nothing else.
297
+
298
+ ### What it does not need
299
+
300
+ - **Pause and `timeScale`** are free. `BaseWorld#runUpdate` skips every system while paused and scales the
301
+ elapsed time it hands them, so the pacing stops and slows with the simulation on its own.
302
+ - **Spawns** are free. The block is seeded from the entity's config, so something added between steps is drawn
303
+ standing where it was put rather than sliding in from the origin.
304
+ - **Velocity changes, collisions and coming to rest** are all just steps the simulation took, and are drawn as
305
+ they happened.
306
+
307
+ ### What it does need: `snapEntity`
308
+
309
+ A teleport and a long move are the same two numbers, so this is the one thing blending cannot work out for
310
+ itself. Write a transform directly and the entity is drawn sliding the whole way there over the next step:
311
+
312
+ ```ts
313
+ import { snapEntity } from '@daneren2005/shared-memory-physics';
314
+
315
+ entity.components.transform!.x = 500;
316
+ snapEntity(entity); // drawn at 500 on the very next frame
317
+ ```
318
+
319
+ ### The cost
320
+
321
+ 32 bytes per interpolated entity, three extra writes per entity per physics step, and a per-frame pass of one
322
+ lerp per entity on the main thread - order 0.2ms at 10,000 entities. An entity without the component pays
323
+ nothing at all. `forceMainThread: false` plus a `getWorker` moves the pass to a worker for worlds large enough
324
+ that it shows up in a profile, at the price of the render position being one frame stale; because consumers only
325
+ ever read `interpolation.x`, that is a constructor flag rather than a migration.
326
+
327
+ Physics writes `prev` and then the transform on the worker thread while the main thread reads both, so the block
328
+ carries a tick stamp that is written **last**, with a release store. The update reads it on either side of its
329
+ own reads and drops the frame if it changed, rather than blending a `prev` from one step against a transform
330
+ from another - which is the one mismatch that would draw an entity moving backwards.
331
+
170
332
  ## Collisions
171
333
 
172
- `createPhysicsUpdate` builds an update that, right after moving each entity, tells a callback of yours what
173
- that entity has moved into. The callback runs wherever the update runs - on the worker thread, in the same
174
- pass as the movement - so it cannot be handed to `PhysicsSystem` at construction time: functions do not
175
- survive `postMessage`. It reaches the worker by being baked into a module that both the worker file and the
176
- main thread import.
334
+ `createPhysicsUpdate` builds an update whose entities notice each other: every move is swept against whatever
335
+ is in front of it, so nothing is ever moved into anything else, and a callback of yours is told what each
336
+ entity ran into. The callback runs wherever the update runs - on the worker thread, in the same pass as the
337
+ movement - so it cannot be handed to `PhysicsSystem` at construction time: functions do not survive
338
+ `postMessage`. It reaches the worker by being baked into a module that both the worker file and the main
339
+ thread import.
340
+
341
+ The callback is optional and the [sweep](#stopping-at-the-edge) is not. `createPhysicsUpdate()` with nothing
342
+ asked of it at all is movement that comes to rest against walls, terrain and other units and says nothing
343
+ about any of it:
344
+
345
+ ```ts
346
+ // Stopped by everything, responds to nothing.
347
+ export const gamePhysicsUpdate = createPhysicsUpdate<Components>();
348
+ ```
349
+
350
+ Movement that walks straight through the world is the bare [`physicsUpdate`](#using-physicsupdate-in-your-own-system)
351
+ instead, which is also the only one that costs nothing per entity for the collidable query.
177
352
 
178
353
  ```ts
179
354
  // game-physics-update.ts - imported by *both* the worker file and the system below
@@ -247,6 +422,9 @@ An entity is never moved into another one. Before anything is written to the tra
247
422
  against everything along its path, and the entity is put down on the edge of whatever is in the way - then
248
423
  `onCollision` runs for it, exactly as it would if the two had ended up overlapping.
249
424
 
425
+ This is every update `createPhysicsUpdate` builds, with or without a callback: where an entity is allowed to
426
+ end up is physics' decision, and what the game does about the contact is a separate one.
427
+
250
428
  ```ts
251
429
  // A ship at x = 0 and a station at x = 30, both 10 wide, with a velocity that asks for 25 in this run.
252
430
  world.update(1000);
@@ -410,11 +588,48 @@ ask it directly rather than building the sort yourself - it settles as soon as t
410
588
  measured to an entity's **box**, not to its centre, so a large target is as near as its nearest edge and
411
589
  `maxDistance` reads as a range past the hull.
412
590
 
591
+ ## Examples
592
+
593
+ Three runnable examples live in [`examples/`](./examples), rendered with [Phaser](https://phaser.io) (a dev
594
+ dependency - it is not part of the library) and driven by plain HTML controls beside the canvas:
595
+
596
+ ```sh
597
+ npm start # http://127.0.0.1:8080
598
+ ```
599
+
600
+ - **Bouncing circles** - circles with a random position and heading, reflected off each other and off the
601
+ walls by an `onCollision` that mirrors the velocity about the contact normal.
602
+ - **Bouncing rectangles** - the same, on boxes of random width and height, where the normal is the axis the
603
+ two are least through each other on rather than the line between their centres.
604
+ - **Walking into a wall** - one unit between two boxes, turned around on a timer so it presses into each of
605
+ them in turn. It starts on a deliberately long 100ms physics step, so turning **interpolate rendering** off is
606
+ a direct before-and-after on the same running scene: the same simulation, drawn six frames at a time instead
607
+ of one.
608
+
609
+ Every page has a **physics step** slider (starting at the library's own 50ms) and an **interpolate rendering**
610
+ toggle, so the trade the [interpolation section](#interpolation) describes can be looked at rather than
611
+ reasoned about.
612
+
613
+ The examples import the library as `@daneren2005/shared-memory-physics`, aliased at the source next door, so
614
+ the code reads the way a game's would and editing a system hot-reloads the running page. `npm start` also
615
+ serves the two [cross-origin isolation](https://developer.mozilla.org/en-US/docs/Web/API/crossOriginIsolated)
616
+ headers `SharedArrayBuffer` needs, so physics genuinely runs on a worker there; where a browser will not hand
617
+ one out, the ECS runs the same update in-process instead and the **Run physics in a worker** toggle says so.
618
+
619
+ The walls the examples bounce off are ordinary entities - a transform and a body, no velocity - so they are
620
+ stopped against by the same sweep that stops anything against anything.
621
+
622
+ Every push to `dev` or `production` publishes them to GitHub Pages, both to the same place - so the site shows
623
+ whichever branch pushed last, which is what you want while the examples are changing faster than releases are
624
+ cut. See [`.github/workflows/pages.yml`](./.github/workflows/pages.yml) for the one-time repository setting it
625
+ needs, and for the one line to drop when it should go back to publishing releases only.
626
+
413
627
  ## Building
414
628
 
415
629
  ```sh
416
630
  npm install
417
- npm run build # emits dist/ (js + d.ts)
631
+ npm run build # emits dist/ (js + d.ts)
632
+ npm run build:examples # emits examples/dist/
418
633
  npm run type-check
419
634
  npm test
420
635
  ```
@@ -0,0 +1,32 @@
1
+ import type { ComponentDefinition } from '@daneren2005/shared-memory-ecs';
2
+ import type { TransformComponent } from './transform-component';
3
+ export interface InterpolationComponent {
4
+ index: number;
5
+ x: number;
6
+ y: number;
7
+ prevX: number;
8
+ prevY: number;
9
+ }
10
+ export interface InterpolationConfig {
11
+ interpolate?: boolean;
12
+ x?: number;
13
+ y?: number;
14
+ }
15
+ export declare const INTERPOLATION_X_INDEX = 0;
16
+ export declare const INTERPOLATION_Y_INDEX = 1;
17
+ export declare const INTERPOLATION_PREV_X_INDEX = 2;
18
+ export declare const INTERPOLATION_PREV_Y_INDEX = 3;
19
+ export declare const INTERPOLATION_PROGRESS_INDEX = 4;
20
+ export declare const INTERPOLATION_SYNCED_TICK_INDEX = 5;
21
+ export declare const INTERPOLATION_DURATION_INDEX = 6;
22
+ export declare const INTERPOLATION_TICK_INDEX = 7;
23
+ export declare const INTERPOLATION_SIZE = 8;
24
+ export declare const interpolationDefinition: ComponentDefinition<InterpolationComponent, Float32Array, InterpolationConfig>;
25
+ export interface SnappableEntity {
26
+ components: {
27
+ transform?: TransformComponent;
28
+ interpolation?: InterpolationComponent;
29
+ };
30
+ }
31
+ export declare function snapEntity(entity: SnappableEntity): void;
32
+ export default interpolationDefinition;
@@ -1,18 +1,30 @@
1
1
  import { type BodyComponent } from './body-component';
2
+ import { type InterpolationComponent } from './interpolation-component';
2
3
  import { type TransformComponent } from './transform-component';
3
4
  import { type VelocityComponent } from './velocity-component';
4
5
  export declare const physicsRegistry: {
5
6
  transform: import("@daneren2005/shared-memory-ecs").ComponentDefinition<TransformComponent, Float32Array<ArrayBufferLike>, import("./transform-component").TransformConfig, import("./transform-component").TransformSerialization>;
6
7
  velocity: import("@daneren2005/shared-memory-ecs").ComponentDefinition<VelocityComponent, Float32Array<ArrayBufferLike>, import("./velocity-component").VelocityConfig, import("./velocity-component").VelocityConfig>;
7
8
  body: import("@daneren2005/shared-memory-ecs").ComponentDefinition<BodyComponent, Uint32Array<ArrayBufferLike>, import("./body-component").BodyConfig, object>;
9
+ interpolation: import("@daneren2005/shared-memory-ecs").ComponentDefinition<InterpolationComponent, Float32Array<ArrayBufferLike>, import("./interpolation-component").InterpolationConfig, object>;
8
10
  };
9
11
  export type PhysicsComponents = {
10
12
  transform: TransformComponent;
11
13
  velocity: VelocityComponent;
12
14
  body: BodyComponent;
15
+ interpolation: InterpolationComponent;
16
+ };
17
+ export type InterpolationComponents = {
18
+ transform: TransformComponent;
19
+ interpolation: InterpolationComponent;
13
20
  };
14
21
  export type PhysicsUpdateComponents = {
15
22
  transform: Float32Array;
16
23
  velocity: Float32Array;
17
24
  body?: Uint32Array;
25
+ interpolation?: Float32Array;
26
+ };
27
+ export type InterpolationUpdateComponents = {
28
+ transform: Float32Array;
29
+ interpolation: Float32Array;
18
30
  };
package/dist/index.d.ts CHANGED
@@ -7,14 +7,20 @@ export type { VelocityComponent, VelocityConfig, VelocitySerialization, } from '
7
7
  export { default as bodyDefinition, canCollide } from './components/body-component';
8
8
  export { SHAPE_RECTANGLE, SHAPE_CIRCLE, SHAPE_CAPSULE, DEFAULT_COLLIDE_CATEGORY, DEFAULT_COLLIDE_MASK, BODY_SHAPE_INDEX, BODY_CATEGORY_INDEX, BODY_MASK_INDEX, BODY_SIZE, } from './components/body-component';
9
9
  export type { BodyComponent, BodyConfig, } from './components/body-component';
10
+ export { default as interpolationDefinition, snapEntity } from './components/interpolation-component';
11
+ 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';
12
+ export type { InterpolationComponent, InterpolationConfig, SnappableEntity, } from './components/interpolation-component';
10
13
  export { physicsRegistry } from './components/registry';
11
- export type { PhysicsComponents, PhysicsUpdateComponents } from './components/registry';
14
+ export type { PhysicsComponents, PhysicsUpdateComponents, InterpolationComponents, InterpolationUpdateComponents, } from './components/registry';
12
15
  export { shapesOverlap, shapeHalfWidth, shapeHalfHeight, shapeIsEmpty, shapeRadius, capsuleHalfLength, boundsHalfWidth, boundsHalfHeight, orientedBoxesOverlap, segmentSegmentDistanceSquared, segmentBoxDistanceSquared, pointSegmentDistanceSquared, } from './math/shapes';
13
16
  export { default as CollisionBroadphase, COLLIDABLE_QUERY } from './systems/collision';
14
17
  export type { CollisionComponents, CollisionEntity, CollisionFunction, MovingEntity, SweepResult } from './systems/collision';
15
18
  export { default as SpatialIndex } from './systems/spatial-index';
16
19
  export type { SpatialComponents, SpatialEntity, SpatialFilter } from './systems/spatial-index';
17
20
  export { default as physicsUpdate, createPhysicsUpdate, POSITION_UPDATED_EVENT } from './systems/physics-update';
18
- export type { PhysicsUpdateFunction, PhysicsUpdateMetadata, PhysicsUpdateOptions } from './systems/physics-update';
19
- export { default as PhysicsSystem } from './systems/physics-system';
21
+ export type { PhysicsUpdateFunction, PhysicsUpdateMetadata, PhysicsUpdateOptions, PhysicsWorld } from './systems/physics-update';
22
+ export { default as PhysicsSystem, DEFAULT_PHYSICS_STEP_MS } from './systems/physics-system';
20
23
  export type { PhysicsSystemConfig } from './systems/physics-system';
24
+ export { default as interpolationUpdate } from './systems/interpolation-update';
25
+ export { default as InterpolationSystem } from './systems/interpolation-system';
26
+ export type { InterpolationSystemConfig } from './systems/interpolation-system';