@daneren2005/shared-memory-physics 1.5.0 → 1.6.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
@@ -18,8 +18,9 @@ npm install @daneren2005/shared-memory-physics @daneren2005/shared-memory-ecs @d
18
18
 
19
19
  | Component | Config | Serialization | Block |
20
20
  | --------------- | -------------------------------------------- | -------------------------- | ---------------------------------------------- |
21
+ | `dynamics` | - | `accelerationX?`, `accelerationY?`, `mass?` | `Float32Array` - acceleration and inverse mass |
21
22
  | `transform` | `width`, `height`, `angle?` / `radius` | `x`, `y`, `angle` | `Float32Array` – where, how big, facing |
22
- | `velocity` | – | `velocityX?`, `velocityY?` | `Float32Array` – world units per second |
23
+ | `velocity` | – | `velocityX?`, `velocityY?`, `damping?` | `Float32Array` – world units per second |
23
24
  | `body` | `shape?`, `collideCategory?`, `collideMask?`, `sensor?`, `continuousCollisionDetection?` | – | `Uint32Array` – what shape, what collides with |
24
25
  | `interpolation` | `interpolate` | – | `Float32Array` – where to *draw* it |
25
26
 
@@ -27,6 +28,34 @@ Entity configs are flat and shared across every component, so velocity is keyed
27
28
  rather than `x` / `y` (which already belong to the transform). Either velocity axis on its own is enough to
28
29
  load one, and the missing one defaults to `0`.
29
30
 
31
+ `damping` is optional linear damping in fraction of speed per second: with it set, a mover started once bleeds
32
+ speed off every step and coasts to a stop instead of gliding forever. It is applied implicitly as
33
+ `velocity / (1 + damping * seconds)`, so it stays stable at any step size and never reverses direction. It
34
+ defaults to `0` (no damping) and must be finite and at least zero. An explicit velocity assignment (a queued
35
+ `queueVelocity`, or writing `velocityX` directly) is authoritative for that step and bypasses damping until the
36
+ entity coasts again.
37
+
38
+ ```ts
39
+ const shot = world.loadEntity({ x: 0, y: 0, radius: 3, velocityX: 400, damping: 2 });
40
+ ```
41
+
42
+ `accelerationX` / `accelerationY` are persistent world units per second squared. Either one (or `mass`) loads a
43
+ `dynamics` component; missing acceleration axes default to `0` and mass defaults to `1`. Physics uses
44
+ semi-implicit Euler: it changes velocity by `acceleration * seconds` first, then moves by that new velocity in
45
+ the same fixed step. That puts gravity on the physics backend instead of tying it to render frames:
46
+
47
+ ```ts
48
+ world.loadEntity({
49
+ x: 0, y: 0, width: 20, height: 40,
50
+ velocityX: 0, velocityY: 0,
51
+ accelerationY: 1600,
52
+ });
53
+ ```
54
+
55
+ Mass is stored as inverse mass and round-trips through `save`; persistent acceleration does not depend on it.
56
+ Mass must be finite and greater than zero, and acceleration must be finite. An entity without `dynamics` has
57
+ effective mass `1` when a force or impulse is queued for it.
58
+
30
59
  The transform is split in two. **Config** is what your entity template says - `width` and `height` are
31
60
  required, `angle` defaults to `0` - and a size is what loads the component at all: a config with neither a
32
61
  width nor a height does not get a transform. **Serialization** is the live state that changes as the world
@@ -52,14 +81,15 @@ its shape and categories back from the template rather than from the save. See
52
81
 
53
82
  ## Body shapes
54
83
 
55
- An entity is one of three shapes, and all three live in the same transform - so which one it is changes what
56
- that width and height *mean* rather than where they are kept:
84
+ An entity is one of four shapes. The transform still owns its position, size and angle; a polygon additionally
85
+ has a `polygon` component holding its outline:
57
86
 
58
87
  | `shape` | Config | What it is |
59
88
  | ----------------- | ------------------------------------- | ------------------------------------------------------------- |
60
89
  | `SHAPE_RECTANGLE` | `width`, `height` | a `width` x `height` box turned to `angle` – the default |
61
90
  | `SHAPE_CIRCLE` | `radius` | a circle of diameter `width`, which `angle` does not affect |
62
91
  | `SHAPE_CAPSULE` | `width`, `height`, `shape` | `width` from end to end, `height` thick, lying along `angle` |
92
+ | `SHAPE_POLYGON` | `vertices` | a convex 3-16 vertex outline, scaled and turned by its transform |
63
93
 
