@daneren2005/shared-memory-physics 1.2.2 → 1.4.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 +80 -7
- package/dist/components/body-component.d.ts +9 -3
- package/dist/components/interpolation-component.d.ts +18 -0
- package/dist/components/transform-component.d.ts +1 -0
- package/dist/index.d.ts +5 -5
- package/dist/index.js +531 -359
- package/dist/index.js.map +1 -1
- package/dist/systems/collision.d.ts +5 -1
- package/dist/systems/physics-system.d.ts +5 -0
- package/dist/systems/physics-update.d.ts +7 -0
- package/package.json +3 -3
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
|
|
|
@@ -308,8 +309,9 @@ happened rather than the one after, which is one frame of latency and nothing el
|
|
|
308
309
|
|
|
309
310
|
- **Pause and `timeScale`** are free. `BaseWorld#runUpdate` skips every system while paused and scales the
|
|
310
311
|
elapsed time it hands them, so the pacing stops and slows with the simulation on its own.
|
|
311
|
-
- **Spawns** are free. The block is seeded from the entity's config, so something added between steps is
|
|
312
|
-
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.
|
|
313
315
|
- **Velocity changes, collisions and coming to rest** are all just steps the simulation took, and are drawn as
|
|
314
316
|
they happened.
|
|
315
317
|
|
|
@@ -325,6 +327,41 @@ entity.components.transform!.x = 500;
|
|
|
325
327
|
snapEntity(entity); // drawn at 500 on the very next frame
|
|
326
328
|
```
|
|
327
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
|
+
|
|
328
365
|
### The cost
|
|
329
366
|
|
|
330
367
|
32 bytes per interpolated entity, three extra writes per entity per physics step, and a per-frame pass of one
|
|
@@ -469,6 +506,17 @@ thing in the way is thick, so at any normal frame rate it does not arise - a shi
|
|
|
469
506
|
moves 1.6 of them per frame at 60fps. A fixed `deltaBetweenRuns` puts a ceiling on it if your game has anything
|
|
470
507
|
fast enough to care.
|
|
471
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
|
+
|
|
472
520
|
### Collision filtering
|
|
473
521
|
|
|
474
522
|
Every body collides **as** a `collideCategory` and **with** a `collideMask`, both plain 32-bit fields. The
|
|
@@ -524,6 +572,26 @@ than off both. The pair is never missed, just attributed to the side that moved
|
|
|
524
572
|
the entity that moves first gets the clear road: it takes its whole move, and the second one is the one that
|
|
525
573
|
comes to rest against it.
|
|
526
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
|
+
|
|
527
595
|
## Using physicsUpdate in your own system
|
|
528
596
|
|
|
529
597
|
`physicsUpdate` is a plain `EntityUpdateFunction`, so a game that already has its own movement system can call
|
|
@@ -557,6 +625,11 @@ transform[TRANSFORM_Y_INDEX] += moveY * fraction;
|
|
|
557
625
|
`blocking` is what the entity came to rest *against*, which `forEachOverlapping` will not report from that
|
|
558
626
|
resting place: the two are touching rather than through each other.
|
|
559
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
|
+
|
|
560
633
|
## Spatial queries
|
|
561
634
|
|
|
562
635
|
`SpatialIndex` answers the other kind of question about where things are - who is in this area, and what 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,15 +4,15 @@ 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
18
|
export { shapesOverlap, contactNormal, shapeHalfWidth, shapeHalfHeight, shapeIsEmpty, shapeRadius, capsuleHalfLength, boundsHalfWidth, boundsHalfHeight, orientedBoxesOverlap, segmentSegmentDistanceSquared, segmentBoxDistanceSquared, pointSegmentDistanceSquared, } from './math/shapes';
|
|
@@ -22,7 +22,7 @@ export type { CollisionComponents, CollisionEntity, CollisionFunction, MoveResul
|
|
|
22
22
|
export { default as SpatialIndex } from './systems/spatial-index';
|
|
23
23
|
export type { SpatialComponents, SpatialEntity, SpatialFilter } from './systems/spatial-index';
|
|
24
24
|
export { default as physicsUpdate, createPhysicsUpdate, POSITION_UPDATED_EVENT } from './systems/physics-update';
|
|
25
|
-
export type { PhysicsUpdateFunction, PhysicsUpdateMetadata, PhysicsUpdateOptions, PhysicsWorld } from './systems/physics-update';
|
|
25
|
+
export type { PhysicsGroupConfig, PhysicsUpdateFunction, PhysicsUpdateMetadata, PhysicsUpdateOptions, PhysicsWorld } from './systems/physics-update';
|
|
26
26
|
export { default as PhysicsSystem, DEFAULT_PHYSICS_STEP_MS } from './systems/physics-system';
|
|
27
27
|
export type { PhysicsSystemConfig } from './systems/physics-system';
|
|
28
28
|
export { default as interpolationUpdate } from './systems/interpolation-update';
|