@daneren2005/shared-memory-physics 0.0.1 → 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,17 +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
- Each axis an entity actually moved along is reported back to the main thread as a `component-property-updated`
125
- event, 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
- entity.on('component-property-updated', (componentName, prop, value) => {
129
- // 'transform', 'x', 12.5
168
+ import { POSITION_UPDATED_EVENT } from '@daneren2005/shared-memory-physics';
169
+
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
+ }
130
177
  });
131
178
  ```
132
179
 
133
- An axis that did not move says nothing, so a world of still entities stays quiet - and so does an entity held
134
- against something it cannot move past.
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.
192
+
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.
135
200
 
136
201
  The move is written with `addAtomicFloat32` rather than a plain `+=`, so a game system on another thread can
137
202
  add to the same position in the same instant without either move being lost.
@@ -160,13 +225,130 @@ world.addSystem(new PhysicsSystem(world, {
160
225
  Without a `getWorker` (or where Web Workers / `SharedArrayBuffer` are unavailable) the exact same
161
226
  `physicsUpdate` runs in-process on the main thread instead.
162
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
+
163
332
  ## Collisions
164
333
 
165
- `createPhysicsUpdate` builds an update that, right after moving each entity, tells a callback of yours what
166
- that entity has moved into. The callback runs wherever the update runs - on the worker thread, in the same
167
- pass as the movement - so it cannot be handed to `PhysicsSystem` at construction time: functions do not
168
- survive `postMessage`. It reaches the worker by being baked into a module that both the worker file and the
169
- 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.
170
352
 
171
353
  ```ts
172
354
  // game-physics-update.ts - imported by *both* the worker file and the system below
@@ -227,7 +409,8 @@ Writes go straight into shared memory, so a callback can bounce an entity by fli
227
409
  value another thread also touches with the atomics from `@daneren2005/shared-memory-objects`. To reach past
228
410
  the two entities in front of it - to credit a third for a kill, say - a callback can walk
229
411
  `queries[COLLIDABLE_QUERY]`, the full collidable list. Anything that has to happen on the main thread goes
230
- through `callbacks`: `entityDied`, `entityComponentChanged`, `createEntity`.
412
+ through `callbacks`: `entityDied`, `entityComponentChanged`, `createEntity`, and `emitEntityEvent` for an
413
+ event of your own - the same one the move above reports itself through.
231
414
 
232
415
  **What collides.** Everything with a transform and a `body`, not only the entities the system moves, so a
233
416
  ship can run into a station that has no velocity of its own. Shapes that only just touch do not count as
@@ -239,14 +422,17 @@ An entity is never moved into another one. Before anything is written to the tra
239
422
  against everything along its path, and the entity is put down on the edge of whatever is in the way - then
240
423
  `onCollision` runs for it, exactly as it would if the two had ended up overlapping.
241
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
+
242
428
  ```ts
243
- // A ship at x = 0 and a station at x = 30, both 10 wide, with a velocity that asks for 100 in this run.
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.
244
430
  world.update(1000);
245
- console.log(ship.components.transform!.x); // ~20, their edges touching - not 100, and never 25
431
+ console.log(ship.components.transform!.x); // ~20, their edges touching - not 25, and never inside it
246
432
  ```
247
433
 
248
- The whole path is one decision rather than one per entity found along it, so a mover closing on two units in a
249
- line comes to rest against the *near* one however the two happened to come up, and only the near one is
434
+ The move is one decision over everything it lands on rather than one per entity, so a mover ending up on top of
435
+ two units comes to rest against the *near* one however the two happened to come up, and only the near one is
250
436
  reported as a collision - the far one was never reached. An entity wedged between two at the same moment gets a
251
437
  call for each.
252
438
 
@@ -259,13 +445,18 @@ Two details worth knowing:
259
445
  forwards each run, so it reports no position change while it pushes - but it does keep reporting the
260
446
  collision for as long as it keeps pushing.
261
447
 
262
- Contact is found by walking the path and then halving in on it, rather than solved: three shapes at any
448
+ **What it costs.** A move is checked where it *ends*, so a clear one - which is nearly every move - is a single
449
+ shape test against each thing near where it lands, and nothing more. Only a move that is genuinely stopping
450
+ somewhere pays for working out where: contact is then halved in on rather than solved, since three shapes at any
263
451
  rotation to one another have no single formula for when a pair first touches, while the overlap test the
264
452
  narrowphase already has answers it for any pair anywhere. The position an entity is put down at is always one