64
94
  ```ts
65
95
  import { SHAPE_CAPSULE } from '@daneren2005/shared-memory-physics';
@@ -67,21 +97,30 @@ import { SHAPE_CAPSULE } from '@daneren2005/shared-memory-physics';
67
97
  world.loadEntity({ x: 0, y: 0, width: 10, height: 4 }); // a rectangle
68
98
  world.loadEntity({ x: 0, y: 0, radius: 5 }); // a circle
69
99
  world.loadEntity({ x: 0, y: 0, width: 40, height: 10, shape: SHAPE_CAPSULE }); // a capsule
100
+ world.loadEntity({ x: 0, y: 0, vertices: [[-8, -6], [10, 0], [-8, 6]] }); // a triangle
70
101
  ```
71
102
 
103
+ Polygon vertices are local to the entity. They must describe a convex boundary in clockwise or counter-clockwise
104
+ order; concave, collinear, non-finite, and out-of-range outlines throw during loading. Their bounding box becomes
105
+ the transform's initial `width` and `height`, and the stored vertices are normalized around that box's centre.
106
+ Changing the transform size therefore scales the polygon, while `angle` rotates it around that centre. Give
107
+ `vertices` by themselves rather than alongside `radius` or `width`/`height`.
108
+
72
109
  `radius` is the same size said differently: it loads as a `width` and a `height` of the diameter, so there is
73
110
  still only one size in the block however the config spelled it. Giving both a `radius` and a `width` throws
74
111
  rather than one quietly winning. A config that says nothing about its `shape` is read off the size it gave - a
75
- `radius` describes a circle, anything else a rectangle - so **only capsules have to name their shape**, and an
112
+ `vertices` describes a polygon, `radius` describes a circle, and anything else a rectangle - so **only capsules
113
+ have to name their shape**, and an
76
114
  unrecognised one throws rather than quietly becoming a rectangle.
77
115
 
78
116
  A capsule's `width` is its **whole** length, caps included, and it lies along the way the entity faces: a 40x10
79
117
  capsule is a 30-long core with a 5 radius cap at each end, and it is the same 10 thick all the way down. One
80
118
  told it is no longer than it is wide collapses to a circle rather than turning itself inside out.
81
119
 
82
- Because a circle is a capsule whose core has no length, all three shapes are *an oriented core grown by a
120
+ Because a circle is a capsule whose core has no length, the three primitive shapes are *an oriented core grown by a
83
121
  radius* - a box with no radius, a point, or a segment - which leaves only three overlap tests rather than one
84
- per pairing: box against box (the separating axis test), core against core, and core against box. Touching
122
+ per primitive pairing: box against box (the separating axis test), core against core, and core against box.
123
+ Those fast paths remain unchanged; only a pair involving a polygon enters the polygon separating-axis test. Touching
85
124
  exactly still counts as **not** overlapping, whichever pair it is, and a shape with no area never overlaps
86
125
  anything. Which of the two sizes decides that depends on the shape: a circle with no height is a perfectly
87
126
  good circle, while a rectangle with no height is nothing at all.
@@ -92,7 +131,9 @@ such direction. It splits the same three ways off the same geometry, so the dire
92
131
  actually hit at the angle the pair is turned to - not the nearest world axis. This is what the native bounce
93
132
  reflects around; a game reaching for its own response can use it the same way.
94
133
 
