@daneren2005/shared-memory-physics 1.2.1 → 1.3.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 +98 -16
- package/dist/components/body-component.d.ts +9 -3
- package/dist/components/interpolation-component.d.ts +18 -0
- package/dist/components/registry.d.ts +1 -0
- package/dist/components/transform-component.d.ts +1 -0
- package/dist/index.d.ts +6 -5
- package/dist/index.js +435 -234
- package/dist/index.js.map +1 -1
- package/dist/math/shapes.d.ts +8 -3
- package/dist/systems/bounce.d.ts +10 -3
- package/dist/systems/collision.d.ts +9 -1
- package/dist/systems/physics-system.d.ts +3 -0
- package/dist/systems/physics-update.d.ts +2 -2
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -18,9 +18,9 @@ npm install @daneren2005/shared-memory-physics @daneren2005/shared-memory-ecs @d
|
|
|
18
18
|
|
|
19
19
|
| Component | Config | Serialization | Block |
|
|
20
20
|
| --------------- | -------------------------------------------- | -------------------------- | ---------------------------------------------- |
|
|
21
|
-
| `transform` | `width`, `height`, `angle?` / `radius` | `x`, `y`
|
|
21
|
+
| `transform` | `width`, `height`, `angle?` / `radius` | `x`, `y`, `angle` | `Float32Array` – where, how big, facing |
|
|
22
22
|
| `velocity` | – | `velocityX?`, `velocityY?` | `Float32Array` – world units per second |
|
|
23
|
-
| `body` | `shape?`, `collideCategory?`, `collideMask?` | – | `Uint32Array` – what shape, what collides with |
|
|
23
|
+
| `body` | `shape?`, `collideCategory?`, `collideMask?`, `sensor?`, `continuousCollisionDetection?` | – | `Uint32Array` – what shape, what collides with |
|
|
24
24
|
| `interpolation` | `interpolate` | – | `Float32Array` – where to *draw* it |
|
|
25
25
|
|
|
26
26
|
Entity configs are flat and shared across every component, so velocity is keyed as `velocityX` / `velocityY`
|
|
@@ -30,12 +30,13 @@ load one, and the missing one defaults to `0`.
|
|
|
30
30
|
The transform is split in two. **Config** is what your entity template says - `width` and `height` are
|
|
31
31
|
required, `angle` defaults to `0` - and a size is what loads the component at all: a config with neither a
|
|
32
32
|
width nor a height does not get a transform. **Serialization** is the live state that changes as the world
|
|
33
|
-
runs, so `save()` returns
|
|
34
|
-
|
|
33
|
+
runs, so `save()` returns `x` / `y` and `angle`, while a reloaded entity gets its size back from your own
|
|
34
|
+
template rather than the save.
|
|
35
35
|
|
|
36
36
|
`x` / `y` are the **centre** of the box, not a corner: the box is rotated about that point and collision
|
|
37
37
|
projects out from it in both directions. `angle` is in **radians** counter-clockwise from the +x axis, and
|
|
38
|
-
|
|
38
|
+
the physics never writes it - it is yours to set, usually from the heading, and collision reads it - but it
|
|
39
|
+
round-trips through `save` so a turned entity comes back facing the same way. `width` /
|
|
39
40
|
`height` are the unrotated size; a box with no area never overlaps anything, not even another box sharing its
|
|
40
41
|
exact position.
|
|
41
42
|
|
|
@@ -85,10 +86,17 @@ exactly still counts as **not** overlapping, whichever pair it is, and a shape w
|
|
|
85
86
|
anything. Which of the two sizes decides that depends on the shape: a circle with no height is a perfectly
|
|
86
87
|
good circle, while a rectangle with no height is nothing at all.
|
|
87
88
|
|
|
89
|
+
`contactNormal` answers the other half of a contact: the unit direction one shape is pushed off the other,
|
|
90
|
+
written into a `{ x, y }` you pass in, and `false` when the two are exactly on top of each other and there is no
|
|
91
|
+
such direction. It splits the same three ways off the same geometry, so the direction is the face that was
|
|
92
|
+
actually hit at the angle the pair is turned to - not the nearest world axis. This is what the native bounce
|
|
93
|
+
reflects around; a game reaching for its own response can use it the same way.
|
|
94
|
+
|
|
88
95
|
`shapesOverlap` is exported, along with `shapeHalfWidth` / `shapeHalfHeight` (the axis-aligned box a shape is
|
|
89
96
|
indexed under), `shapeRadius`, `shapeIsEmpty`, `capsuleHalfLength`, and the distance primitives underneath -
|
|
90
|
-
`segmentSegmentDistanceSquared`, `segmentBoxDistanceSquared` and `pointSegmentDistanceSquared
|
|
91
|
-
|
|
97
|
+
`segmentSegmentDistanceSquared`, `segmentBoxDistanceSquared` and `pointSegmentDistanceSquared`, each of which
|
|
98
|
+
takes an optional `out` vector and fills it with the gap it measured. `orientedBoxesOverlap`, `boundsHalfWidth`
|
|
99
|
+
and `boundsHalfHeight` are still there and still rectangle-only.
|
|
92
100
|
|
|
93
101
|
## Registering
|
|
94
102
|
|
|
@@ -301,8 +309,9 @@ happened rather than the one after, which is one frame of latency and nothing el
|
|
|
301
309
|
|
|
302
310
|
- **Pause and `timeScale`** are free. `BaseWorld#runUpdate` skips every system while paused and scales the
|
|
303
311
|
elapsed time it hands them, so the pacing stops and slows with the simulation on its own.
|
|
304
|
-
- **Spawns** are free. The block is seeded from the entity's config, so something added between steps is
|
|
305
|
-
standing where it was put rather than sliding in from the origin.
|
|
312
|
+
- **Spawns at rest** are free. The block is seeded from the entity's config, so something added between steps is
|
|
313
|
+
drawn standing where it was put rather than sliding in from the origin. A spawn *already moving* is the one case
|
|
314
|
+
that needs a hand - see [`startInterpolation`](#a-moving-spawn-startinterpolation) below.
|
|
306
315
|
- **Velocity changes, collisions and coming to rest** are all just steps the simulation took, and are drawn as
|
|
307
316
|
they happened.
|
|
308
317
|
|
|
@@ -318,6 +327,41 @@ entity.components.transform!.x = 500;
|
|
|
318
327
|
snapEntity(entity); // drawn at 500 on the very next frame
|
|
319
328
|
```
|
|
320
329
|
|
|
330
|
+
### A moving spawn: `startInterpolation`
|
|
331
|
+
|
|
332
|
+
Blending has only one position for a brand-new entity - where it spawned - so it holds there until the first
|
|
333
|
+
physics step gives it a second, up to a whole step later. For anything at rest that is exactly right (the bullet
|
|
334
|
+
above stands where it was put). For something spawned *already moving* - a bullet leaving a barrel - that step of
|
|
335
|
+
stillness reads as the shot hanging in the air before it goes.
|
|
336
|
+
|
|
337
|
+
`startInterpolation` fixes it by **opening the render at the spawn point** and moving it out along the entity's own
|
|
338
|
+
velocity at its true speed, **jumping the transform forward** to where that motion reaches by the time the first
|
|
339
|
+
real step lands. So the entity is drawn leaving the spawn in its true direction from the very next frame, and the
|
|
340
|
+
seeded segment hands straight over to the first step with no seam. How far it jumps - and how long the seeded
|
|
341
|
+
segment lasts - is whatever is left of the current step, since that is when the first run lands:
|
|
342
|
+
|
|
343
|
+
```ts
|
|
344
|
+
const bullet = world.loadEntity({
|
|
345
|
+
x: 0, y: 0, velocityX: 400, velocityY: 0, radius: 3, interpolate: true,
|
|
346
|
+
});
|
|
347
|
+
physics.startInterpolation(bullet); // drawn from the spawn point and moving out, not parked there until the first step
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
It reads the step and the accumulator off the system, so there is nothing to pass.
|
|
351
|
+
|
|
352
|
+
**The jump skips a collision sweep.** It is a real move of the transform, applied without sweeping the span it
|
|
353
|
+
covers, so the entity skips forward up to a step: it can **tunnel** through anything within that span of the spawn,
|
|
354
|
+
and it runs that far ahead of where its velocity alone would have put it from then on. This is a deliberate trade
|
|
355
|
+
for **spawned projectiles**, where leaving the muzzle instantly matters more than that first sweep - a shot fired
|
|
356
|
+
*into* a target still lands inside it and is caught and killed by the next run's overlap test; only something thin
|
|
357
|
+
enough to sit entirely within that jumped span is passed through uncaught. Reach for it on bullets and the like,
|
|
358
|
+
not on something that must never skip a step of collision. Because the transform is jumped to the segment's end,
|
|
359
|
+
the render is still never drawn ahead of the simulation.
|
|
360
|
+
|
|
361
|
+
A no-op for an entity with no velocity or no interpolation, or while the system steps every frame. Driving physics
|
|
362
|
+
by hand rather than through `PhysicsSystem`? Call the underlying `startSpawnInterpolation(entity, { stepMs,
|
|
363
|
+
stepFraction, tick })` directly.
|
|
364
|
+
|
|
321
365
|
### The cost
|
|
322
366
|
|
|
323
367
|
32 bytes per interpolated entity, three extra writes per entity per physics step, and a per-frame pass of one
|
|
@@ -401,11 +445,13 @@ world.addSystem(new PhysicsSystem<Components, GameUpdateComponents>(world, {
|
|
|
401
445
|
`createPhysicsUpdate` stamps what it needs onto the function it returns, so `optional` and the collidable
|
|
402
446
|
query come across with `updateFunction` and never have to be repeated on the system.
|
|
403
447
|
|
|
404
|
-
**
|
|
405
|
-
on
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
448
|
+
**Once per pair, per run.** Both sides of a contact find it - one by sweeping into it, the other by overlapping
|
|
449
|
+
it on its own turn - and the callback fires once, for whichever got there first. `self` is that entity, with
|
|
450
|
+
every block the system asked for; `other` is the one it hit, where only `transform` is guaranteed. **The other
|
|
451
|
+
side gets no call of its own**, so a callback that only touches `self` leaves it untouched: apply damage, tag a
|
|
452
|
+
kill, or raise an event for *both* entities in the one call. Which of the two is `self` follows update order
|
|
453
|
+
and is not something to depend on. An entity with no velocity is never `self`, because the system never moves
|
|
454
|
+
it, but it is still found as `other`.
|
|
409
455
|
|
|
410
456
|
Writes go straight into shared memory, so a callback can bounce an entity by flipping its velocity, or push a
|
|
411
457
|
value another thread also touches with the atomics from `@daneren2005/shared-memory-objects`. To reach past
|
|
@@ -460,6 +506,17 @@ thing in the way is thick, so at any normal frame rate it does not arise - a shi
|
|
|
460
506
|
moves 1.6 of them per frame at 60fps. A fixed `deltaBetweenRuns` puts a ceiling on it if your game has anything
|
|
461
507
|
fast enough to care.
|
|
462
508
|
|
|
509
|
+
**Continuous collision detection** lifts that ceiling for the bodies that need it, without slowing down the
|
|
510
|
+
ones that do not. Set `continuousCollisionDetection: true` on a body and its own move is tested along the whole
|
|
511
|
+
path it swept this run rather than only where the step ends - so a bullet that steps clean past a target still
|
|
512
|
+
comes to rest against it (or, if it is a sensor, still reports the pass through it). It is one extra swept-shape
|
|
513
|
+
test per candidate and only for that body: every other body stays on the single end-of-move test. Each shape
|
|
514
|
+
sweeps to a single covering shape - a rectangle to an oriented box spanning start to end, a circle or capsule to
|
|
515
|
+
a capsule down its path - chosen to be a superset of the ground actually covered, so a contact is reported early
|
|
516
|
+
at worst, never missed. A continuous body comes straight to rest at its first contact and does not corner-slide,
|
|
517
|
+
which is the behaviour a fast thing wants anyway. Reach for it on the small, fast things that tunnel - bullets,
|
|
518
|
+
thrown weapons - and leave it off everything else.
|
|
519
|
+
|
|
463
520
|
### Collision filtering
|
|
464
521
|
|
|
465
522
|
Every body collides **as** a `collideCategory` and **with** a `collideMask`, both plain 32-bit fields. The
|
|
@@ -515,6 +572,26 @@ than off both. The pair is never missed, just attributed to the side that moved
|
|
|
515
572
|
the entity that moves first gets the clear road: it takes its whole move, and the second one is the one that
|
|
516
573
|
comes to rest against it.
|
|
517
574
|
|
|
575
|
+
### Scoping a system to a group
|
|
576
|
+
|
|
577
|
+
Categories decide which *kinds* of body collide; `scope` decides which *world* they collide in. A game that
|
|
578
|
+
runs several independent groups at once - solar systems around the same origin, floors of a dungeon - gives
|
|
579
|
+
each group its own `PhysicsSystem` with a `scope`, and each one then only ever sees, moves, or collides against
|
|
580
|
+
the entities that pass it:
|
|
581
|
+
|
|
582
|
+
```ts
|
|
583
|
+
world.addSystem(new PhysicsSystem<Components>(world, {
|
|
584
|
+
updateFunction: gamePhysicsUpdate,
|
|
585
|
+
scope: entity => entity.components.solarSystem?.solarSystemId === solId,
|
|
586
|
+
}));
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
Unlike a category rule - which every body in the world is still weighed against - a scoped-out body never
|
|
590
|
+
enters this system's broadphase at all, so two groups whose boxes overlap around a shared origin cannot collide
|
|
591
|
+
across the boundary, and the tree each run builds is only as big as its own group. `scope` is separate from the
|
|
592
|
+
sharding `filter` (which narrows only *which* movers a worker steps, still sweeping the whole world so one
|
|
593
|
+
simulation can spread across cores); an entity handed both has to pass each to be moved.
|
|
594
|
+
|
|
518
595
|
## Using physicsUpdate in your own system
|
|
519
596
|
|
|
520
597
|
`physicsUpdate` is a plain `EntityUpdateFunction`, so a game that already has its own movement system can call
|
|
@@ -548,6 +625,11 @@ transform[TRANSFORM_Y_INDEX] += moveY * fraction;
|
|
|
548
625
|
`blocking` is what the entity came to rest *against*, which `forEachOverlapping` will not report from that
|
|
549
626
|
resting place: the two are touching rather than through each other.
|
|
550
627
|
|
|
628
|
+
Both honour a body's `continuousCollisionDetection` flag. `sweep` picks it up on its own - a continuous body's
|
|
629
|
+
move is swept along its whole path. `forEachOverlapping` needs the move handed to it, since it runs after the
|
|
630
|
+
entity has already been put down: `forEachOverlapping(self, handle, moveX, moveY)` tests the path a continuous
|
|
631
|
+
body just swept, while the two-argument form (and every non-continuous body) tests only where it now stands.
|
|
632
|
+
|
|
551
633
|
## Spatial queries
|
|
552
634
|
|
|
553
635
|
`SpatialIndex` answers the other kind of question about where things are - who is in this area, and what is
|
|
@@ -600,8 +682,8 @@ npm start # http://127.0.0.1:8080
|
|
|
600
682
|
```
|
|
601
683
|
|
|
602
684
|
- **Bouncing circles** - circles with a random position and heading, reflected off each other and off the
|
|
603
|
-
walls by
|
|
604
|
-
- **Bouncing rectangles** - the same, on boxes of random width and height, where the normal is the
|
|
685
|
+
walls by the native bounce, which mirrors the velocity about the contact normal.
|
|
686
|
+
- **Bouncing rectangles** - the same, on boxes of random width and height, where the normal is the face the
|
|
605
687
|
two are least through each other on rather than the line between their centres.
|
|
606
688
|
- **Walking into a wall** - one unit between two boxes, turned around on a timer so it presses into each of
|
|
607
689
|
them in turn. It starts on a deliberately long 100ms physics step, so turning **interpolate rendering** off is
|
|
@@ -5,6 +5,7 @@ export interface BodyComponent {
|
|
|
5
5
|
collideCategory: number;
|
|
6
6
|
collideMask: number;
|
|
7
7
|
sensor: boolean;
|
|
8
|
+
continuousCollisionDetection: boolean;
|
|
8
9
|
}
|
|
9
10
|
export declare const SHAPE_RECTANGLE = 1;
|
|
10
11
|
export declare const SHAPE_CIRCLE = 2;
|
|
@@ -20,12 +21,17 @@ export interface BodyConfig {
|
|
|
20
21
|
collideCategory?: number;
|
|
21
22
|
collideMask?: number;
|
|
22
23
|
sensor?: boolean;
|
|
24
|
+
continuousCollisionDetection?: boolean;
|
|
23
25
|
}
|
|
24
|
-
export declare const
|
|
26
|
+
export declare const BODY_FLAGS_INDEX = 0;
|
|
25
27
|
export declare const BODY_CATEGORY_INDEX = 1;
|
|
26
28
|
export declare const BODY_MASK_INDEX = 2;
|
|
27
|
-
export declare const
|
|
28
|
-
export declare const
|
|
29
|
+
export declare const BODY_SIZE = 3;
|
|
30
|
+
export declare const BODY_SHAPE_MASK = 3;
|
|
31
|
+
export declare const BODY_SENSOR_FLAG = 4;
|
|
32
|
+
export declare const BODY_CCD_FLAG = 8;
|
|
33
|
+
export declare function bodyShape(body: Uint32Array): number;
|
|
29
34
|
export declare function isSensor(body: Uint32Array): boolean;
|
|
35
|
+
export declare function isContinuous(body: Uint32Array): boolean;
|
|
30
36
|
export declare const bodyDefinition: ComponentDefinition<BodyComponent, Uint32Array, BodyConfig>;
|
|
31
37
|
export default bodyDefinition;
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import type { ComponentDefinition } from '@daneren2005/shared-memory-ecs';
|
|
2
2
|
import type { TransformComponent } from './transform-component';
|
|
3
|
+
import type { VelocityComponent } from './velocity-component';
|
|
3
4
|
export interface InterpolationComponent {
|
|
4
5
|
index: number;
|
|
5
6
|
x: number;
|
|
6
7
|
y: number;
|
|
7
8
|
prevX: number;
|
|
8
9
|
prevY: number;
|
|
10
|
+
progress: number;
|
|
11
|
+
syncedTick: number;
|
|
12
|
+
duration: number;
|
|
13
|
+
tick: number;
|
|
9
14
|
}
|
|
10
15
|
export interface InterpolationConfig {
|
|
11
16
|
interpolate?: boolean;
|
|
@@ -29,4 +34,17 @@ export interface SnappableEntity {
|
|
|
29
34
|
};
|
|
30
35
|
}
|
|
31
36
|
export declare function snapEntity(entity: SnappableEntity): void;
|
|
37
|
+
export interface SpawnableEntity {
|
|
38
|
+
components: {
|
|
39
|
+
transform?: TransformComponent;
|
|
40
|
+
velocity?: VelocityComponent;
|
|
41
|
+
interpolation?: InterpolationComponent;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export interface SpawnInterpolationOptions {
|
|
45
|
+
stepMs: number;
|
|
46
|
+
stepFraction: number;
|
|
47
|
+
tick: number;
|
|
48
|
+
}
|
|
49
|
+
export declare function startSpawnInterpolation(entity: SpawnableEntity, options: SpawnInterpolationOptions): void;
|
|
32
50
|
export default interpolationDefinition;
|
package/dist/index.d.ts
CHANGED
|
@@ -4,18 +4,19 @@ export type { TransformComponent, TransformConfig, TransformSerialization, } fro
|
|
|
4
4
|
export { default as velocityDefinition } from './components/velocity-component';
|
|
5
5
|
export { VELOCITY_X_INDEX, VELOCITY_Y_INDEX, VELOCITY_SIZE, } from './components/velocity-component';
|
|
6
6
|
export type { VelocityComponent, VelocityConfig, VelocitySerialization, } from './components/velocity-component';
|
|
7
|
-
export { default as bodyDefinition, canCollide, isSensor } from './components/body-component';
|
|
8
|
-
export { SHAPE_RECTANGLE, SHAPE_CIRCLE, SHAPE_CAPSULE, DEFAULT_COLLIDE_CATEGORY, DEFAULT_COLLIDE_MASK,
|
|
7
|
+
export { default as bodyDefinition, canCollide, isSensor, isContinuous, bodyShape } from './components/body-component';
|
|
8
|
+
export { SHAPE_RECTANGLE, SHAPE_CIRCLE, SHAPE_CAPSULE, DEFAULT_COLLIDE_CATEGORY, DEFAULT_COLLIDE_MASK, BODY_FLAGS_INDEX, BODY_SHAPE_MASK, BODY_SENSOR_FLAG, BODY_CCD_FLAG, BODY_CATEGORY_INDEX, BODY_MASK_INDEX, BODY_SIZE, } from './components/body-component';
|
|
9
9
|
export type { BodyComponent, BodyConfig, } from './components/body-component';
|
|
10
10
|
export { default as bouncinessDefinition } from './components/bounciness-component';
|
|
11
11
|
export { BOUNCINESS_INDEX, BOUNCINESS_SIZE, } from './components/bounciness-component';
|
|
12
12
|
export type { BouncinessComponent, BouncinessConfig, } from './components/bounciness-component';
|
|
13
|
-
export { default as interpolationDefinition, snapEntity } from './components/interpolation-component';
|
|
13
|
+
export { default as interpolationDefinition, snapEntity, startSpawnInterpolation } from './components/interpolation-component';
|
|
14
14
|
export { INTERPOLATION_X_INDEX, INTERPOLATION_Y_INDEX, INTERPOLATION_PREV_X_INDEX, INTERPOLATION_PREV_Y_INDEX, INTERPOLATION_PROGRESS_INDEX, INTERPOLATION_SYNCED_TICK_INDEX, INTERPOLATION_DURATION_INDEX, INTERPOLATION_TICK_INDEX, INTERPOLATION_SIZE, } from './components/interpolation-component';
|
|
15
|
-
export type { InterpolationComponent, InterpolationConfig, SnappableEntity, } from './components/interpolation-component';
|
|
15
|
+
export type { InterpolationComponent, InterpolationConfig, SnappableEntity, SpawnableEntity, SpawnInterpolationOptions, } from './components/interpolation-component';
|
|
16
16
|
export { physicsRegistry } from './components/registry';
|
|
17
17
|
export type { PhysicsComponents, PhysicsUpdateComponents, InterpolationComponents, InterpolationUpdateComponents, } from './components/registry';
|
|
18
|
-
export { shapesOverlap, shapeHalfWidth, shapeHalfHeight, shapeIsEmpty, shapeRadius, capsuleHalfLength, boundsHalfWidth, boundsHalfHeight, orientedBoxesOverlap, segmentSegmentDistanceSquared, segmentBoxDistanceSquared, pointSegmentDistanceSquared, } from './math/shapes';
|
|
18
|
+
export { shapesOverlap, contactNormal, shapeHalfWidth, shapeHalfHeight, shapeIsEmpty, shapeRadius, capsuleHalfLength, boundsHalfWidth, boundsHalfHeight, orientedBoxesOverlap, segmentSegmentDistanceSquared, segmentBoxDistanceSquared, pointSegmentDistanceSquared, } from './math/shapes';
|
|
19
|
+
export type { Vector } from './math/shapes';
|
|
19
20
|
export { default as CollisionBroadphase, COLLIDABLE_QUERY } from './systems/collision';
|
|
20
21
|
export type { CollisionComponents, CollisionEntity, CollisionFunction, MoveResult, MovingEntity, SweepResult } from './systems/collision';
|
|
21
22
|
export { default as SpatialIndex } from './systems/spatial-index';
|