@daneren2005/shared-memory-physics 1.5.0 → 1.6.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 +155 -25
- package/dist/components/body-component.d.ts +7 -4
- package/dist/components/dynamics-component.d.ts +19 -0
- package/dist/components/polygon-component.d.ts +22 -0
- package/dist/components/registry.d.ts +8 -0
- package/dist/components/transform-component.d.ts +2 -0
- package/dist/components/velocity-component.d.ts +4 -1
- package/dist/index.d.ts +14 -5
- package/dist/index.js +713 -297
- package/dist/index.js.map +1 -1
- package/dist/math/polygons.d.ts +3 -0
- package/dist/systems/bounce.d.ts +2 -1
- package/dist/systems/collision.d.ts +13 -3
- package/dist/systems/dynamics.d.ts +43 -0
- package/dist/systems/interpolation-system.d.ts +2 -2
- package/dist/systems/physics-system.d.ts +13 -4
- package/dist/systems/physics-update.d.ts +10 -4
- package/dist/world.d.ts +2 -2
- package/package.json +3 -2
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
|
|
56
|
-
|
|
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
|
|
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,
|
|
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.
|
|
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`
|
|
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
|
|
123
|
-
position once per run - and, once you give it an update built by
|
|
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)
|
|
137
|
-
**second**,
|
|
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 `
|
|
171
|
-
`
|
|
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 {
|
|
304
|
+
import { createEntitySystemWorker } from '@daneren2005/shared-memory-ecs';
|
|
222
305
|
import { physicsUpdate } from '@daneren2005/shared-memory-physics';
|
|
223
306
|
|
|
224
|
-
|
|
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`.
|
|
450
|
-
self.
|
|
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 {
|
|
546
|
+
import { createEntitySystemWorker } from '@daneren2005/shared-memory-ecs';
|
|
466
547
|
import { gamePhysicsUpdate } from './game-physics-update';
|
|
467
548
|
|
|
468
|
-
|
|
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.
|
|
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(
|
|
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:
|
|
@@ -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 =
|
|
31
|
-
export declare const BODY_SENSOR_FLAG =
|
|
32
|
-
export declare const BODY_CCD_FLAG =
|
|
33
|
-
export declare const BODY_DYING_FLAG =
|
|
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
|
|
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';
|