95
- `shapesOverlap` is exported, along with `shapeHalfWidth` / `shapeHalfHeight` (the axis-aligned box a shape is
134
+ `shapesOverlap` and `contactNormal` are the allocation-free primitive-shape functions. Their polygon counterparts
135
+ are `polygonShapesOverlap` and `polygonContactNormal`, which additionally take the polygon component blocks.
136
+ `shapeHalfWidth` / `shapeHalfHeight` (the axis-aligned box a shape is
96
137
  indexed under), `shapeRadius`, `shapeIsEmpty`, `capsuleHalfLength`, and the distance primitives underneath -
97
138
  `segmentSegmentDistanceSquared`, `segmentBoxDistanceSquared` and `pointSegmentDistanceSquared`, each of which
98
139
  takes an optional `out` vector and fills it with the gap it measured. `orientedBoxesOverlap`, `boundsHalfWidth`
@@ -119,8 +160,9 @@ console.log(entity.components.transform!.x); // 0
119
160
 
120
161
  ## PhysicsSystem
121
162
 
122
- `PhysicsSystem` moves every entity that has both a `transform` and a `velocity`, integrating velocity into
123
- position once per run - and, once you give it an update built by [`createPhysicsUpdate`](#collisions), tells
163
+ `PhysicsSystem` moves every entity that has both a `transform` and a `velocity`, integrating optional acceleration
164
+ and queued commands into velocity and then velocity into position once per run - and, once you give it an update built by
165
+ [`createPhysicsUpdate`](#collisions), tells
124
166
  you which entities have run into each other. It is generic over your full component map, so your world passes
125
167
  straight in:
126
168
 
@@ -133,8 +175,49 @@ await world.init();
133
175
  world.update(1000); // one second: the entity above is now at x = 10
134
176
  ```
135
177
 
136
- `elapsedTime` is in milliseconds (what `BaseWorld#update` is driven with) and velocity is in world units per
137
- **second**, so a run covering 16ms moves an entity 16/1000ths of its velocity.
178
+ `elapsedTime` is in milliseconds (what `BaseWorld#update` is driven with), velocity is in world units per
179
+ **second**, and acceleration is in world units per **second squared**. A run covering 16ms first adds 16/1000ths
180
+ of the acceleration to velocity, then moves 16/1000ths of that updated velocity.
181
+
182
+ ### Dynamics commands
183
+
184
+ Queue forces, impulses, or velocity assignments on the `PhysicsSystem` that owns the mover. A force acts for the
185
+ duration of the next physics run; an impulse changes velocity once without being scaled by that run's duration.
186
+ Both are divided by the entity's mass. A velocity assignment replaces only the axes it names, after acceleration,
187
+ force, and impulse integration but before movement, so it is exact and independent of mass:
188
+
189
+ ```ts
190
+ const physics = world.addSystem(new PhysicsSystem(world));
191
+
192
+ physics.queueForce(entity, 0, 20); // next run: acceleration += force / mass
193
+ physics.queueImpulse(entity, 12, -40); // next run: velocity += impulse / mass
194
+ physics.queueVelocity(entity, { velocityY: -980 }); // next run: set y without changing x
195
+ ```
196
+
197
+ The methods also accept an entity id. Forces and impulses queued for the same entity before dispatch are summed;
198
+ the last velocity assignment for each axis wins. Zero force/impulse vectors and empty velocity assignments are
199
+ ignored. Non-finite values throw immediately.
200
+
201
+ Commands have a strict run boundary. A command queued before dispatch belongs to that run and is consumed once.
202
+ A command queued while a worker run is in flight belongs to the following run. A command is still consumed if
203
+ its entity is absent from the mover query because it was removed, filtered, scoped out, or in a skipped group;
204
+ queue it again when the entity becomes eligible. In a sharded simulation, queue on the system whose mover filter
205
+ owns the entity.
206
+
207
+ The same methods are available on `world` inside a `createPhysicsUpdate` collision callback, with entity ids:
208
+
209
+ ```ts
210
+ onCollision(world, self, other, queries, callbacks, contact) {
211
+ world.queueVelocity(self.entityId, {
212
+ velocityX: contact.normalX * 480,
213
+ velocityY: -720,
214
+ });
215
+ }
216
+ ```
217
+
218
+ Callback commands always target the next run and survive between runs in either backend. They combine with
219
+ commands queued through `PhysicsSystem`: forces and impulses add together, while a newer main-thread velocity
220
+ assignment replaces a callback assignment on each axis it names.
138
221
 
139
222
  ### The step
140
223
 
@@ -167,8 +250,8 @@ class GamePhysicsSystem extends PhysicsSystem<Components, GameComponents, GameWo
167
250
  }
168
251
  ```
169
252
 
170
- `GameWorld` extends `PhysicsWorld` (which is `ComponentSystemWorld` plus that `tick`) rather than
171
- `ComponentSystemWorld` directly.
253
+ `GameWorld` extends `PhysicsWorld` (which is `EntityWorkerSystemWorld` plus that `tick`) rather than
254
+ `EntityWorkerSystemWorld` directly.
172
255
 
173
256
  A run's moves are reported back to the main thread as a single `position-updated` event **on the system**,
174
257
  carrying the ids of everything that moved, so anything you keep keyed off position can follow the moves
@@ -218,10 +301,10 @@ lives in your game and is handed to the system through `getWorker`:
218
301
 
219
302
  ```ts
220
303
  // physics.worker.ts
221
- import { createComponentWorker } from '@daneren2005/shared-memory-ecs';
304
+ import { createEntitySystemWorker } from '@daneren2005/shared-memory-ecs';
222
305
  import { physicsUpdate } from '@daneren2005/shared-memory-physics';
223
306
 
224
- createComponentWorker(self, physicsUpdate);
307
+ createEntitySystemWorker(self, physicsUpdate);
225
308
  ```
226
309
 
227
310
  ```ts
@@ -375,7 +458,7 @@ the same entry fraction the sweep already refines) and **defer the kill by one r
375
458
  needs to play that last stride onto the impact before the entity is gone:
376
459
 
377
460
  ```ts
378
- onCollision(world, self, other, queries, callbacks) {
461
+ onCollision(world, self, other, queries, callbacks, contact) {
379
462
  // `self` (a fast sensor bullet) just swept through `other`. Draw it reaching `other`, then die - rather than
380
463
  // vanishing a stride short.
381
464
  world.dieAtImpact(self, other);
@@ -433,7 +516,6 @@ instead, which is also the only one that costs nothing per entity for the collid
433
516
  // game-physics-update.ts - imported by *both* the worker file and the system below
434
517
  import {
435
518
  createPhysicsUpdate,
436
- VELOCITY_X_INDEX,
437
519
  VELOCITY_Y_INDEX,
438
520
  type PhysicsUpdateComponents,
439
521
  } from '@daneren2005/shared-memory-physics';
@@ -445,10 +527,9 @@ export type GameUpdateComponents = PhysicsUpdateComponents & { health?: Float32A
445
527
  export const gamePhysicsUpdate = createPhysicsUpdate<Components, GameUpdateComponents>({
446
528
  // Extra components to send along to the callback, on top of the transform and velocity.
447
529
  optional: ['health'],
448
- onCollision(world, self, other, queries, callbacks) {
449
- // `self` just moved into `other`. Bounce it back off whatever it hit...
450
- self.components.velocity[VELOCITY_X_INDEX] *= -1;
451
- self.components.velocity[VELOCITY_Y_INDEX] *= -1;
530
+ onCollision(world, self, other, queries, callbacks, contact) {
531
+ // `self` just moved into `other`. Throw it away from the contact on the next run.
532
+ world.queueImpulse(self.entityId, contact.normalX * 40, contact.normalY * 40);
452
533
 
453
534
  // ...and take a bite out of the thing it ran into, if that is something with health.
454
535
  const health = other.components.health;
@@ -462,10 +543,10 @@ export const gamePhysicsUpdate = createPhysicsUpdate<Components, GameUpdateCompo
462
543
 
463
544
  ```ts
464
545
  // physics.worker.ts
465
- import { createComponentWorker } from '@daneren2005/shared-memory-ecs';
546
+ import { createEntitySystemWorker } from '@daneren2005/shared-memory-ecs';
466
547
  import { gamePhysicsUpdate } from './game-physics-update';
467
548
 
468
- createComponentWorker(self, gamePhysicsUpdate);
549
+ createEntitySystemWorker(self, gamePhysicsUpdate);
469
550
  ```
470
551
 
471
552
  ```ts
@@ -478,12 +559,28 @@ world.addSystem(new PhysicsSystem<Components, GameUpdateComponents>(world, {
478
559
  `createPhysicsUpdate` stamps what it needs onto the function it returns, so `optional` and the collidable
479
560
  query come across with `updateFunction` and never have to be repeated on the system.
480
561
 
562
+ The final `contact` argument carries the unit normal at first touch as `normalX` and `normalY`. It points from
563
+ `other` toward `self`; negate both values when applying it to `other`. This is the same shape-accurate direction
564
+ used by native bounce, including rotated rectangles, circles, capsules, and polygons. A continuous sensor that
565
+ crossed an entire target gets the normal where it entered, not from wherever its step ended after passing through.
566
+ Exactly coincident shapes have no unique direction and report `(0, 0)`.
567
+
568
+ ```ts
569
+ onCollision(world, self, other, queries, callbacks, contact) {
570
+ // Screen coordinates grow downwards, so a normal pointing up means self landed on other.
571
+ if (contact.normalY < -0.5) {
572
+ self.components.velocity[VELOCITY_Y_INDEX] = 0;
573
+ }
574
+ }
575
+ ```
576
+
481
577
  **Once per pair, per run.** Both sides of a contact find it - one by sweeping into it, the other by overlapping
482
578
  it on its own turn - and the callback fires once, for whichever got there first. `self` is that entity, with
483
579
  every block the system asked for; `other` is the one it hit, where only `transform` is guaranteed. **The other
484
580
  side gets no call of its own**, so a callback that only touches `self` leaves it untouched: apply damage, tag a
485
581
  kill, or raise an event for *both* entities in the one call. Which of the two is `self` follows update order
486
- and is not something to depend on. An entity with no velocity is never `self`, because the system never moves
582
+ and is not something to depend on. The normal follows that same ordering and always points away from `other`.
583
+ An entity with no velocity is never `self`, because the system never moves
487
584
  it, but it is still found as `other`.
488
585
 
489
586
  Writes go straight into shared memory, so a callback can bounce an entity by flipping its velocity, or push a
@@ -495,6 +592,11 @@ event of your own - the same one the move above reports itself through. To kill
495
592
  still have it drawn reaching what it hit, call `world.dieAtImpact(dying, other)` in place of `entityDied` - see
496
593
  [`dieAtImpact`](#a-moving-death-dieatimpact).
497
594
 
595
+ For a dynamics response that does not touch raw blocks, the callback world has `queueForce`, `queueImpulse`, and
596
+ `queueVelocity`. They take an entity id and retain the command inside whichever backend is running, including a
597
+ worker. A callback happens after this run's move, so its command is integrated at the start of the next run;
598
+ deferring it also makes the outcome independent of which side of the pair updated first.
599
+
498
600
  **What collides.** Everything with a transform and a `body`, not only the entities the system moves, so a
499
601
  ship can run into a station that has no velocity of its own. Shapes that only just touch do not count as
500
602
  overlapping, and one with no area never collides at all.
@@ -528,6 +630,22 @@ Two details worth knowing:
528
630
  forwards each run, so it reports no position change while it pushes - but it does keep reporting the
529
631
  collision for as long as it keeps pushing.
530
632
 
633
+ By default, the sweep changes position but leaves velocity alone for an entity without `bounciness`, preserving
634
+ the original behavior for games that implement their own response. Set `stopVelocityOnContact: true` on
635
+ `createPhysicsUpdate` to opt into a non-bouncing solid response: physics removes only the velocity component
636
+ pointing into the contact normal, so bodies stop naturally against floors, ceilings, walls, and slopes while
637
+ keeping their motion along the surface.
638
+
639
+ ```ts
640
+ export const gamePhysicsUpdate = createPhysicsUpdate<Components>({
641
+ stopVelocityOnContact: true,
642
+ });
643
+ ```
644
+
645
+ This applies to both sides of each solid contact when they have velocity. Sensors still pass through, and an
646
+ entity with a `bounciness` component keeps its configured bounce instead; `bounciness: 0` remains equivalent to
647
+ removing the inward velocity component.
648
+
531
649
  **What it costs.** A move is checked where it *ends*, so a clear one - which is nearly every move - is a single
532
650
  shape test against each thing near where it lands, and nothing more. Only a move that is genuinely stopping
533
651
  somewhere pays for working out where: contact is then halved in on rather than solved, since three shapes at any
@@ -636,6 +754,11 @@ it rather than adding a second system - the block offsets it reads are exported
636
754
  import { physicsUpdate, TRANSFORM_X_INDEX, VELOCITY_X_INDEX } from '@daneren2005/shared-memory-physics';
637
755
  ```
638
756
 
757
+ Both `physicsUpdate` and updates built by `createPhysicsUpdate` integrate `dynamics` automatically. A custom
758
+ update that replaces library movement owns that step too: request `dynamics` through the system's `optional`
759
+ components and call `integrateDynamics(world, entityId, components.velocity, components.dynamics)` before
760
+ turning velocity into movement.
761
+
639
762
  `CollisionBroadphase` is available the same way, for a system that wants the collision detection without the
640
763
  movement. Build one per run from a list of `{ entityId, components }` and how many **seconds** the run covers
641
764
  (that is what sizes the room left for movement), then ask it what any given entity is overlapping:
@@ -643,10 +766,17 @@ movement. Build one per run from a list of `{ entityId, components }` and how ma
643
766
  ```ts
644
767
  import { CollisionBroadphase, COLLIDABLE_QUERY } from '@daneren2005/shared-memory-physics';
645
768
 
646
- const broadphase = new CollisionBroadphase(queries[COLLIDABLE_QUERY], world.elapsedTime / 1000);
769
+ const broadphase = new CollisionBroadphase(
770
+ queries[COLLIDABLE_QUERY],
771
+ world.elapsedTime / 1000,
772
+ world.dynamicsCommands,
773
+ );
647
774
  broadphase.forEachOverlapping({ entityId, components }, other => { /* ... */ });
648
775
  ```
649
776
 
777
+ The third argument is optional, but a custom update that applies queued commands in the same run must pass it so
778
+ the index leaves enough travel room for the resulting velocity.
779
+
650
780
  `sweep` is the other half, for a system that moves things itself and wants the same
651
781
  [stopping at the edge](#stopping-at-the-edge). Hand it the move you were about to make and it says how much of
652
782
  it is available, along with whatever the rest of it would have run into:
@@ -690,6 +820,10 @@ box around their actual outline. A direct transform/body edit outside the physic
690
820
  `world.updateSpatialEntity(entity)`. A custom physics wrapper that changes a transform after calling the library
691
821
  update can call `updateSpatialMap(workerWorld, entityId, components)` instead.
692
822
 
823
+ Games with registered ECS wrapper classes can pass their entity union as `PhysicalWorld`'s second generic. The
824
+ world's entity map, lifecycle callbacks, and spatial-query results then retain that union instead of widening to
825
+ `BaseEntity`.
826
+
693
827
  `PhysicsSystem` requires a `PhysicalWorld` (or another `BaseWorld` that implements `PhysicalWorldSource`) and
694
828
  automatically shares its map with the physics worker. Another worker system can receive the same map by calling
695
829
  `addPhysicalWorldData(sourceWorld, runWorld)` from its system's `addDataToWorld`, then `getSpatialMap(runWorld)` in
@@ -1,4 +1,5 @@
1
1
  import type { ComponentDefinition } from '@daneren2005/shared-memory-ecs';
2
+ import type { PolygonVertex } from './polygon-component';
2
3
  export interface BodyComponent {
3
4
  index: number;
4
5
  shape: number;
@@ -10,6 +11,7 @@ export interface BodyComponent {
10
11
  export declare const SHAPE_RECTANGLE = 1;
11
12
  export declare const SHAPE_CIRCLE = 2;
12
13
  export declare const SHAPE_CAPSULE = 3;
14
+ export declare const SHAPE_POLYGON = 4;
13
15
  export declare const DEFAULT_COLLIDE_CATEGORY = 1;
14
16
  export declare const DEFAULT_COLLIDE_MASK = 4294967295;
15
17
  export declare function canCollide(categoryA: number, maskA: number, categoryB: number, maskB: number): boolean;
@@ -18,6 +20,7 @@ export interface BodyConfig {
18
20
  height?: number;
19
21
  shape?: number;
20
22
  radius?: number;
23
+ vertices?: ReadonlyArray<PolygonVertex>;
21
24
  collideCategory?: number;
22
25
  collideMask?: number;
23
26
  sensor?: boolean;
@@ -27,10 +30,10 @@ export declare const BODY_FLAGS_INDEX = 0;
27
30
  export declare const BODY_CATEGORY_INDEX = 1;
28
31
  export declare const BODY_MASK_INDEX = 2;
29
32
  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 const BODY_DYING_FLAG = 16;
33
+ export declare const BODY_SHAPE_MASK = 7;
34
+ export declare const BODY_SENSOR_FLAG = 8;
35
+ export declare const BODY_CCD_FLAG = 16;
36
+ export declare const BODY_DYING_FLAG = 32;
34
37
  export declare function bodyShape(body: Uint32Array): number;
35
38
  export declare function isSensor(body: Uint32Array): boolean;
36
39
  export declare function isContinuous(body: Uint32Array): boolean;
@@ -0,0 +1,19 @@
1
+ import type { ComponentDefinition } from '@daneren2005/shared-memory-ecs';
2
+ export interface DynamicsComponent {
3
+ index: number;
4
+ accelerationX: number;
5
+ accelerationY: number;
6
+ mass: number;
7
+ }
8
+ export interface DynamicsConfig {
9
+ accelerationX?: number;
10
+ accelerationY?: number;
11
+ mass?: number;
12
+ }
13
+ export type DynamicsSerialization = DynamicsConfig;
14
+ export declare const DYNAMICS_ACCELERATION_X_INDEX = 0;
15
+ export declare const DYNAMICS_ACCELERATION_Y_INDEX = 1;
16
+ export declare const DYNAMICS_INVERSE_MASS_INDEX = 2;
17
+ export declare const DYNAMICS_SIZE = 3;
18
+ export declare const dynamicsDefinition: ComponentDefinition<DynamicsComponent, Float32Array, DynamicsConfig, DynamicsSerialization>;
19
+ export default dynamicsDefinition;
@@ -0,0 +1,22 @@
1
+ import type { ComponentDefinition } from '@daneren2005/shared-memory-ecs';
2
+ export type PolygonVertex = readonly [x: number, y: number];
3
+ export interface PolygonConfig {
4
+ vertices?: ReadonlyArray<PolygonVertex>;
5
+ }
6
+ export interface PolygonComponent {
7
+ index: number;
8
+ readonly block: Float32Array;
9
+ readonly vertexCount: number;
10
+ }
11
+ export declare const MAX_POLYGON_VERTICES = 16;
12
+ export declare const POLYGON_VERTEX_COUNT_INDEX = 0;
13
+ export declare const POLYGON_VERTICES_INDEX = 1;
14
+ export declare const POLYGON_SIZE: number;
15
+ export interface PreparedPolygon {
16
+ width: number;
17
+ height: number;
18
+ vertices: Array<number>;
19
+ }
20
+ export declare const polygonDefinition: ComponentDefinition<PolygonComponent, Float32Array, PolygonConfig>;
21
+ export declare function preparePolygon(vertices: ReadonlyArray<PolygonVertex> | undefined): PreparedPolygon;
22
+ export default polygonDefinition;
@@ -1,20 +1,26 @@
1
1
  import { type BodyComponent } from './body-component';
2
2
  import { type BouncinessComponent } from './bounciness-component';
3
+ import { type DynamicsComponent } from './dynamics-component';
3
4
  import { type InterpolationComponent } from './interpolation-component';
5
+ import { type PolygonComponent } from './polygon-component';
4
6
  import { type TransformComponent } from './transform-component';
5
7
  import { type VelocityComponent } from './velocity-component';
6
8
  export declare const physicsRegistry: {
7
9
  transform: import("@daneren2005/shared-memory-ecs").ComponentDefinition<TransformComponent, Float32Array<ArrayBufferLike>, import("./transform-component").TransformConfig, import("./transform-component").TransformSerialization>;
8
10
  velocity: import("@daneren2005/shared-memory-ecs").ComponentDefinition<VelocityComponent, Float32Array<ArrayBufferLike>, import("./velocity-component").VelocityConfig, import("./velocity-component").VelocityConfig>;
9
11
  body: import("@daneren2005/shared-memory-ecs").ComponentDefinition<BodyComponent, Uint32Array<ArrayBufferLike>, import("./body-component").BodyConfig, object>;
12
+ polygon: import("@daneren2005/shared-memory-ecs").ComponentDefinition<PolygonComponent, Float32Array<ArrayBufferLike>, import("./polygon-component").PolygonConfig, object>;
10
13
  bounciness: import("@daneren2005/shared-memory-ecs").ComponentDefinition<BouncinessComponent, Float32Array<ArrayBufferLike>, import("./bounciness-component").BouncinessConfig, object>;
14
+ dynamics: import("@daneren2005/shared-memory-ecs").ComponentDefinition<DynamicsComponent, Float32Array<ArrayBufferLike>, import("./dynamics-component").DynamicsConfig, import("./dynamics-component").DynamicsConfig>;
11
15
  interpolation: import("@daneren2005/shared-memory-ecs").ComponentDefinition<InterpolationComponent, Float32Array<ArrayBufferLike>, import("./interpolation-component").InterpolationConfig, object>;
12
16
  };
13
17
  export type PhysicsComponents = {
14
18
  transform: TransformComponent;
15
19
  velocity: VelocityComponent;
16
20
  body: BodyComponent;
21
+ polygon: PolygonComponent;
17
22
  bounciness: BouncinessComponent;
23
+ dynamics: DynamicsComponent;
18
24
  interpolation: InterpolationComponent;
19
25
  };
20
26
  export type InterpolationComponents = {
@@ -25,7 +31,9 @@ export type PhysicsUpdateComponents = {
25
31
  transform: Float32Array;
26
32
  velocity: Float32Array;
27
33
  body?: Uint32Array;
34
+ polygon?: Float32Array;
28
35
  bounciness?: Float32Array;
36
+ dynamics?: Float32Array;
29
37
  interpolation?: Float32Array;
30
38
  entity?: Uint32Array;
31
39
  };
@@ -1,4 +1,5 @@
1
1
  import type { ComponentDefinition } from '@daneren2005/shared-memory-ecs';
2
+ import { type PolygonVertex } from './polygon-component';
2
3
  export interface TransformComponent {
3
4
  index: number;
4
5
  x: number;
@@ -12,6 +13,7 @@ export interface TransformConfig {
12
13
  height: number;
13
14
  angle?: number;
14
15
  radius?: number;
16
+ vertices?: ReadonlyArray<PolygonVertex>;
15
17
  }
16
18
  export interface TransformSerialization {
17
19
  x: number;
@@ -3,14 +3,17 @@ export interface VelocityComponent {
3
3
  index: number;
4
4
  velocityX: number;
5
5
  velocityY: number;
6
+ damping: number;
6
7
  }
7
8
  export interface VelocityConfig {
8
9
  velocityX?: number;
9
10
  velocityY?: number;
11
+ damping?: number;
10
12
  }
11
13
  export type VelocitySerialization = VelocityConfig;
12
14
  export declare const VELOCITY_X_INDEX = 0;
13
15
  export declare const VELOCITY_Y_INDEX = 1;
14
- export declare const VELOCITY_SIZE = 2;
16
+ export declare const VELOCITY_DAMPING_INDEX = 2;
17
+ export declare const VELOCITY_SIZE = 3;
15
18
  export declare const velocityDefinition: ComponentDefinition<VelocityComponent, Float32Array, VelocityConfig, VelocitySerialization>;
16
19
  export default velocityDefinition;
package/dist/index.d.ts CHANGED
@@ -2,11 +2,17 @@ export { default as transformDefinition } from './components/transform-component
2
2
  export { TRANSFORM_X_INDEX, TRANSFORM_Y_INDEX, TRANSFORM_WIDTH_INDEX, TRANSFORM_HEIGHT_INDEX, TRANSFORM_ANGLE_INDEX, TRANSFORM_SIZE, } from './components/transform-component';
3
3
  export type { TransformComponent, TransformConfig, TransformSerialization, } from './components/transform-component';
4
4
  export { default as velocityDefinition } from './components/velocity-component';
5
- export { VELOCITY_X_INDEX, VELOCITY_Y_INDEX, VELOCITY_SIZE, } from './components/velocity-component';
5
+ export { VELOCITY_X_INDEX, VELOCITY_Y_INDEX, VELOCITY_DAMPING_INDEX, VELOCITY_SIZE, } from './components/velocity-component';
6
6
  export type { VelocityComponent, VelocityConfig, VelocitySerialization, } from './components/velocity-component';
7
+ export { default as dynamicsDefinition } from './components/dynamics-component';
8
+ export { DYNAMICS_ACCELERATION_X_INDEX, DYNAMICS_ACCELERATION_Y_INDEX, DYNAMICS_INVERSE_MASS_INDEX, DYNAMICS_SIZE, } from './components/dynamics-component';
9
+ export type { DynamicsComponent, DynamicsConfig, DynamicsSerialization, } from './components/dynamics-component';
7
10
  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';
11
+ export { SHAPE_RECTANGLE, SHAPE_CIRCLE, SHAPE_CAPSULE, SHAPE_POLYGON, 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
12
  export type { BodyComponent, BodyConfig, } from './components/body-component';
13
+ export { default as polygonDefinition, preparePolygon } from './components/polygon-component';
14
+ export { MAX_POLYGON_VERTICES, POLYGON_VERTEX_COUNT_INDEX, POLYGON_VERTICES_INDEX, POLYGON_SIZE, } from './components/polygon-component';
15
+ export type { PolygonComponent, PolygonConfig, PolygonVertex, PreparedPolygon } from './components/polygon-component';
10
16
  export { default as bouncinessDefinition } from './components/bounciness-component';
11
17
  export { BOUNCINESS_INDEX, BOUNCINESS_SIZE, } from './components/bounciness-component';
12
18
  export type { BouncinessComponent, BouncinessConfig, } from './components/bounciness-component';
@@ -19,14 +25,17 @@ export { default as PhysicalWorld, addPhysicalWorldData, getSpatialMap } from '.
19
25
  export type { PhysicalSystemWorld, PhysicalWorldData, PhysicalWorldSource, PhysicalWorldEntity, PhysicalWorldFilter, PhysicalWorldOptions, } from './world';
20
26
  export { shapesOverlap, contactNormal, shapeHalfWidth, shapeHalfHeight, shapeIsEmpty, shapeRadius, capsuleHalfLength, boundsHalfWidth, boundsHalfHeight, orientedBoxesOverlap, segmentSegmentDistanceSquared, segmentBoxDistanceSquared, pointSegmentDistanceSquared, } from './math/shapes';
21
27
  export type { Vector } from './math/shapes';
28
+ export { polygonShapesOverlap, polygonContactNormal } from './math/polygons';
22
29
  export { default as CollisionBroadphase, COLLIDABLE_QUERY } from './systems/collision';
23
- export type { CollisionComponents, CollisionEntity, CollisionFunction, MoveResult, MovingEntity, SweepResult } from './systems/collision';
30
+ export type { CollisionComponents, CollisionContact, CollisionEntity, CollisionFunction, MoveResult, MovingEntity, SweepResult } from './systems/collision';
24
31
  export { default as SpatialIndex } from './systems/spatial-index';
25
32
  export type { SpatialComponents, SpatialEntity, SpatialFilter } from './systems/spatial-index';
26
33
  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';
34
+ export type { DeathInterpolationEntity, PhysicsCallbackWorld, PhysicsGroupConfig, PhysicsUpdateFunction, PhysicsUpdateMetadata, PhysicsUpdateOptions, PhysicsWorld } from './systems/physics-update';
28
35
  export { default as PhysicsSystem, DEFAULT_PHYSICS_STEP_MS } from './systems/physics-system';
29
- export type { PhysicsSystemConfig } from './systems/physics-system';
36
+ export type { PhysicsSystemConfig, VelocityAssignment } from './systems/physics-system';
37
+ export { default as integrateDynamics } from './systems/dynamics';
38
+ export type { DynamicsCommand, DynamicsCommandBuffer, DynamicsCommandQueue, DynamicsCommands, DynamicsWorld } from './systems/dynamics';
30
39
  export { default as interpolationUpdate } from './systems/interpolation-update';
31
40
  export { default as InterpolationSystem } from './systems/interpolation-system';
32
41
  export type { InterpolationSystemConfig } from './systems/interpolation-system';