@gg-web-engine/matter 0.0.72 → 0.0.74
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/dist/components/matter-character-controller.component.d.ts +191 -0
- package/dist/components/matter-character-controller.component.js +498 -0
- package/dist/components/matter-rigid-body.component.d.ts +43 -2
- package/dist/components/matter-rigid-body.component.js +68 -5
- package/dist/components/matter-trigger.component.d.ts +66 -4
- package/dist/components/matter-trigger.component.js +139 -4
- package/dist/components/matter-world.component.d.ts +44 -5
- package/dist/components/matter-world.component.js +131 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/matter-factory.d.ts +8 -1
- package/dist/matter-factory.js +116 -11
- package/dist/types.d.ts +2 -0
- package/package.json +5 -4
- package/test/components/matter-character-controller.component.spec.ts +298 -0
- package/test/components/matter-rigid-body-collision.spec.ts +27 -13
- package/test/components/matter-rigid-body-sleep.spec.ts +79 -0
- package/test/components/matter-rigid-body.component.spec.ts +98 -0
- package/test/components/matter-trigger.component.spec.ts +135 -13
- package/test/components/matter-world.component.spec.ts +44 -9
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BitMask, DebugBody2DSettings, Pnt2, } from '@gg-web-engine/core';
|
|
2
|
-
import { Body, Composite, Vector } from 'matter-js';
|
|
2
|
+
import { Body, Composite, Sleeping, Vector } from 'matter-js';
|
|
3
3
|
import { Subject } from 'rxjs';
|
|
4
4
|
// FIXME why this needs to be introduced? investigate units in matter.js
|
|
5
5
|
const MATTER_VELOCITY_SCALE = 0.0166667;
|
|
@@ -14,7 +14,7 @@ export class MatterRigidBodyComponent {
|
|
|
14
14
|
return this.nativeBody.angle;
|
|
15
15
|
}
|
|
16
16
|
set rotation(value) {
|
|
17
|
-
this.nativeBody
|
|
17
|
+
Body.setAngle(this.nativeBody, value);
|
|
18
18
|
}
|
|
19
19
|
get linearVelocity() {
|
|
20
20
|
return Pnt2.scalarMult(this.nativeBody.velocity, 1 / MATTER_VELOCITY_SCALE);
|
|
@@ -28,15 +28,40 @@ export class MatterRigidBodyComponent {
|
|
|
28
28
|
set angularVelocity(value) {
|
|
29
29
|
Body.setAngularVelocity(this.nativeBody, value);
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* See `IRigidBodyComponent.bodyOptions`'s own doc. `mass`/`friction`/`restitution` are read live
|
|
33
|
+
* off the native matter-js body (a plain JS object - `.mass`/`.friction`/`.restitution` are
|
|
34
|
+
* ordinary fields, resolved to matter-js's own defaults by `Body.create` for whichever of them
|
|
35
|
+
* weren't explicitly given, so this reflects the body's *actual* resolved values, not just
|
|
36
|
+
* whatever was requested). `bodyType`/`ccd` are stored as originally requested rather than
|
|
37
|
+
* derived from the native body: matter-js has no kinematic body concept at all (a requested
|
|
38
|
+
* `kinematic_pos`/`kinematic_vel` degrades to a plain `isStatic` body - see `MatterFactory
|
|
39
|
+
* .transformOptions`'s own doc) and no CCD, so neither is recoverable from - or even meaningfully
|
|
40
|
+
* "live" on - the native body itself; echoing the request instead keeps a level JSON reloaded
|
|
41
|
+
* under a different, kinematic/CCD-capable adapter faithful to what was actually asked for.
|
|
42
|
+
*/
|
|
43
|
+
get bodyOptions() {
|
|
44
|
+
return {
|
|
45
|
+
bodyType: this.bodyType,
|
|
46
|
+
mass: this.nativeBody.mass,
|
|
47
|
+
friction: this.nativeBody.friction,
|
|
48
|
+
restitution: this.nativeBody.restitution,
|
|
49
|
+
ccd: this.ccd,
|
|
50
|
+
ownCollisionGroups: this.ownCollisionGroups,
|
|
51
|
+
interactWithCollisionGroups: this.interactWithCollisionGroups,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
31
54
|
get onCollisionStart() {
|
|
32
55
|
return this.onCollisionStart$.asObservable();
|
|
33
56
|
}
|
|
34
57
|
get onCollisionEnd() {
|
|
35
58
|
return this.onCollisionEnd$.asObservable();
|
|
36
59
|
}
|
|
37
|
-
constructor(nativeBody, shape) {
|
|
60
|
+
constructor(nativeBody, shape, bodyType = 'dynamic', ccd = false) {
|
|
38
61
|
this.nativeBody = nativeBody;
|
|
39
62
|
this.shape = shape;
|
|
63
|
+
this.bodyType = bodyType;
|
|
64
|
+
this.ccd = ccd;
|
|
40
65
|
this.name = '';
|
|
41
66
|
this.entity = null;
|
|
42
67
|
// matter-js has no native kinematic body - a body requested as `kinematic_pos`/`kinematic_vel`
|
|
@@ -45,7 +70,7 @@ export class MatterRigidBodyComponent {
|
|
|
45
70
|
// rather than what an app originally asked for - the console warning at creation time is what
|
|
46
71
|
// tells a developer their kinematic request wasn't honored, not this view.
|
|
47
72
|
this.debugBodySettings = new DebugBody2DSettings(isFinite(this.nativeBody.mass)
|
|
48
|
-
? { type: 'RIGID_DYNAMIC', sleeping: () => this.
|
|
73
|
+
? { type: 'RIGID_DYNAMIC', sleeping: () => this.isSleeping }
|
|
49
74
|
: { type: 'RIGID_STATIC' }, this.shape);
|
|
50
75
|
this._interactWithCGsMask = BitMask.full(16);
|
|
51
76
|
this._ownCGsMask = BitMask.full(16);
|
|
@@ -110,7 +135,7 @@ export class MatterRigidBodyComponent {
|
|
|
110
135
|
}
|
|
111
136
|
clone() {
|
|
112
137
|
const clonedBody = Body.create(Object.assign(Object.assign({}, this.nativeBody), { collisionFilter: Object.assign({}, this.nativeBody.collisionFilter) }));
|
|
113
|
-
const component = new MatterRigidBodyComponent(clonedBody, this.shape);
|
|
138
|
+
const component = new MatterRigidBodyComponent(clonedBody, this.shape, this.bodyType, this.ccd);
|
|
114
139
|
component.ownCollisionGroups = this.ownCollisionGroups;
|
|
115
140
|
component.interactWithCollisionGroups = this.interactWithCollisionGroups;
|
|
116
141
|
return component;
|
|
@@ -147,4 +172,42 @@ export class MatterRigidBodyComponent {
|
|
|
147
172
|
Body.setVelocity(this.nativeBody, Pnt2.O);
|
|
148
173
|
Body.setAngularVelocity(this.nativeBody, 0);
|
|
149
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* A `kinematic_pos`/`kinematic_vel` request also reports `isStatic: true` here - see this
|
|
177
|
+
* component's own `bodyType`/`bodyOptions` doc for why matter-js can't distinguish that from a
|
|
178
|
+
* genuine `'static'` body natively. Both are equally never simulated as a sleepable dynamic body,
|
|
179
|
+
* so gating on the native `isStatic` flag rather than `this.bodyType === 'static'` is correct for
|
|
180
|
+
* both cases, not just the literal static one.
|
|
181
|
+
*
|
|
182
|
+
* Also always `false` regardless of `isStatic` unless the world's underlying `Matter.Engine` was
|
|
183
|
+
* created with `enableSleeping: true` - this adapter never turns that on itself (see
|
|
184
|
+
* `MatterWorldComponent`'s own doc on its `matterWorld`/engine setup), so a body constructed
|
|
185
|
+
* through this adapter alone never actually falls asleep on its own; `sleep()` below still forces
|
|
186
|
+
* it regardless of that engine setting.
|
|
187
|
+
*/
|
|
188
|
+
get isSleeping() {
|
|
189
|
+
return !this.nativeBody.isStatic && this.nativeBody.isSleeping;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* No-op on a body that reports `isStatic` (a genuine `'static'` body, or a `kinematic_pos`/
|
|
193
|
+
* `kinematic_vel` request - see `isSleeping`'s own doc for why both are treated the same here).
|
|
194
|
+
*/
|
|
195
|
+
wakeUp() {
|
|
196
|
+
if (this.nativeBody.isStatic) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
Sleeping.set(this.nativeBody, false);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* No-op on a body that reports `isStatic` (see `isSleeping`'s own doc). Forces sleep immediately,
|
|
203
|
+
* regardless of whether the world's `Matter.Engine` has `enableSleeping` turned on - unlike a
|
|
204
|
+
* body naturally falling asleep from inactivity (which requires that engine flag), an explicit
|
|
205
|
+
* `Sleeping.set(body, true)` call takes effect either way.
|
|
206
|
+
*/
|
|
207
|
+
sleep() {
|
|
208
|
+
if (this.nativeBody.isStatic) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
Sleeping.set(this.nativeBody, true);
|
|
212
|
+
}
|
|
150
213
|
}
|
|
@@ -1,21 +1,61 @@
|
|
|
1
1
|
import { Observable, Subject } from 'rxjs';
|
|
2
2
|
import { Body } from 'matter-js';
|
|
3
3
|
import { MatterRigidBodyComponent } from './matter-rigid-body.component';
|
|
4
|
+
import { MatterCharacterControllerComponent } from './matter-character-controller.component';
|
|
4
5
|
import { DebugBody2DSettings, ITrigger2dComponent, Shape2DDescriptor } from '@gg-web-engine/core';
|
|
5
6
|
import { MatterWorldComponent } from './matter-world.component';
|
|
6
7
|
import { MatterGgWorld, MatterPhysicsTypeDocRepo } from '../types';
|
|
7
8
|
export declare class MatterTriggerComponent extends MatterRigidBodyComponent implements ITrigger2dComponent<MatterPhysicsTypeDocRepo> {
|
|
8
9
|
readonly shape: Shape2DDescriptor;
|
|
9
10
|
protected readonly world: MatterWorldComponent;
|
|
10
|
-
get onEntityEntered(): Observable<MatterRigidBodyComponent>;
|
|
11
|
-
get onEntityLeft(): Observable<MatterRigidBodyComponent>;
|
|
12
|
-
protected readonly onEnter$: Subject<MatterRigidBodyComponent>;
|
|
13
|
-
protected readonly onLeft$: Subject<MatterRigidBodyComponent>;
|
|
11
|
+
get onEntityEntered(): Observable<MatterRigidBodyComponent | MatterCharacterControllerComponent>;
|
|
12
|
+
get onEntityLeft(): Observable<MatterRigidBodyComponent | MatterCharacterControllerComponent>;
|
|
13
|
+
protected readonly onEnter$: Subject<MatterRigidBodyComponent | MatterCharacterControllerComponent>;
|
|
14
|
+
protected readonly onLeft$: Subject<MatterRigidBodyComponent | MatterCharacterControllerComponent>;
|
|
15
|
+
/** Character controllers currently overlapping this trigger, as of the last `checkOverlaps()`
|
|
16
|
+
* poll - see that method's own doc for why this needs its own separate polling mechanism instead
|
|
17
|
+
* of the native `collisionStart`/`collisionEnd` events `handleCollisionStart`/`handleCollisionEnd`
|
|
18
|
+
* below rely on for ordinary rigid bodies. */
|
|
19
|
+
protected currentCharacterOverlaps: Set<MatterCharacterControllerComponent>;
|
|
14
20
|
readonly debugBodySettings: DebugBody2DSettings;
|
|
15
21
|
protected intersectionsAmount: number;
|
|
16
22
|
protected currentOverlaps: Set<MatterRigidBodyComponent>;
|
|
23
|
+
/** The subset of `currentOverlaps` whose exit `checkOverlaps()` detects by polling, because matter's
|
|
24
|
+
* detector doesn't pair them with this (static) trigger body - see {@link isPolled}. A body moves
|
|
25
|
+
* back to the native-event path once a native `collisionStart` reports it (it woke up inside). */
|
|
26
|
+
protected polledOverlaps: Set<MatterRigidBodyComponent>;
|
|
27
|
+
/** `Composite.remove` (what `removeFromWorld` calls) never fires a native `collisionEnd` for the
|
|
28
|
+
* body it removes - matter-js simply stops considering that body's pairs on the next step, it
|
|
29
|
+
* doesn't retroactively report the pairs that were active at removal time. Without this, a body
|
|
30
|
+
* removed from the world while still overlapping this trigger would leave `currentOverlaps`
|
|
31
|
+
* (and, for a character, `currentCharacterOverlaps`) permanently stale and `onEntityLeft` would
|
|
32
|
+
* never fire for it. `world.physicsWorld.removed$` fires for every component removal regardless
|
|
33
|
+
* of overlap state, so this only actually acts when the removed component is one this trigger was
|
|
34
|
+
* still tracking as an overlap.
|
|
35
|
+
*
|
|
36
|
+
* The stale entry is dropped synchronously, but `onLeft$` itself is only ever emitted on a
|
|
37
|
+
* microtask from this reaction, never inline. `removed$` fires from inside whatever call stack
|
|
38
|
+
* performed the removal - which can be another component's own in-progress lifecycle operation:
|
|
39
|
+
* `CharacterController2dEntity.recreateCapsule()` (crouch/stand) removes the *old*
|
|
40
|
+
* `characterController` with `dispose: true` before it has assigned the new one, so an `onLeft$`
|
|
41
|
+
* subscriber running inline there (app code driven by `Trigger2dEntity`, e.g. a kill volume
|
|
42
|
+
* resetting `.position` on the character that just left it) would write through the old,
|
|
43
|
+
* already-removed `MatterCharacterControllerComponent` - `Body.setPosition` on a body no longer
|
|
44
|
+
* queried by anything - and the write is silently lost, with the replacement capsule then spawning
|
|
45
|
+
* at the stale position. Deferring the emission keeps it within the same JS turn (well before the
|
|
46
|
+
* next `Engine.update`) but strictly after every synchronous call stack in flight, including the
|
|
47
|
+
* removal's own, has unwound. */
|
|
48
|
+
private removedSub?;
|
|
17
49
|
private handleCollisionStart;
|
|
18
50
|
private handleCollisionEnd;
|
|
51
|
+
/**
|
|
52
|
+
* Whether matter's detector never pairs `comp` with this trigger: it skips every pair whose bodies
|
|
53
|
+
* are both static or sleeping, and the trigger body is static. That covers sleeping bodies and
|
|
54
|
+
* kinematic ones (built as static here, see `MatterFactory`). Bodies requested as `'static'` are
|
|
55
|
+
* level geometry and are never reported.
|
|
56
|
+
*/
|
|
57
|
+
protected isPolled(comp: MatterRigidBodyComponent): boolean;
|
|
58
|
+
protected overlaps(comp: MatterRigidBodyComponent): boolean;
|
|
19
59
|
constructor(nativeBody: Body, shape: Shape2DDescriptor, world: MatterWorldComponent);
|
|
20
60
|
addToWorld(world: MatterGgWorld): void;
|
|
21
61
|
removeFromWorld(world: MatterGgWorld, dispose?: boolean): void;
|
|
@@ -23,6 +63,28 @@ export declare class MatterTriggerComponent extends MatterRigidBodyComponent imp
|
|
|
23
63
|
* `onCollisionStart$`/`onCollisionEnd$` completion (via `super.dispose()`) - this trigger's own
|
|
24
64
|
* enter/exit subjects are a separate pair this subclass owns and must complete itself. */
|
|
25
65
|
dispose(): void;
|
|
66
|
+
/**
|
|
67
|
+
* Awake dynamic bodies are handled by `handleCollisionStart`/`handleCollisionEnd` above, off
|
|
68
|
+
* matter's own native `collisionStart`/`collisionEnd` engine events; kinematic and sleeping ones,
|
|
69
|
+
* which matter never pairs with this static body, are polled by `checkPolledBodyOverlaps()`.
|
|
70
|
+
* A `MatterCharacterControllerComponent`'s own phantom body is
|
|
71
|
+
* deliberately never added to `Composite`/`engine.world` at all (see that class's own doc), so no
|
|
72
|
+
* native collision pair - and thus no native event - can ever involve it. Since `checkOverlaps()` is
|
|
73
|
+
* already called once per tick by `Trigger2dEntity` regardless of backend, this is the natural place
|
|
74
|
+
* to add the poll this needs instead of inventing a second, differently-shaped mechanism: every
|
|
75
|
+
* `MatterCharacterControllerComponent` currently in the world (`world.children`, which - unlike
|
|
76
|
+
* matter's own `Composite` - already tracks it) is tested against this trigger's own body via
|
|
77
|
+
* `Query.collides`, diffed against `currentCharacterOverlaps` to fire `onEntityEntered`/
|
|
78
|
+
* `onEntityLeft` exactly on the enter/exit transitions, the same as the native-event path does for
|
|
79
|
+
* ordinary bodies.
|
|
80
|
+
*/
|
|
26
81
|
checkOverlaps(): void;
|
|
82
|
+
/**
|
|
83
|
+
* Enter/exit of the rigid bodies matter's detector never pairs with this trigger (see
|
|
84
|
+
* {@link isPolled}): a kinematic platform moving in or out, a body asleep inside. Like the
|
|
85
|
+
* character poll below, `Query.collides` ignores `collisionFilter`, so `Detector.canCollide` is
|
|
86
|
+
* checked by hand.
|
|
87
|
+
*/
|
|
88
|
+
protected checkPolledBodyOverlaps(): void;
|
|
27
89
|
clone(): MatterTriggerComponent;
|
|
28
90
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { map, merge, Subject } from 'rxjs';
|
|
2
|
-
import { Body, Events } from 'matter-js';
|
|
2
|
+
import { Body, Detector, Events, Query } from 'matter-js';
|
|
3
3
|
import { MatterRigidBodyComponent } from './matter-rigid-body.component';
|
|
4
|
+
import { MatterCharacterControllerComponent } from './matter-character-controller.component';
|
|
4
5
|
import { DebugBody2DSettings } from '@gg-web-engine/core';
|
|
5
6
|
export class MatterTriggerComponent extends MatterRigidBodyComponent {
|
|
6
7
|
get onEntityEntered() {
|
|
@@ -20,7 +21,13 @@ export class MatterTriggerComponent extends MatterRigidBodyComponent {
|
|
|
20
21
|
}
|
|
21
22
|
if (body) {
|
|
22
23
|
let comp = this.world.children.find(c => c.nativeBody === body);
|
|
23
|
-
if (comp) {
|
|
24
|
+
if (comp instanceof MatterRigidBodyComponent) {
|
|
25
|
+
if (this.currentOverlaps.has(comp)) {
|
|
26
|
+
// already inside via polling (it was asleep or kinematic): the native pair takes over
|
|
27
|
+
this.polledOverlaps.delete(comp);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
this.currentOverlaps.add(comp);
|
|
24
31
|
this.onEnter$.next(comp);
|
|
25
32
|
}
|
|
26
33
|
}
|
|
@@ -37,22 +44,56 @@ export class MatterTriggerComponent extends MatterRigidBodyComponent {
|
|
|
37
44
|
}
|
|
38
45
|
if (body) {
|
|
39
46
|
let comp = this.world.children.find(c => c.nativeBody === body);
|
|
40
|
-
if (comp) {
|
|
47
|
+
if (comp instanceof MatterRigidBodyComponent && this.currentOverlaps.has(comp)) {
|
|
48
|
+
if (this.isPolled(comp) && this.overlaps(comp)) {
|
|
49
|
+
// a body falling asleep inside: matter drops the pair without it having left
|
|
50
|
+
this.polledOverlaps.add(comp);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
this.currentOverlaps.delete(comp);
|
|
54
|
+
this.polledOverlaps.delete(comp);
|
|
41
55
|
this.onLeft$.next(comp);
|
|
42
56
|
}
|
|
43
57
|
}
|
|
44
58
|
}
|
|
45
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Whether matter's detector never pairs `comp` with this trigger: it skips every pair whose bodies
|
|
62
|
+
* are both static or sleeping, and the trigger body is static. That covers sleeping bodies and
|
|
63
|
+
* kinematic ones (built as static here, see `MatterFactory`). Bodies requested as `'static'` are
|
|
64
|
+
* level geometry and are never reported.
|
|
65
|
+
*/
|
|
66
|
+
isPolled(comp) {
|
|
67
|
+
return comp.bodyType === 'kinematic_pos' || comp.bodyType === 'kinematic_vel' || comp.nativeBody.isSleeping;
|
|
68
|
+
}
|
|
69
|
+
overlaps(comp) {
|
|
70
|
+
return Query.collides(this.nativeBody, [comp.nativeBody]).length > 0;
|
|
71
|
+
}
|
|
46
72
|
constructor(nativeBody, shape, world) {
|
|
47
73
|
super(nativeBody, shape);
|
|
48
74
|
this.shape = shape;
|
|
49
75
|
this.world = world;
|
|
50
76
|
this.onEnter$ = new Subject();
|
|
51
77
|
this.onLeft$ = new Subject();
|
|
78
|
+
/** Character controllers currently overlapping this trigger, as of the last `checkOverlaps()`
|
|
79
|
+
* poll - see that method's own doc for why this needs its own separate polling mechanism instead
|
|
80
|
+
* of the native `collisionStart`/`collisionEnd` events `handleCollisionStart`/`handleCollisionEnd`
|
|
81
|
+
* below rely on for ordinary rigid bodies. */
|
|
82
|
+
this.currentCharacterOverlaps = new Set();
|
|
52
83
|
this.debugBodySettings = new DebugBody2DSettings({ type: 'TRIGGER', activated: () => this.intersectionsAmount > 0 }, this.shape);
|
|
53
84
|
this.intersectionsAmount = 0;
|
|
54
85
|
this.currentOverlaps = new Set();
|
|
86
|
+
/** The subset of `currentOverlaps` whose exit `checkOverlaps()` detects by polling, because matter's
|
|
87
|
+
* detector doesn't pair them with this (static) trigger body - see {@link isPolled}. A body moves
|
|
88
|
+
* back to the native-event path once a native `collisionStart` reports it (it woke up inside). */
|
|
89
|
+
this.polledOverlaps = new Set();
|
|
55
90
|
this.nativeBody.isSensor = true;
|
|
91
|
+
// A trigger is a fixed volume: left non-static, matter-js integrates its sensor body under
|
|
92
|
+
// gravity like any dynamic body, and since a sensor never collides it falls through the level
|
|
93
|
+
// (overlapping, and reporting, whatever it passes on the way down). Static also keeps
|
|
94
|
+
// static-static pairs (trigger vs level geometry) out of matter's broadphase entirely - which
|
|
95
|
+
// drops kinematic and sleeping bodies too, so `checkOverlaps()` polls those.
|
|
96
|
+
Body.setStatic(this.nativeBody, true);
|
|
56
97
|
merge(this.onEnter$.pipe(map(() => true)), this.onLeft$.pipe(map(() => false))).subscribe(enter => {
|
|
57
98
|
if (enter) {
|
|
58
99
|
this.intersectionsAmount++;
|
|
@@ -70,17 +111,39 @@ export class MatterTriggerComponent extends MatterRigidBodyComponent {
|
|
|
70
111
|
}
|
|
71
112
|
this.intersectionsAmount = 0;
|
|
72
113
|
this.currentOverlaps.clear();
|
|
114
|
+
this.polledOverlaps.clear();
|
|
73
115
|
super.addToWorld(world);
|
|
74
116
|
Events.on(world.physicsWorld.matterEngine, 'collisionStart', this.handleCollisionStart);
|
|
75
117
|
Events.on(world.physicsWorld.matterEngine, 'collisionEnd', this.handleCollisionEnd);
|
|
118
|
+
this.removedSub = world.physicsWorld.removed$.subscribe(c => {
|
|
119
|
+
if (c === this) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (c instanceof MatterCharacterControllerComponent) {
|
|
123
|
+
if (this.currentCharacterOverlaps.delete(c)) {
|
|
124
|
+
queueMicrotask(() => this.onLeft$.next(c));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else if (c instanceof MatterRigidBodyComponent && this.currentOverlaps.delete(c)) {
|
|
128
|
+
this.polledOverlaps.delete(c);
|
|
129
|
+
queueMicrotask(() => this.onLeft$.next(c));
|
|
130
|
+
}
|
|
131
|
+
});
|
|
76
132
|
}
|
|
77
133
|
removeFromWorld(world, dispose) {
|
|
134
|
+
var _a;
|
|
78
135
|
Events.off(world.physicsWorld.matterEngine, 'collisionStart', this.handleCollisionStart);
|
|
79
136
|
Events.off(world.physicsWorld.matterEngine, 'collisionEnd', this.handleCollisionEnd);
|
|
137
|
+
(_a = this.removedSub) === null || _a === void 0 ? void 0 : _a.unsubscribe();
|
|
80
138
|
for (const body of this.currentOverlaps) {
|
|
81
139
|
this.onLeft$.next(body);
|
|
82
140
|
}
|
|
83
141
|
this.currentOverlaps.clear();
|
|
142
|
+
this.polledOverlaps.clear();
|
|
143
|
+
for (const character of this.currentCharacterOverlaps) {
|
|
144
|
+
this.onLeft$.next(character);
|
|
145
|
+
}
|
|
146
|
+
this.currentCharacterOverlaps.clear();
|
|
84
147
|
super.removeFromWorld(world, dispose);
|
|
85
148
|
}
|
|
86
149
|
/** Completes `onEnter$`/`onLeft$` on top of `MatterRigidBodyComponent.dispose()`'s own
|
|
@@ -91,8 +154,80 @@ export class MatterTriggerComponent extends MatterRigidBodyComponent {
|
|
|
91
154
|
this.onLeft$.complete();
|
|
92
155
|
super.dispose();
|
|
93
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* Awake dynamic bodies are handled by `handleCollisionStart`/`handleCollisionEnd` above, off
|
|
159
|
+
* matter's own native `collisionStart`/`collisionEnd` engine events; kinematic and sleeping ones,
|
|
160
|
+
* which matter never pairs with this static body, are polled by `checkPolledBodyOverlaps()`.
|
|
161
|
+
* A `MatterCharacterControllerComponent`'s own phantom body is
|
|
162
|
+
* deliberately never added to `Composite`/`engine.world` at all (see that class's own doc), so no
|
|
163
|
+
* native collision pair - and thus no native event - can ever involve it. Since `checkOverlaps()` is
|
|
164
|
+
* already called once per tick by `Trigger2dEntity` regardless of backend, this is the natural place
|
|
165
|
+
* to add the poll this needs instead of inventing a second, differently-shaped mechanism: every
|
|
166
|
+
* `MatterCharacterControllerComponent` currently in the world (`world.children`, which - unlike
|
|
167
|
+
* matter's own `Composite` - already tracks it) is tested against this trigger's own body via
|
|
168
|
+
* `Query.collides`, diffed against `currentCharacterOverlaps` to fire `onEntityEntered`/
|
|
169
|
+
* `onEntityLeft` exactly on the enter/exit transitions, the same as the native-event path does for
|
|
170
|
+
* ordinary bodies.
|
|
171
|
+
*/
|
|
94
172
|
checkOverlaps() {
|
|
95
|
-
|
|
173
|
+
this.checkPolledBodyOverlaps();
|
|
174
|
+
// `Query.collides` tests raw geometry only and knows nothing about `collisionFilter` (see
|
|
175
|
+
// `MatterCharacterControllerComponent.collectObstacles`'s own doc on this same gap) - call
|
|
176
|
+
// matter's own `Detector.canCollide` directly so a character controller whose collision groups
|
|
177
|
+
// wouldn't ordinarily interact with this trigger isn't falsely reported entering it just because
|
|
178
|
+
// this poll bypasses the broadphase that would otherwise exclude it.
|
|
179
|
+
const characters = this.world.children.filter((c) => c instanceof MatterCharacterControllerComponent &&
|
|
180
|
+
Detector.canCollide(this.nativeBody.collisionFilter, c.nativeBody.collisionFilter));
|
|
181
|
+
const stillOverlapping = new Set();
|
|
182
|
+
if (characters.length > 0) {
|
|
183
|
+
const collisions = Query.collides(this.nativeBody, characters.map(c => c.nativeBody));
|
|
184
|
+
for (const collision of collisions) {
|
|
185
|
+
const otherNative = collision.parentA === this.nativeBody ? collision.parentB : collision.parentA;
|
|
186
|
+
const comp = characters.find(c => c.nativeBody === otherNative);
|
|
187
|
+
if (comp) {
|
|
188
|
+
stillOverlapping.add(comp);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
for (const comp of stillOverlapping) {
|
|
193
|
+
if (!this.currentCharacterOverlaps.has(comp)) {
|
|
194
|
+
this.currentCharacterOverlaps.add(comp);
|
|
195
|
+
this.onEnter$.next(comp);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
for (const comp of this.currentCharacterOverlaps) {
|
|
199
|
+
if (!stillOverlapping.has(comp)) {
|
|
200
|
+
this.currentCharacterOverlaps.delete(comp);
|
|
201
|
+
this.onLeft$.next(comp);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Enter/exit of the rigid bodies matter's detector never pairs with this trigger (see
|
|
207
|
+
* {@link isPolled}): a kinematic platform moving in or out, a body asleep inside. Like the
|
|
208
|
+
* character poll below, `Query.collides` ignores `collisionFilter`, so `Detector.canCollide` is
|
|
209
|
+
* checked by hand.
|
|
210
|
+
*/
|
|
211
|
+
checkPolledBodyOverlaps() {
|
|
212
|
+
for (const comp of this.world.children) {
|
|
213
|
+
if (comp instanceof MatterRigidBodyComponent &&
|
|
214
|
+
!(comp instanceof MatterTriggerComponent) &&
|
|
215
|
+
!this.currentOverlaps.has(comp) &&
|
|
216
|
+
this.isPolled(comp) &&
|
|
217
|
+
Detector.canCollide(this.nativeBody.collisionFilter, comp.nativeBody.collisionFilter) &&
|
|
218
|
+
this.overlaps(comp)) {
|
|
219
|
+
this.currentOverlaps.add(comp);
|
|
220
|
+
this.polledOverlaps.add(comp);
|
|
221
|
+
this.onEnter$.next(comp);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
for (const comp of this.polledOverlaps) {
|
|
225
|
+
if (!this.overlaps(comp)) {
|
|
226
|
+
this.polledOverlaps.delete(comp);
|
|
227
|
+
this.currentOverlaps.delete(comp);
|
|
228
|
+
this.onLeft$.next(comp);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
96
231
|
}
|
|
97
232
|
clone() {
|
|
98
233
|
const clonedBody = Body.create(Object.assign(Object.assign({}, this.nativeBody), { isSensor: true, collisionFilter: Object.assign({}, this.nativeBody.collisionFilter) }));
|
|
@@ -5,24 +5,40 @@ import { MatterPhysicsTypeDocRepo } from '../types';
|
|
|
5
5
|
import { Subject } from 'rxjs';
|
|
6
6
|
import { MatterRigidBodyComponent } from './matter-rigid-body.component';
|
|
7
7
|
import { MatterTriggerComponent } from './matter-trigger.component';
|
|
8
|
+
import { MatterCharacterControllerComponent } from './matter-character-controller.component';
|
|
9
|
+
type MatterWorldChild = MatterRigidBodyComponent | MatterTriggerComponent | MatterCharacterControllerComponent;
|
|
8
10
|
export declare class MatterWorldComponent implements IPhysicsWorld2dComponent<MatterPhysicsTypeDocRepo> {
|
|
9
11
|
protected matterEngine_: Engine | null;
|
|
10
12
|
get matterEngine(): Engine | null;
|
|
11
13
|
get matterWorld(): World | null;
|
|
12
14
|
readonly factory: MatterFactory;
|
|
13
|
-
readonly added$: Subject<
|
|
14
|
-
readonly removed$: Subject<
|
|
15
|
-
readonly children:
|
|
15
|
+
readonly added$: Subject<MatterWorldChild>;
|
|
16
|
+
readonly removed$: Subject<MatterWorldChild>;
|
|
17
|
+
readonly children: MatterWorldChild[];
|
|
16
18
|
/** Mirrors the rapier packages' `handleIdEntityMap` pattern: `Body.id` (matter-js's own
|
|
17
19
|
* globally-unique numeric id, assigned once per body via `Body.nextId` and stable for its whole
|
|
18
20
|
* lifetime) to component, kept in sync alongside `children` so `findRigidBody` - called once per
|
|
19
|
-
* collision pair, per step - is an O(1) lookup instead of an O(n) `Array.find` scan.
|
|
20
|
-
|
|
21
|
+
* collision pair, per step - is an O(1) lookup instead of an O(n) `Array.find` scan. Also used by
|
|
22
|
+
* `MatterCharacterControllerComponent.pushDynamicBodies` to resolve a native body it just bumped
|
|
23
|
+
* into back to its owning component. */
|
|
24
|
+
readonly handleIdEntityMap: Map<number, MatterWorldChild>;
|
|
21
25
|
private _gravity;
|
|
22
26
|
get gravity(): Point2;
|
|
23
27
|
set gravity(value: Point2);
|
|
24
28
|
readonly mainCollisionGroup: CollisionGroup;
|
|
25
29
|
constructor();
|
|
30
|
+
/**
|
|
31
|
+
* `Engine.create` is never given `enableSleeping: true` here, matching matter-js's own default -
|
|
32
|
+
* so a body this world creates never *naturally* falls asleep from prolonged inactivity, no matter
|
|
33
|
+
* how long it rests. `IRigidBodyComponent.sleep()`/`wakeUp()` still work regardless of this flag
|
|
34
|
+
* (`Matter.Sleeping.set` writes a body's `isSleeping` flag directly, and `Engine.update`'s own
|
|
35
|
+
* per-body integration skip checks that flag unconditionally, not gated behind
|
|
36
|
+
* `engine.enableSleeping` - only the *automatic* fall-asleep-from-rest/wake-on-collision behavior
|
|
37
|
+
* is gated behind that flag) - only automatic, inactivity-driven sleeping is unavailable under the
|
|
38
|
+
* current setup. A future change enabling it should re-check
|
|
39
|
+
* `gg-engine-physics-adapter-matter`'s own note on this before assuming every existing
|
|
40
|
+
* position/velocity setter still behaves the same way once bodies can sleep on their own.
|
|
41
|
+
*/
|
|
26
42
|
init(): Promise<void>;
|
|
27
43
|
private findRigidBody;
|
|
28
44
|
/**
|
|
@@ -54,6 +70,29 @@ export declare class MatterWorldComponent implements IPhysicsWorld2dComponent<Ma
|
|
|
54
70
|
deregisterCollisionGroup(group: CollisionGroup): void;
|
|
55
71
|
private lastDelta;
|
|
56
72
|
simulate(delta: number): void;
|
|
73
|
+
/**
|
|
74
|
+
* matter-js has no native raycast query. Built on a true parametric ray-vs-polygon intersection
|
|
75
|
+
* (`segmentIntersection`/`bodyPolygons`), not `Matter.Query.ray` - that helper is only a thin
|
|
76
|
+
* wrapper over `Query.collides` (a full-geometry SAT test of a synthetic, very thin rectangle body
|
|
77
|
+
* against candidates), which reports an approximate overlap contact point, not a true "where does
|
|
78
|
+
* the ray segment first cross this body's boundary" point; verified via this adapter's own
|
|
79
|
+
* regression suite (`MatterWorldComponent.spec.ts`'s `Raycast` block expects a precise hit point at
|
|
80
|
+
* a known box edge) that the SAT-approximated version produces a visibly wrong point.
|
|
81
|
+
*
|
|
82
|
+
* `Query.collides`/`Collision.collides` test raw geometry only and know nothing about
|
|
83
|
+
* `collisionFilter` (the same limitation `MatterCharacterControllerComponent.collectObstacles`'s
|
|
84
|
+
* own doc describes) - candidates are pre-filtered here via `Detector.canCollide` itself, called
|
|
85
|
+
* directly against a synthetic filter built from `options.collisionFilterGroups`/
|
|
86
|
+
* `collisionFilterMask`. Sensor bodies (triggers) are excluded from candidates entirely, mirroring
|
|
87
|
+
* `collectObstacles`'s own exclusion - a trigger never physically blocks anything, so it shouldn't
|
|
88
|
+
* register as a raycast hit either.
|
|
89
|
+
*
|
|
90
|
+
* `hitNormal` is derived from whichever polygon edge the closest intersection landed on (rotated
|
|
91
|
+
* 90°, sign chosen to point back towards `options.from`) - not from a matter-js collision object at
|
|
92
|
+
* all, sidestepping the sign-convention pitfall `handleCollisionStart`'s own doc describes for
|
|
93
|
+
* `pair.collision.normal`.
|
|
94
|
+
*/
|
|
57
95
|
raycast(options: RaycastOptions<Point2>): RaycastResult<Point2, MatterRigidBodyComponent>;
|
|
58
96
|
dispose(): void;
|
|
59
97
|
}
|
|
98
|
+
export {};
|