265
- that tested clear, so it is genuinely outside everything - within about `1e-4` of world units of touching. A
266
- move long enough to cross many times the size of what is in the way runs out of that walk's step budget, and
267
- can still pass clean through something very thin; a fixed `deltaBetweenRuns` is the cure if that is ever a real
268
- prospect in your game.
453
+ that tested clear, so it is genuinely outside everything - within about `1e-4` world units of touching.
454
+
455
+ The trade for that is **a move long enough to carry an entity clean past something is not stopped by it**: by
456
+ the time the move is out there is nothing left to land on. That takes a single run covering more ground than the
457
+ thing in the way is thick, so at any normal frame rate it does not arise - a ship crossing 100 units a second
458
+ moves 1.6 of them per frame at 60fps. A fixed `deltaBetweenRuns` puts a ceiling on it if your game has anything
459
+ fast enough to care.
269
460
 
270
461
  ### Collision filtering
271
462
 
@@ -355,11 +546,90 @@ transform[TRANSFORM_Y_INDEX] += moveY * fraction;
355
546
  `blocking` is what the entity came to rest *against*, which `forEachOverlapping` will not report from that
356
547
  resting place: the two are touching rather than through each other.
357
548
 
549
+ ## Spatial queries
550
+
551
+ `SpatialIndex` answers the other kind of question about where things are - who is in this area, and what is
552
+ the closest thing to this point - for the systems that are not about collisions at all: targeting, aggro
553
+ range, spawning somewhere clear, area effects. It is the same R-tree the broadphase is built on, without the
554
+ collide categories or the shape tests.
555
+
556
+ Build one from anything shaped like a query result and it reads the transform (and the body, to know which
557
+ outline that transform describes) off each entity. Only the transform is needed, so it indexes anything with
558
+ a place in the world rather than only what collides:
559
+
560
+ ```ts
561
+ import { SpatialIndex } from '@daneren2005/shared-memory-physics';
562
+
563
+ const index = new SpatialIndex(queries.collidable);
564
+ ```
565
+
566
+ Nothing reads the blocks after that, so the index is a **snapshot** of where everything was when it was
567
+ built - build one per run, in `preRun`, and let every entity in that run search it.
568
+
569
+ | Query | Answers |
570
+ | -------------------------------------------------- | ---------------------------------------------------- |
571
+ | `search(minX, minY, maxX, maxY, filter?)` | everything whose box reaches into that one |
572
+ | `searchAround(x, y, reachX, reachY, filter?)` | the same, written from the middle out |
573
+ | `findNearest(x, y, maxDistance?, filter?)` | the closest one, or `undefined` |
574
+ | `findNearby(x, y, maxResults, maxDistance?, filter?)` | the closest `maxResults`, nearest first |
575
+
576
+ Every one of them takes a `filter`, run per candidate the tree turns up, and hands it the whole entity -
577
+ `{ entityId, components }` - so it can read back any block that came along with the query. That is where the
578
+ things the index cannot know go: the searcher's own id, which side something is on, whether it is worth
579
+ shooting at.
580
+
581
+ ```ts
582
+ const target = index.findNearest(x, y, 150, other => other.entityId !== entityId && isEnemy(other));
583
+ ```
584
+
585
+ `findNearest` walks the tree in distance order rather than searching a box and sorting what comes back, so
586
+ ask it directly rather than building the sort yourself - it settles as soon as the nearest box is reached, and
587
+ `maxDistance` stops it walking the rest of the world when the answer is that there is nobody. Distance is
588
+ measured to an entity's **box**, not to its centre, so a large target is as near as its nearest edge and
589
+ `maxDistance` reads as a range past the hull.
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
+
358
627
  ## Building
359
628
 
360
629
  ```sh
361
630
  npm install
362
- 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/
363
633
  npm run type-check
364
634
  npm test
365
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,12 +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';
12
- export { shapesOverlap, shapeHalfWidth, shapeHalfHeight, shapeIsEmpty, shapeMinHalfExtent, shapeRadius, capsuleHalfLength, boundsHalfWidth, boundsHalfHeight, orientedBoxesOverlap, segmentSegmentDistanceSquared, segmentBoxDistanceSquared, pointSegmentDistanceSquared, } from './math/shapes';
14
+ export type { PhysicsComponents, PhysicsUpdateComponents, InterpolationComponents, InterpolationUpdateComponents, } from './components/registry';
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
- export { default as physicsUpdate, createPhysicsUpdate } from './systems/physics-update';
16
- export type { PhysicsUpdateFunction, PhysicsUpdateMetadata, PhysicsUpdateOptions } from './systems/physics-update';
17
- export { default as PhysicsSystem } from './systems/physics-system';
18
+ export { default as SpatialIndex } from './systems/spatial-index';
19
+ export type { SpatialComponents, SpatialEntity, SpatialFilter } from './systems/spatial-index';
20
+ export { default as physicsUpdate, createPhysicsUpdate, POSITION_UPDATED_EVENT } from './systems/physics-update';
21
+ export type { PhysicsUpdateFunction, PhysicsUpdateMetadata, PhysicsUpdateOptions, PhysicsWorld } from './systems/physics-update';
22
+ export { default as PhysicsSystem, DEFAULT_PHYSICS_STEP_MS } from './systems/physics-system';
18
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';