@gg-web-engine/matter 0.0.73 → 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-rigid-body.component.d.ts +26 -0
- package/dist/components/matter-rigid-body.component.js +41 -3
- package/dist/components/matter-trigger.component.d.ts +23 -3
- package/dist/components/matter-trigger.component.js +69 -4
- package/dist/components/matter-world.component.d.ts +12 -0
- package/dist/components/matter-world.component.js +12 -0
- package/package.json +3 -3
- package/test/components/matter-rigid-body-sleep.spec.ts +79 -0
- package/test/components/matter-rigid-body.component.spec.ts +40 -0
- package/test/components/matter-trigger.component.spec.ts +94 -0
|
@@ -60,4 +60,30 @@ export declare class MatterRigidBodyComponent implements IRigidBody2dComponent<M
|
|
|
60
60
|
removeFromWorld(world: MatterGgWorld, dispose?: boolean): void;
|
|
61
61
|
dispose(): void;
|
|
62
62
|
resetMotion(): void;
|
|
63
|
+
/**
|
|
64
|
+
* A `kinematic_pos`/`kinematic_vel` request also reports `isStatic: true` here - see this
|
|
65
|
+
* component's own `bodyType`/`bodyOptions` doc for why matter-js can't distinguish that from a
|
|
66
|
+
* genuine `'static'` body natively. Both are equally never simulated as a sleepable dynamic body,
|
|
67
|
+
* so gating on the native `isStatic` flag rather than `this.bodyType === 'static'` is correct for
|
|
68
|
+
* both cases, not just the literal static one.
|
|
69
|
+
*
|
|
70
|
+
* Also always `false` regardless of `isStatic` unless the world's underlying `Matter.Engine` was
|
|
71
|
+
* created with `enableSleeping: true` - this adapter never turns that on itself (see
|
|
72
|
+
* `MatterWorldComponent`'s own doc on its `matterWorld`/engine setup), so a body constructed
|
|
73
|
+
* through this adapter alone never actually falls asleep on its own; `sleep()` below still forces
|
|
74
|
+
* it regardless of that engine setting.
|
|
75
|
+
*/
|
|
76
|
+
get isSleeping(): boolean;
|
|
77
|
+
/**
|
|
78
|
+
* No-op on a body that reports `isStatic` (a genuine `'static'` body, or a `kinematic_pos`/
|
|
79
|
+
* `kinematic_vel` request - see `isSleeping`'s own doc for why both are treated the same here).
|
|
80
|
+
*/
|
|
81
|
+
wakeUp(): void;
|
|
82
|
+
/**
|
|
83
|
+
* No-op on a body that reports `isStatic` (see `isSleeping`'s own doc). Forces sleep immediately,
|
|
84
|
+
* regardless of whether the world's `Matter.Engine` has `enableSleeping` turned on - unlike a
|
|
85
|
+
* body naturally falling asleep from inactivity (which requires that engine flag), an explicit
|
|
86
|
+
* `Sleeping.set(body, true)` call takes effect either way.
|
|
87
|
+
*/
|
|
88
|
+
sleep(): void;
|
|
63
89
|
}
|
|
@@ -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);
|
|
@@ -70,7 +70,7 @@ export class MatterRigidBodyComponent {
|
|
|
70
70
|
// rather than what an app originally asked for - the console warning at creation time is what
|
|
71
71
|
// tells a developer their kinematic request wasn't honored, not this view.
|
|
72
72
|
this.debugBodySettings = new DebugBody2DSettings(isFinite(this.nativeBody.mass)
|
|
73
|
-
? { type: 'RIGID_DYNAMIC', sleeping: () => this.
|
|
73
|
+
? { type: 'RIGID_DYNAMIC', sleeping: () => this.isSleeping }
|
|
74
74
|
: { type: 'RIGID_STATIC' }, this.shape);
|
|
75
75
|
this._interactWithCGsMask = BitMask.full(16);
|
|
76
76
|
this._ownCGsMask = BitMask.full(16);
|
|
@@ -172,4 +172,42 @@ export class MatterRigidBodyComponent {
|
|
|
172
172
|
Body.setVelocity(this.nativeBody, Pnt2.O);
|
|
173
173
|
Body.setAngularVelocity(this.nativeBody, 0);
|
|
174
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
|
+
}
|
|
175
213
|
}
|
|
@@ -20,6 +20,10 @@ export declare class MatterTriggerComponent extends MatterRigidBodyComponent imp
|
|
|
20
20
|
readonly debugBodySettings: DebugBody2DSettings;
|
|
21
21
|
protected intersectionsAmount: number;
|
|
22
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>;
|
|
23
27
|
/** `Composite.remove` (what `removeFromWorld` calls) never fires a native `collisionEnd` for the
|
|
24
28
|
* body it removes - matter-js simply stops considering that body's pairs on the next step, it
|
|
25
29
|
* doesn't retroactively report the pairs that were active at removal time. Without this, a body
|
|
@@ -44,6 +48,14 @@ export declare class MatterTriggerComponent extends MatterRigidBodyComponent imp
|
|
|
44
48
|
private removedSub?;
|
|
45
49
|
private handleCollisionStart;
|
|
46
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;
|
|
47
59
|
constructor(nativeBody: Body, shape: Shape2DDescriptor, world: MatterWorldComponent);
|
|
48
60
|
addToWorld(world: MatterGgWorld): void;
|
|
49
61
|
removeFromWorld(world: MatterGgWorld, dispose?: boolean): void;
|
|
@@ -52,9 +64,10 @@ export declare class MatterTriggerComponent extends MatterRigidBodyComponent imp
|
|
|
52
64
|
* enter/exit subjects are a separate pair this subclass owns and must complete itself. */
|
|
53
65
|
dispose(): void;
|
|
54
66
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
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
|
|
58
71
|
* deliberately never added to `Composite`/`engine.world` at all (see that class's own doc), so no
|
|
59
72
|
* native collision pair - and thus no native event - can ever involve it. Since `checkOverlaps()` is
|
|
60
73
|
* already called once per tick by `Trigger2dEntity` regardless of backend, this is the natural place
|
|
@@ -66,5 +79,12 @@ export declare class MatterTriggerComponent extends MatterRigidBodyComponent imp
|
|
|
66
79
|
* ordinary bodies.
|
|
67
80
|
*/
|
|
68
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;
|
|
69
89
|
clone(): MatterTriggerComponent;
|
|
70
90
|
}
|
|
@@ -22,6 +22,11 @@ export class MatterTriggerComponent extends MatterRigidBodyComponent {
|
|
|
22
22
|
if (body) {
|
|
23
23
|
let comp = this.world.children.find(c => c.nativeBody === body);
|
|
24
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
|
+
}
|
|
25
30
|
this.currentOverlaps.add(comp);
|
|
26
31
|
this.onEnter$.next(comp);
|
|
27
32
|
}
|
|
@@ -39,13 +44,31 @@ export class MatterTriggerComponent extends MatterRigidBodyComponent {
|
|
|
39
44
|
}
|
|
40
45
|
if (body) {
|
|
41
46
|
let comp = this.world.children.find(c => c.nativeBody === body);
|
|
42
|
-
if (comp instanceof MatterRigidBodyComponent) {
|
|
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
|
+
}
|
|
43
53
|
this.currentOverlaps.delete(comp);
|
|
54
|
+
this.polledOverlaps.delete(comp);
|
|
44
55
|
this.onLeft$.next(comp);
|
|
45
56
|
}
|
|
46
57
|
}
|
|
47
58
|
}
|
|
48
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
|
+
}
|
|
49
72
|
constructor(nativeBody, shape, world) {
|
|
50
73
|
super(nativeBody, shape);
|
|
51
74
|
this.shape = shape;
|
|
@@ -60,7 +83,17 @@ export class MatterTriggerComponent extends MatterRigidBodyComponent {
|
|
|
60
83
|
this.debugBodySettings = new DebugBody2DSettings({ type: 'TRIGGER', activated: () => this.intersectionsAmount > 0 }, this.shape);
|
|
61
84
|
this.intersectionsAmount = 0;
|
|
62
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();
|
|
63
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);
|
|
64
97
|
merge(this.onEnter$.pipe(map(() => true)), this.onLeft$.pipe(map(() => false))).subscribe(enter => {
|
|
65
98
|
if (enter) {
|
|
66
99
|
this.intersectionsAmount++;
|
|
@@ -78,6 +111,7 @@ export class MatterTriggerComponent extends MatterRigidBodyComponent {
|
|
|
78
111
|
}
|
|
79
112
|
this.intersectionsAmount = 0;
|
|
80
113
|
this.currentOverlaps.clear();
|
|
114
|
+
this.polledOverlaps.clear();
|
|
81
115
|
super.addToWorld(world);
|
|
82
116
|
Events.on(world.physicsWorld.matterEngine, 'collisionStart', this.handleCollisionStart);
|
|
83
117
|
Events.on(world.physicsWorld.matterEngine, 'collisionEnd', this.handleCollisionEnd);
|
|
@@ -91,6 +125,7 @@ export class MatterTriggerComponent extends MatterRigidBodyComponent {
|
|
|
91
125
|
}
|
|
92
126
|
}
|
|
93
127
|
else if (c instanceof MatterRigidBodyComponent && this.currentOverlaps.delete(c)) {
|
|
128
|
+
this.polledOverlaps.delete(c);
|
|
94
129
|
queueMicrotask(() => this.onLeft$.next(c));
|
|
95
130
|
}
|
|
96
131
|
});
|
|
@@ -104,6 +139,7 @@ export class MatterTriggerComponent extends MatterRigidBodyComponent {
|
|
|
104
139
|
this.onLeft$.next(body);
|
|
105
140
|
}
|
|
106
141
|
this.currentOverlaps.clear();
|
|
142
|
+
this.polledOverlaps.clear();
|
|
107
143
|
for (const character of this.currentCharacterOverlaps) {
|
|
108
144
|
this.onLeft$.next(character);
|
|
109
145
|
}
|
|
@@ -119,9 +155,10 @@ export class MatterTriggerComponent extends MatterRigidBodyComponent {
|
|
|
119
155
|
super.dispose();
|
|
120
156
|
}
|
|
121
157
|
/**
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
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
|
|
125
162
|
* deliberately never added to `Composite`/`engine.world` at all (see that class's own doc), so no
|
|
126
163
|
* native collision pair - and thus no native event - can ever involve it. Since `checkOverlaps()` is
|
|
127
164
|
* already called once per tick by `Trigger2dEntity` regardless of backend, this is the natural place
|
|
@@ -133,6 +170,7 @@ export class MatterTriggerComponent extends MatterRigidBodyComponent {
|
|
|
133
170
|
* ordinary bodies.
|
|
134
171
|
*/
|
|
135
172
|
checkOverlaps() {
|
|
173
|
+
this.checkPolledBodyOverlaps();
|
|
136
174
|
// `Query.collides` tests raw geometry only and knows nothing about `collisionFilter` (see
|
|
137
175
|
// `MatterCharacterControllerComponent.collectObstacles`'s own doc on this same gap) - call
|
|
138
176
|
// matter's own `Detector.canCollide` directly so a character controller whose collision groups
|
|
@@ -164,6 +202,33 @@ export class MatterTriggerComponent extends MatterRigidBodyComponent {
|
|
|
164
202
|
}
|
|
165
203
|
}
|
|
166
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
|
+
}
|
|
231
|
+
}
|
|
167
232
|
clone() {
|
|
168
233
|
const clonedBody = Body.create(Object.assign(Object.assign({}, this.nativeBody), { isSensor: true, collisionFilter: Object.assign({}, this.nativeBody.collisionFilter) }));
|
|
169
234
|
const component = new MatterTriggerComponent(clonedBody, this.shape, this.world);
|
|
@@ -27,6 +27,18 @@ export declare class MatterWorldComponent implements IPhysicsWorld2dComponent<Ma
|
|
|
27
27
|
set gravity(value: Point2);
|
|
28
28
|
readonly mainCollisionGroup: CollisionGroup;
|
|
29
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
|
+
*/
|
|
30
42
|
init(): Promise<void>;
|
|
31
43
|
private findRigidBody;
|
|
32
44
|
/**
|
|
@@ -118,6 +118,18 @@ export class MatterWorldComponent {
|
|
|
118
118
|
this.handleCollisionStart = this.handleCollisionStart.bind(this);
|
|
119
119
|
this.handleCollisionEnd = this.handleCollisionEnd.bind(this);
|
|
120
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* `Engine.create` is never given `enableSleeping: true` here, matching matter-js's own default -
|
|
123
|
+
* so a body this world creates never *naturally* falls asleep from prolonged inactivity, no matter
|
|
124
|
+
* how long it rests. `IRigidBodyComponent.sleep()`/`wakeUp()` still work regardless of this flag
|
|
125
|
+
* (`Matter.Sleeping.set` writes a body's `isSleeping` flag directly, and `Engine.update`'s own
|
|
126
|
+
* per-body integration skip checks that flag unconditionally, not gated behind
|
|
127
|
+
* `engine.enableSleeping` - only the *automatic* fall-asleep-from-rest/wake-on-collision behavior
|
|
128
|
+
* is gated behind that flag) - only automatic, inactivity-driven sleeping is unavailable under the
|
|
129
|
+
* current setup. A future change enabling it should re-check
|
|
130
|
+
* `gg-engine-physics-adapter-matter`'s own note on this before assuming every existing
|
|
131
|
+
* position/velocity setter still behaves the same way once bodies can sleep on their own.
|
|
132
|
+
*/
|
|
121
133
|
init() {
|
|
122
134
|
return __awaiter(this, void 0, void 0, function* () {
|
|
123
135
|
this.matterEngine_ = Engine.create({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gg-web-engine/matter",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.74",
|
|
4
4
|
"description": "An attempt to create open source game engine for browser",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"poly-decomp": "^0.3.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
|
-
"@gg-web-engine/core": "0.0.
|
|
33
|
+
"@gg-web-engine/core": "0.0.74",
|
|
34
34
|
"@types/jest": "^30.0.0",
|
|
35
35
|
"@types/matter-js": "0.20.2",
|
|
36
36
|
"@types/node": "^26.4.0",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"typescript": "~6.0.3"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
|
-
"@gg-web-engine/core": "0.0.
|
|
46
|
+
"@gg-web-engine/core": "0.0.74",
|
|
47
47
|
"matter-js": "0.20.0",
|
|
48
48
|
"rxjs": "7.8.2"
|
|
49
49
|
},
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Pnt2 } from '@gg-web-engine/core';
|
|
2
|
+
import { MatterFactory, MatterWorldComponent } from '../../src';
|
|
3
|
+
|
|
4
|
+
describe('MatterRigidBodyComponent sleep API', () => {
|
|
5
|
+
let world: MatterWorldComponent;
|
|
6
|
+
let factory: MatterFactory;
|
|
7
|
+
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
if (world) {
|
|
10
|
+
world.dispose();
|
|
11
|
+
}
|
|
12
|
+
world = new MatterWorldComponent();
|
|
13
|
+
factory = new MatterFactory(world);
|
|
14
|
+
await world.init();
|
|
15
|
+
world.gravity = Pnt2.O;
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterAll(() => {
|
|
19
|
+
world.dispose();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('reports a fresh dynamic body as not sleeping', () => {
|
|
23
|
+
const body = factory.createRigidBody(
|
|
24
|
+
{ shape: { shape: 'CIRCLE', radius: 1 }, body: { bodyType: 'dynamic', mass: 1 } },
|
|
25
|
+
{ position: { x: 0, y: 0 } },
|
|
26
|
+
);
|
|
27
|
+
body.addToWorld({ physicsWorld: world } as any);
|
|
28
|
+
|
|
29
|
+
expect(body.isSleeping).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('sleep() forces isSleeping true, wakeUp() flips it back - regardless of the world engine\'s enableSleeping setting', () => {
|
|
33
|
+
const body = factory.createRigidBody(
|
|
34
|
+
{ shape: { shape: 'CIRCLE', radius: 1 }, body: { bodyType: 'dynamic', mass: 1 } },
|
|
35
|
+
{ position: { x: 0, y: 0 } },
|
|
36
|
+
);
|
|
37
|
+
body.addToWorld({ physicsWorld: world } as any);
|
|
38
|
+
|
|
39
|
+
body.sleep();
|
|
40
|
+
expect(body.isSleeping).toBe(true);
|
|
41
|
+
|
|
42
|
+
body.wakeUp();
|
|
43
|
+
expect(body.isSleeping).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('a position write on a sleeping body does NOT wake it back up - unlike every other adapter', () => {
|
|
47
|
+
const body = factory.createRigidBody(
|
|
48
|
+
{ shape: { shape: 'CIRCLE', radius: 1 }, body: { bodyType: 'dynamic', mass: 1 } },
|
|
49
|
+
{ position: { x: 0, y: 0 } },
|
|
50
|
+
);
|
|
51
|
+
body.addToWorld({ physicsWorld: world } as any);
|
|
52
|
+
|
|
53
|
+
body.sleep();
|
|
54
|
+
expect(body.isSleeping).toBe(true);
|
|
55
|
+
|
|
56
|
+
body.position = { x: 1, y: 2 };
|
|
57
|
+
expect(body.isSleeping).toBe(true);
|
|
58
|
+
// the write itself is not silently dropped - a caller that must write state on a sleeping
|
|
59
|
+
// body without waking it (the whole point of this adapter's setters preserving sleep state)
|
|
60
|
+
// still sees the new position take effect.
|
|
61
|
+
expect(body.position).toEqual({ x: 1, y: 2 });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('a static body always reports not sleeping, and sleep()/wakeUp() are no-ops', () => {
|
|
65
|
+
const floor = factory.createRigidBody(
|
|
66
|
+
{ shape: { shape: 'BOX', dimensions: { x: 10, y: 1 } }, body: { bodyType: 'static' } },
|
|
67
|
+
{ position: { x: 0, y: 0 } },
|
|
68
|
+
);
|
|
69
|
+
floor.addToWorld({ physicsWorld: world } as any);
|
|
70
|
+
|
|
71
|
+
expect(floor.isSleeping).toBe(false);
|
|
72
|
+
|
|
73
|
+
floor.sleep();
|
|
74
|
+
expect(floor.isSleeping).toBe(false);
|
|
75
|
+
|
|
76
|
+
floor.wakeUp();
|
|
77
|
+
expect(floor.isSleeping).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -56,3 +56,43 @@ describe('MatterRigidBodyComponent.bodyOptions', () => {
|
|
|
56
56
|
expect(body.bodyOptions.interactWithCollisionGroups).toEqual([5, 6]);
|
|
57
57
|
});
|
|
58
58
|
});
|
|
59
|
+
|
|
60
|
+
describe('MatterRigidBodyComponent.rotation', () => {
|
|
61
|
+
let world: MatterWorldComponent;
|
|
62
|
+
let factory: MatterFactory;
|
|
63
|
+
|
|
64
|
+
beforeEach(async () => {
|
|
65
|
+
if (world) {
|
|
66
|
+
world.dispose();
|
|
67
|
+
}
|
|
68
|
+
world = new MatterWorldComponent();
|
|
69
|
+
factory = new MatterFactory(world);
|
|
70
|
+
await world.init();
|
|
71
|
+
world.gravity = Pnt2.O;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
afterAll(() => {
|
|
75
|
+
world.dispose();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('rotates via Body.setAngle, preserving angular velocity and actually rotating the vertices', () => {
|
|
79
|
+
const body = factory.createRigidBody(
|
|
80
|
+
{ shape: { shape: 'BOX', dimensions: { x: 2, y: 1 } }, body: { bodyType: 'dynamic', mass: 1 } },
|
|
81
|
+
{ position: { x: 0, y: 0 } },
|
|
82
|
+
);
|
|
83
|
+
body.addToWorld({ physicsWorld: world } as any);
|
|
84
|
+
|
|
85
|
+
body.rotation = Math.PI / 2;
|
|
86
|
+
world.simulate(60);
|
|
87
|
+
|
|
88
|
+
expect(body.rotation).toBeCloseTo(Math.PI / 2);
|
|
89
|
+
expect(body.angularVelocity).toBeCloseTo(0);
|
|
90
|
+
|
|
91
|
+
// A 2x1 box rotated 90 degrees should report swapped width/height bounds - proof the
|
|
92
|
+
// underlying vertices were actually rotated, not just the `.angle` field bumped.
|
|
93
|
+
const width = body.nativeBody.bounds.max.x - body.nativeBody.bounds.min.x;
|
|
94
|
+
const height = body.nativeBody.bounds.max.y - body.nativeBody.bounds.min.y;
|
|
95
|
+
expect(width).toBeCloseTo(1);
|
|
96
|
+
expect(height).toBeCloseTo(2);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -19,6 +19,100 @@ describe(`MatterTriggerComponent`, () => {
|
|
|
19
19
|
world.dispose();
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
+
it(`stays where it was placed under gravity, and never reports static level geometry`, async () => {
|
|
23
|
+
world.gravity = { x: 0, y: 9.82 };
|
|
24
|
+
const ground = factory.createRigidBody(
|
|
25
|
+
{ shape: { shape: 'BOX', dimensions: { x: 100, y: 10 } }, body: { bodyType: 'static', mass: 0 } },
|
|
26
|
+
{ position: { x: 0, y: 50 } },
|
|
27
|
+
);
|
|
28
|
+
ground.addToWorld({ physicsWorld: world } as any);
|
|
29
|
+
const trigger = factory.createTrigger({ shape: 'BOX', dimensions: { x: 10, y: 10 } }, { position: { x: 0, y: 0 } });
|
|
30
|
+
trigger.addToWorld({ physicsWorld: world } as any);
|
|
31
|
+
let entered = 0;
|
|
32
|
+
trigger.onEntityEntered.subscribe(() => entered++);
|
|
33
|
+
for (let i = 0; i < 300; i++) {
|
|
34
|
+
world.simulate(16);
|
|
35
|
+
trigger.checkOverlaps();
|
|
36
|
+
}
|
|
37
|
+
expect(trigger.position).toEqual({ x: 0, y: 0 });
|
|
38
|
+
expect(entered).toBe(0);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it(`detects a kinematic body moving in and out`, async () => {
|
|
42
|
+
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
43
|
+
const trigger = factory.createTrigger({ shape: 'BOX', dimensions: { x: 10, y: 10 } });
|
|
44
|
+
trigger.addToWorld({ physicsWorld: world } as any);
|
|
45
|
+
const platform = factory.createRigidBody(
|
|
46
|
+
{ shape: { shape: 'BOX', dimensions: { x: 2, y: 2 } }, body: { bodyType: 'kinematic_pos', mass: 0 } },
|
|
47
|
+
{ position: { x: 20, y: 0 } },
|
|
48
|
+
);
|
|
49
|
+
warn.mockRestore();
|
|
50
|
+
platform.addToWorld({ physicsWorld: world } as any);
|
|
51
|
+
const events: string[] = [];
|
|
52
|
+
trigger.onEntityEntered.subscribe(obj => events.push(obj === platform ? 'enter' : 'other'));
|
|
53
|
+
trigger.onEntityLeft.subscribe(obj => events.push(obj === platform ? 'left' : 'other'));
|
|
54
|
+
const step = () => {
|
|
55
|
+
world.simulate(16);
|
|
56
|
+
trigger.checkOverlaps();
|
|
57
|
+
};
|
|
58
|
+
step();
|
|
59
|
+
platform.position = { x: 0, y: 0 };
|
|
60
|
+
step();
|
|
61
|
+
step();
|
|
62
|
+
expect(events).toEqual(['enter']);
|
|
63
|
+
platform.position = { x: 20, y: 0 };
|
|
64
|
+
step();
|
|
65
|
+
expect(events).toEqual(['enter', 'left']);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it(`keeps a body that falls asleep inside, and reports it leaving after waking up`, async () => {
|
|
69
|
+
const trigger = factory.createTrigger({ shape: 'BOX', dimensions: { x: 10, y: 10 } });
|
|
70
|
+
trigger.addToWorld({ physicsWorld: world } as any);
|
|
71
|
+
const box = factory.createRigidBody(
|
|
72
|
+
{ shape: { shape: 'BOX', dimensions: { x: 2, y: 2 } }, body: { bodyType: 'dynamic', mass: 1 } },
|
|
73
|
+
{ position: Pnt2.O },
|
|
74
|
+
);
|
|
75
|
+
box.addToWorld({ physicsWorld: world } as any);
|
|
76
|
+
const events: string[] = [];
|
|
77
|
+
trigger.onEntityEntered.subscribe(obj => events.push(obj === box ? 'enter' : 'other'));
|
|
78
|
+
trigger.onEntityLeft.subscribe(obj => events.push(obj === box ? 'left' : 'other'));
|
|
79
|
+
const step = () => {
|
|
80
|
+
world.simulate(16);
|
|
81
|
+
trigger.checkOverlaps();
|
|
82
|
+
};
|
|
83
|
+
step();
|
|
84
|
+
expect(events).toEqual(['enter']);
|
|
85
|
+
box.sleep();
|
|
86
|
+
for (let i = 0; i < 10; i++) {
|
|
87
|
+
step();
|
|
88
|
+
}
|
|
89
|
+
expect(events).toEqual(['enter']);
|
|
90
|
+
box.wakeUp();
|
|
91
|
+
step();
|
|
92
|
+
step();
|
|
93
|
+
expect(events).toEqual(['enter']);
|
|
94
|
+
box.position = { x: 20, y: 0 };
|
|
95
|
+
step();
|
|
96
|
+
step();
|
|
97
|
+
expect(events).toEqual(['enter', 'left']);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it(`detects a body that is put to sleep before entering`, async () => {
|
|
101
|
+
const trigger = factory.createTrigger({ shape: 'BOX', dimensions: { x: 10, y: 10 } });
|
|
102
|
+
trigger.addToWorld({ physicsWorld: world } as any);
|
|
103
|
+
const box = factory.createRigidBody(
|
|
104
|
+
{ shape: { shape: 'BOX', dimensions: { x: 2, y: 2 } }, body: { bodyType: 'dynamic', mass: 1 } },
|
|
105
|
+
{ position: Pnt2.O },
|
|
106
|
+
);
|
|
107
|
+
box.sleep();
|
|
108
|
+
box.addToWorld({ physicsWorld: world } as any);
|
|
109
|
+
let entered = 0;
|
|
110
|
+
trigger.onEntityEntered.subscribe(() => entered++);
|
|
111
|
+
world.simulate(16);
|
|
112
|
+
trigger.checkOverlaps();
|
|
113
|
+
expect(entered).toBe(1);
|
|
114
|
+
});
|
|
115
|
+
|
|
22
116
|
// Note: this test is identical to rapier2d trigger test, and it is expected
|
|
23
117
|
it(`should detect object intersection`, async () => {
|
|
24
118
|
const trigger = factory.createTrigger({ shape: 'BOX', dimensions: { x: 10, y: 10 } });
|