emeraldengine 3.0.0 → 3.1.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 +1498 -1659
- package/dist/types/index.d.ts +4 -1
- package/dist/types/src/Animator.d.ts +1 -1
- package/dist/types/src/CollisionLayers.d.ts +2 -2
- package/dist/types/src/Color.d.ts +1 -0
- package/dist/types/src/Drawable.d.ts +1 -1
- package/dist/types/src/EmeraldDB.d.ts +2 -2
- package/dist/types/src/InstancedTexture.d.ts +17 -2
- package/dist/types/src/Material.d.ts +2 -2
- package/dist/types/src/MathUtils.d.ts +2 -1
- package/dist/types/src/ParticleEmitter.d.ts +1 -1
- package/dist/types/src/Physics.d.ts +148 -18
- package/dist/types/src/Scene.d.ts +1 -1
- package/dist/types/src/Shaders.d.ts +2 -2
- package/dist/types/src/Tilemap.d.ts +1 -1
- package/dist/types/src/UI.d.ts +1 -1
- package/dist/types/src/components/Behaviour.d.ts +2 -2
- package/dist/types/src/components/Collider.d.ts +7 -1
- package/dist/types/src/components/GameObject.d.ts +2 -2
- package/dist/types/src/components/PolygonCollider.d.ts +33 -0
- package/dist/types/src/components/RigidBody.d.ts +281 -8
- package/dist/types/src/importers/Aseprite.d.ts +2 -2
- package/dist/types/src/importers/ForgeLevel.d.ts +97 -0
- package/dist/types/src/importers/TiledMap.d.ts +1 -1
- package/dist/types/src/managers/EventManager.d.ts +1 -1
- package/dist/types/src/managers/Gamepad.d.ts +102 -0
- package/dist/types/src/managers/InputManager.d.ts +93 -2
- package/dist/types/src/managers/NetworkManager.d.ts +2 -2
- package/dist/types/src/managers/RenderStats.d.ts +1 -1
- package/dist/types/src/managers/TextureManager.d.ts +1 -1
- package/dist/types/src/physics/AABB.d.ts +92 -0
- package/dist/types/src/physics/Body.d.ts +435 -0
- package/dist/types/src/physics/BodyType.d.ts +6 -0
- package/dist/types/src/physics/BroadPhase.d.ts +210 -0
- package/dist/types/src/physics/Collision.d.ts +102 -0
- package/dist/types/src/physics/Contact.d.ts +206 -0
- package/dist/types/src/physics/ContactSolver.d.ts +108 -0
- package/dist/types/src/physics/Distance.d.ts +54 -0
- package/dist/types/src/physics/DistanceJoint.d.ts +90 -0
- package/dist/types/src/physics/Fixture.d.ts +221 -0
- package/dist/types/src/physics/Island.d.ts +52 -0
- package/dist/types/src/physics/Joint.d.ts +59 -0
- package/dist/types/src/physics/Math2D.d.ts +371 -0
- package/dist/types/src/physics/RevoluteJoint.d.ts +119 -0
- package/dist/types/src/physics/Settings.d.ts +22 -0
- package/dist/types/src/physics/Shapes.d.ts +207 -0
- package/dist/types/src/physics/TimeOfImpact.d.ts +22 -0
- package/dist/types/src/physics/World.d.ts +274 -0
- package/dist/types/src/physics/index.d.ts +34 -0
- package/index.js +6 -0
- package/package.json +2 -3
- package/src/Animator.js +1 -1
- package/src/CollisionLayers.js +3 -3
- package/src/Color.js +8 -0
- package/src/Drawable.js +1 -1
- package/src/Emerald.js +1 -1
- package/src/EmeraldDB.js +2 -2
- package/src/InstancedTexture.js +57 -9
- package/src/Material.js +2 -2
- package/src/MathUtils.js +2 -1
- package/src/ParticleEmitter.js +1 -1
- package/src/Physics.js +270 -60
- package/src/Scene.js +1 -1
- package/src/Shaders.js +20 -20
- package/src/Tilemap.js +1 -1
- package/src/UI.js +1 -1
- package/src/components/Behaviour.js +2 -2
- package/src/components/BoxCollider.js +7 -9
- package/src/components/BoxColliderDebug.js +3 -4
- package/src/components/CircleCollider.js +7 -9
- package/src/components/CircleColliderDebug.js +3 -2
- package/src/components/Collider.js +13 -3
- package/src/components/GameObject.js +2 -2
- package/src/components/PolygonCollider.js +55 -0
- package/src/components/RigidBody.js +441 -14
- package/src/importers/Aseprite.js +2 -2
- package/src/importers/ForgeLevel.js +581 -0
- package/src/importers/TiledMap.js +1 -1
- package/src/managers/EventManager.js +1 -1
- package/src/managers/Gamepad.js +126 -0
- package/src/managers/InputManager.js +129 -3
- package/src/managers/NetworkManager.js +2 -2
- package/src/managers/RenderStats.js +1 -1
- package/src/managers/TextureManager.js +1 -1
- package/src/physics/AABB.js +207 -0
- package/src/physics/Body.js +862 -0
- package/src/physics/BodyType.js +16 -0
- package/src/physics/BroadPhase.js +641 -0
- package/src/physics/Collision.js +534 -0
- package/src/physics/Contact.js +500 -0
- package/src/physics/ContactSolver.js +526 -0
- package/src/physics/Distance.js +403 -0
- package/src/physics/DistanceJoint.js +227 -0
- package/src/physics/Fixture.js +346 -0
- package/src/physics/Island.js +203 -0
- package/src/physics/Joint.js +78 -0
- package/src/physics/Math2D.js +573 -0
- package/src/physics/RevoluteJoint.js +278 -0
- package/src/physics/Settings.js +78 -0
- package/src/physics/Shapes.js +549 -0
- package/src/physics/TimeOfImpact.js +87 -0
- package/src/physics/World.js +731 -0
- package/src/physics/index.js +79 -0
package/src/Physics.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { World, Box, Vec2 as PhysicsVec2 } from "./physics/index.js";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @function dispatchCollision
|
|
@@ -23,48 +23,75 @@ function dispatchCollision(object, other, contact, type) {
|
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* @class Physics
|
|
26
|
-
* @description
|
|
26
|
+
* @description The game-facing front end of Emerald's own rigid-body engine
|
|
27
|
+
* (see `src/physics`). It owns the {@link World}, converts between world
|
|
28
|
+
* (pixel) units and physics units via `scale`, steps the simulation on a fixed
|
|
29
|
+
* timestep, and routes contacts to `onCollisionEnter`/`onCollisionExit` on your
|
|
30
|
+
* objects and {@link Behaviour} components.
|
|
27
31
|
* @param {number} gravity - The gravity of the physics engine
|
|
28
|
-
* @param {number} scale -
|
|
29
|
-
* @param {number} velocityThreshold -
|
|
32
|
+
* @param {number} scale - Pixels per physics unit (meter)
|
|
33
|
+
* @param {number} velocityThreshold - Relative speed below which impacts stop
|
|
34
|
+
* bouncing, which is what lets resting bodies settle instead of jittering
|
|
30
35
|
*/
|
|
31
36
|
class Physics {
|
|
32
37
|
constructor(gravity, scale, velocityThreshold = 0.1) {
|
|
33
|
-
this.world = new
|
|
34
|
-
gravity: new
|
|
38
|
+
this.world = new World({
|
|
39
|
+
gravity: new PhysicsVec2(0, gravity),
|
|
40
|
+
velocityThreshold,
|
|
35
41
|
});
|
|
36
|
-
planck.Settings.velocityThreshold = velocityThreshold;
|
|
37
42
|
this.gravity = gravity;
|
|
38
43
|
this.scale = scale;
|
|
39
44
|
|
|
40
45
|
this.fixedTimeStep = 1 / 60;
|
|
41
46
|
this.maxSubSteps = 5;
|
|
47
|
+
/** Largest slice the variable mode will simulate in one step. */
|
|
48
|
+
this.maxTimeStep = 1 / 30;
|
|
49
|
+
/** "fixed" or "variable", see {@link Physics#setVariableTimeStep}. */
|
|
50
|
+
this.timeStepMode = "fixed";
|
|
51
|
+
/** Steps taken by the last process() call. */
|
|
52
|
+
this.lastStepCount = 0;
|
|
42
53
|
/** @private */
|
|
43
54
|
this._accumulator = 0;
|
|
55
|
+
/** World-level listeners added via onCollisionEnter. @private */
|
|
56
|
+
this._enterCallbacks = [];
|
|
57
|
+
/** World-level listeners added via onCollisionExit. @private */
|
|
58
|
+
this._exitCallbacks = [];
|
|
44
59
|
|
|
45
60
|
this._dispatchContacts();
|
|
46
61
|
}
|
|
47
62
|
|
|
48
63
|
/**
|
|
49
64
|
* @method _dispatchContacts
|
|
50
|
-
* @description
|
|
51
|
-
* components receive onCollisionEnter/onCollisionExit
|
|
52
|
-
* RigidBody carry the owner
|
|
65
|
+
* @description Binds contact routing to the current world: owning objects (so
|
|
66
|
+
* Behaviour components receive onCollisionEnter/onCollisionExit) first, then
|
|
67
|
+
* any world-level listeners. Bodies created through RigidBody carry the owner
|
|
68
|
+
* via userData. Re-run whenever the world is replaced, so subscriptions
|
|
69
|
+
* survive a clear().
|
|
53
70
|
* @private
|
|
54
71
|
*/
|
|
55
72
|
_dispatchContacts() {
|
|
56
|
-
const fire = (contact, type) => {
|
|
57
|
-
const
|
|
58
|
-
const
|
|
73
|
+
const fire = (contact, type, callbacks) => {
|
|
74
|
+
const fixtureA = contact.getFixtureA();
|
|
75
|
+
const fixtureB = contact.getFixtureB();
|
|
76
|
+
const bodyA = fixtureA.getBody();
|
|
77
|
+
const bodyB = fixtureB.getBody();
|
|
78
|
+
|
|
79
|
+
const a = bodyA.getUserData();
|
|
80
|
+
const b = bodyB.getUserData();
|
|
59
81
|
const objA = a && a.parentObject ? a.parentObject : null;
|
|
60
82
|
const objB = b && b.parentObject ? b.parentObject : null;
|
|
61
83
|
dispatchCollision(objA, objB, contact, type);
|
|
62
84
|
dispatchCollision(objB, objA, contact, type);
|
|
85
|
+
|
|
86
|
+
for (const callback of callbacks) callback(bodyA, bodyB, contact);
|
|
63
87
|
};
|
|
88
|
+
|
|
64
89
|
this.world.on("begin-contact", (contact) =>
|
|
65
|
-
fire(contact, "onCollisionEnter")
|
|
90
|
+
fire(contact, "onCollisionEnter", this._enterCallbacks)
|
|
91
|
+
);
|
|
92
|
+
this.world.on("end-contact", (contact) =>
|
|
93
|
+
fire(contact, "onCollisionExit", this._exitCallbacks)
|
|
66
94
|
);
|
|
67
|
-
this.world.on("end-contact", (contact) => fire(contact, "onCollisionExit"));
|
|
68
95
|
}
|
|
69
96
|
|
|
70
97
|
/**
|
|
@@ -80,8 +107,8 @@ class Physics {
|
|
|
80
107
|
const len = Math.hypot(direction.x, direction.y) || 1;
|
|
81
108
|
const dx = direction.x / len;
|
|
82
109
|
const dy = direction.y / len;
|
|
83
|
-
const p1 = new
|
|
84
|
-
const p2 = new
|
|
110
|
+
const p1 = new PhysicsVec2(origin.x / scale, origin.y / scale);
|
|
111
|
+
const p2 = new PhysicsVec2(
|
|
85
112
|
(origin.x + dx * maxDistance) / scale,
|
|
86
113
|
(origin.y + dy * maxDistance) / scale
|
|
87
114
|
);
|
|
@@ -108,29 +135,84 @@ class Physics {
|
|
|
108
135
|
* @returns {Array} - Owning objects at the point
|
|
109
136
|
*/
|
|
110
137
|
queryPoint(point) {
|
|
111
|
-
const p = new
|
|
138
|
+
const p = new PhysicsVec2(point.x / this.scale, point.y / this.scale);
|
|
112
139
|
const results = [];
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
140
|
+
const seen = new Set();
|
|
141
|
+
this.world.queryPoint(p, (fixture) => {
|
|
142
|
+
const body = fixture.getBody();
|
|
143
|
+
if (seen.has(body)) return true;
|
|
144
|
+
seen.add(body);
|
|
145
|
+
const rb = body.getUserData();
|
|
146
|
+
if (rb && rb.parentObject) results.push(rb.parentObject);
|
|
147
|
+
return true;
|
|
148
|
+
});
|
|
122
149
|
return results;
|
|
123
150
|
}
|
|
124
151
|
|
|
125
152
|
/**
|
|
126
153
|
* @method setFixedTimeStep
|
|
127
|
-
* @description
|
|
154
|
+
* @description Runs the simulation in constant-size slices, independent of
|
|
155
|
+
* the frame rate: `process()` banks the elapsed time and steps as many whole
|
|
156
|
+
* slices as fit. This is the default and the safe choice: the same inputs
|
|
157
|
+
* produce the same result on every machine, and a slow frame can't destabilise
|
|
158
|
+
* the solver.
|
|
159
|
+
*
|
|
160
|
+
* The cost is that motion updates at the step rate, not the display rate. On
|
|
161
|
+
* a 120Hz screen with a 1/60 step, every simulated position is shown for two
|
|
162
|
+
* frames, so anything that moves in the render frame (a smoothly lerped
|
|
163
|
+
* camera, for instance) will slide against sprites that only move every other
|
|
164
|
+
* frame. Either drive those from the same fixed step, or use
|
|
165
|
+
* {@link Physics#setVariableTimeStep}.
|
|
166
|
+
*
|
|
128
167
|
* @param {number} step - Fixed step in seconds (e.g. 1/60)
|
|
129
168
|
* @param {number} [maxSubSteps] - Max steps per process() call (spiral guard)
|
|
169
|
+
* @returns {Physics} - this
|
|
130
170
|
*/
|
|
131
171
|
setFixedTimeStep(step, maxSubSteps = this.maxSubSteps) {
|
|
132
172
|
if (step > 0) this.fixedTimeStep = step;
|
|
133
173
|
this.maxSubSteps = maxSubSteps;
|
|
174
|
+
this.timeStepMode = "fixed";
|
|
175
|
+
return this;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* @method setVariableTimeStep
|
|
180
|
+
* @description Advances the simulation once per `process()` call using the
|
|
181
|
+
* frame's own delta, so physics runs at exactly the rendering rate. Every
|
|
182
|
+
* rendered frame then shows a freshly simulated position, which is what
|
|
183
|
+
* removes the stepping you otherwise see when the display refreshes faster
|
|
184
|
+
* than the simulation.
|
|
185
|
+
*
|
|
186
|
+
* The trade-offs are real and worth knowing:
|
|
187
|
+
* - **Not deterministic.** Results depend on the frame timings the machine
|
|
188
|
+
* happened to produce, so replays and lockstep networking need fixed steps.
|
|
189
|
+
* - **Solver accuracy tracks the frame rate.** Contacts are resolved
|
|
190
|
+
* iteratively, so a long frame is a coarser solve; deep stacks and fast
|
|
191
|
+
* bodies are more forgiving under a fixed step.
|
|
192
|
+
*
|
|
193
|
+
* A frame longer than `maxStep` is split into several equal steps rather than
|
|
194
|
+
* simulated in one lump, up to `maxSubSteps`; beyond that the excess time is
|
|
195
|
+
* dropped instead of letting the world explode or spiral.
|
|
196
|
+
*
|
|
197
|
+
* @param {number} [maxStep=1/30] - Longest slice to simulate in one step
|
|
198
|
+
* @param {number} [maxSubSteps] - Max steps per process() call
|
|
199
|
+
* @returns {Physics} - this
|
|
200
|
+
*/
|
|
201
|
+
setVariableTimeStep(maxStep = 1 / 30, maxSubSteps = this.maxSubSteps) {
|
|
202
|
+
if (maxStep > 0) this.maxTimeStep = maxStep;
|
|
203
|
+
this.maxSubSteps = maxSubSteps;
|
|
204
|
+
this.timeStepMode = "variable";
|
|
205
|
+
this._accumulator = 0;
|
|
206
|
+
return this;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* @method getTimeStepMode
|
|
211
|
+
* @description Returns "fixed" or "variable".
|
|
212
|
+
* @returns {string}
|
|
213
|
+
*/
|
|
214
|
+
getTimeStepMode() {
|
|
215
|
+
return this.timeStepMode;
|
|
134
216
|
}
|
|
135
217
|
|
|
136
218
|
/**
|
|
@@ -158,7 +240,7 @@ class Physics {
|
|
|
158
240
|
) {
|
|
159
241
|
const bodyRef = this.world.createBody({
|
|
160
242
|
type: type,
|
|
161
|
-
position: new
|
|
243
|
+
position: new PhysicsVec2(
|
|
162
244
|
position.x / this.scale,
|
|
163
245
|
position.y / this.scale
|
|
164
246
|
),
|
|
@@ -166,7 +248,7 @@ class Physics {
|
|
|
166
248
|
});
|
|
167
249
|
|
|
168
250
|
if (attachFixture) {
|
|
169
|
-
bodyRef.createFixture(
|
|
251
|
+
bodyRef.createFixture(Box(fixtureSize.x, fixtureSize.y), {
|
|
170
252
|
density: density,
|
|
171
253
|
friction: friction,
|
|
172
254
|
restitution: restitution,
|
|
@@ -177,47 +259,158 @@ class Physics {
|
|
|
177
259
|
}
|
|
178
260
|
|
|
179
261
|
/**
|
|
180
|
-
* @method
|
|
181
|
-
* @description
|
|
182
|
-
*
|
|
262
|
+
* @method createDistanceJoint
|
|
263
|
+
* @description Connects two rigid bodies with a fixed-length rod between two
|
|
264
|
+
* world-space anchor points, or a damped spring toward that length when
|
|
265
|
+
* `frequencyHz` is set.
|
|
266
|
+
* @param {RigidBody} rigidBodyA
|
|
267
|
+
* @param {RigidBody} rigidBodyB
|
|
268
|
+
* @param {Object} [options] - `{ anchorA, anchorB, length, frequencyHz,
|
|
269
|
+
* dampingRatio, collideConnected }`. `anchorA`/`anchorB` are world-space
|
|
270
|
+
* pixel points; each defaults to its body's own center. `length` is in
|
|
271
|
+
* pixels; it defaults to the current distance between the anchors.
|
|
272
|
+
* @returns {DistanceJoint}
|
|
183
273
|
*/
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
274
|
+
createDistanceJoint(rigidBodyA, rigidBodyB, options = {}) {
|
|
275
|
+
const bodyA = rigidBodyA.body;
|
|
276
|
+
const bodyB = rigidBodyB.body;
|
|
277
|
+
const scale = this.scale;
|
|
278
|
+
const anchorA = options.anchorA
|
|
279
|
+
? new PhysicsVec2(options.anchorA.x / scale, options.anchorA.y / scale)
|
|
280
|
+
: bodyA.getWorldCenter();
|
|
281
|
+
const anchorB = options.anchorB
|
|
282
|
+
? new PhysicsVec2(options.anchorB.x / scale, options.anchorB.y / scale)
|
|
283
|
+
: bodyB.getWorldCenter();
|
|
284
|
+
const length =
|
|
285
|
+
options.length != null
|
|
286
|
+
? options.length / scale
|
|
287
|
+
: Math.hypot(anchorB.x - anchorA.x, anchorB.y - anchorA.y);
|
|
288
|
+
|
|
289
|
+
return this.world.createJoint({
|
|
290
|
+
type: "distance",
|
|
291
|
+
bodyA,
|
|
292
|
+
bodyB,
|
|
293
|
+
localAnchorA: bodyA.getLocalPoint(anchorA),
|
|
294
|
+
localAnchorB: bodyB.getLocalPoint(anchorB),
|
|
295
|
+
length,
|
|
296
|
+
frequencyHz: options.frequencyHz,
|
|
297
|
+
dampingRatio: options.dampingRatio,
|
|
298
|
+
collideConnected: options.collideConnected,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
188
301
|
|
|
189
|
-
|
|
190
|
-
|
|
302
|
+
/**
|
|
303
|
+
* @method createRevoluteJoint
|
|
304
|
+
* @description Pins two rigid bodies together at a shared world-space point,
|
|
305
|
+
* like a hinge: a door, a pendulum arm, a see-saw. Set `enableMotor` to
|
|
306
|
+
* drive it toward a target angular speed instead of swinging freely.
|
|
307
|
+
* @param {RigidBody} rigidBodyA
|
|
308
|
+
* @param {RigidBody} rigidBodyB
|
|
309
|
+
* @param {Object} anchor - World-space pixel point both bodies pin to
|
|
310
|
+
* @param {Object} [options] - `{ enableMotor, motorSpeed, maxMotorTorque,
|
|
311
|
+
* collideConnected }`
|
|
312
|
+
* @returns {RevoluteJoint}
|
|
313
|
+
*/
|
|
314
|
+
createRevoluteJoint(rigidBodyA, rigidBodyB, anchor, options = {}) {
|
|
315
|
+
const bodyA = rigidBodyA.body;
|
|
316
|
+
const bodyB = rigidBodyB.body;
|
|
317
|
+
const worldAnchor = new PhysicsVec2(
|
|
318
|
+
anchor.x / this.scale,
|
|
319
|
+
anchor.y / this.scale
|
|
320
|
+
);
|
|
191
321
|
|
|
192
|
-
|
|
322
|
+
return this.world.createJoint({
|
|
323
|
+
type: "revolute",
|
|
324
|
+
bodyA,
|
|
325
|
+
bodyB,
|
|
326
|
+
localAnchorA: bodyA.getLocalPoint(worldAnchor),
|
|
327
|
+
localAnchorB: bodyB.getLocalPoint(worldAnchor),
|
|
328
|
+
enableMotor: options.enableMotor,
|
|
329
|
+
motorSpeed: options.motorSpeed,
|
|
330
|
+
maxMotorTorque: options.maxMotorTorque,
|
|
331
|
+
collideConnected: options.collideConnected,
|
|
193
332
|
});
|
|
194
333
|
}
|
|
195
334
|
|
|
196
335
|
/**
|
|
197
|
-
* @method
|
|
198
|
-
* @description
|
|
199
|
-
* @
|
|
336
|
+
* @method destroyJoint
|
|
337
|
+
* @description Removes a joint created by {@link Physics#createDistanceJoint}
|
|
338
|
+
* or {@link Physics#createRevoluteJoint}.
|
|
339
|
+
* @param {Joint} joint
|
|
200
340
|
*/
|
|
201
|
-
|
|
202
|
-
this.world.
|
|
203
|
-
|
|
204
|
-
const fixtureB = contact.getFixtureB();
|
|
341
|
+
destroyJoint(joint) {
|
|
342
|
+
this.world.destroyJoint(joint);
|
|
343
|
+
}
|
|
205
344
|
|
|
206
|
-
|
|
207
|
-
|
|
345
|
+
/**
|
|
346
|
+
* @method onCollisionEnter
|
|
347
|
+
* @description Registers a world-level listener called whenever any two
|
|
348
|
+
* fixtures start touching. Survives {@link Physics#clear}.
|
|
349
|
+
* @param {Function} callback - Called with (bodyA, bodyB, contact)
|
|
350
|
+
*/
|
|
351
|
+
onCollisionEnter(callback) {
|
|
352
|
+
if (typeof callback === "function") this._enterCallbacks.push(callback);
|
|
353
|
+
}
|
|
208
354
|
|
|
209
|
-
|
|
210
|
-
|
|
355
|
+
/**
|
|
356
|
+
* @method onCollisionExit
|
|
357
|
+
* @description Registers a world-level listener called whenever any two
|
|
358
|
+
* fixtures stop touching. Survives {@link Physics#clear}.
|
|
359
|
+
* @param {Function} callback - Called with (bodyA, bodyB, contact)
|
|
360
|
+
*/
|
|
361
|
+
onCollisionExit(callback) {
|
|
362
|
+
if (typeof callback === "function") this._exitCallbacks.push(callback);
|
|
211
363
|
}
|
|
212
364
|
|
|
213
365
|
/**
|
|
214
366
|
* @method process
|
|
215
|
-
* @description
|
|
216
|
-
*
|
|
367
|
+
* @description Advances the simulation by `dt` seconds, in whichever way the
|
|
368
|
+
* current time-step mode calls for. Call it once per frame.
|
|
369
|
+
* @param {number} dt - Seconds elapsed since the previous call
|
|
370
|
+
* @returns {number} - How many steps were simulated
|
|
217
371
|
*/
|
|
218
372
|
process(dt) {
|
|
219
|
-
|
|
373
|
+
this.lastStepCount = 0;
|
|
374
|
+
if (!Number.isFinite(dt) || dt <= 0) return 0;
|
|
375
|
+
|
|
376
|
+
this.lastStepCount =
|
|
377
|
+
this.timeStepMode === "variable"
|
|
378
|
+
? this._stepVariable(dt)
|
|
379
|
+
: this._stepFixed(dt);
|
|
380
|
+
return this.lastStepCount;
|
|
381
|
+
}
|
|
220
382
|
|
|
383
|
+
/**
|
|
384
|
+
* @method _stepVariable
|
|
385
|
+
* @description Simulates the frame's own delta. Normally that is a single
|
|
386
|
+
* step of exactly `dt`, so physics advances in lock-step with rendering. Only
|
|
387
|
+
* an unusually long frame is divided, and only far enough to keep each slice
|
|
388
|
+
* within `maxTimeStep`.
|
|
389
|
+
* @param {number} dt
|
|
390
|
+
* @returns {number} - Steps taken
|
|
391
|
+
* @private
|
|
392
|
+
*/
|
|
393
|
+
_stepVariable(dt) {
|
|
394
|
+
const budget = this.maxTimeStep * this.maxSubSteps;
|
|
395
|
+
const total = Math.min(dt, budget);
|
|
396
|
+
const steps = Math.max(1, Math.ceil(total / this.maxTimeStep));
|
|
397
|
+
const step = total / steps;
|
|
398
|
+
|
|
399
|
+
for (let i = 0; i < steps; i++) {
|
|
400
|
+
this.world.step(step);
|
|
401
|
+
}
|
|
402
|
+
return steps;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* @method _stepFixed
|
|
407
|
+
* @description Banks elapsed time and simulates as many constant-size slices
|
|
408
|
+
* as have accumulated, leaving the remainder for next frame.
|
|
409
|
+
* @param {number} dt
|
|
410
|
+
* @returns {number} - Steps taken
|
|
411
|
+
* @private
|
|
412
|
+
*/
|
|
413
|
+
_stepFixed(dt) {
|
|
221
414
|
this._accumulator += dt;
|
|
222
415
|
let steps = 0;
|
|
223
416
|
while (
|
|
@@ -229,20 +422,37 @@ class Physics {
|
|
|
229
422
|
steps++;
|
|
230
423
|
}
|
|
231
424
|
|
|
232
|
-
if (steps === this.maxSubSteps)
|
|
233
|
-
|
|
234
|
-
|
|
425
|
+
if (steps === this.maxSubSteps) this._accumulator = 0;
|
|
426
|
+
return steps;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* @method getInterpolationAlpha
|
|
431
|
+
* @description How far the fixed-step simulation currently sits between its
|
|
432
|
+
* last completed step and the next one (0..1). Useful if you interpolate
|
|
433
|
+
* renderables between physics states. Always 0 in variable mode, where every
|
|
434
|
+
* frame already renders a freshly simulated position.
|
|
435
|
+
* @returns {number}
|
|
436
|
+
*/
|
|
437
|
+
getInterpolationAlpha() {
|
|
438
|
+
if (this.timeStepMode === "variable" || this.fixedTimeStep <= 0) return 0;
|
|
439
|
+
return Math.min(1, this._accumulator / this.fixedTimeStep);
|
|
235
440
|
}
|
|
236
441
|
|
|
237
442
|
/**
|
|
238
443
|
* @method clear
|
|
239
|
-
* @description
|
|
444
|
+
* @description Drops every body and starts a fresh world with the original
|
|
445
|
+
* gravity; use it when tearing a level down. Collision routing and any
|
|
446
|
+
* listeners added through onCollisionEnter/onCollisionExit are re-bound to
|
|
447
|
+
* the new world, so they keep working afterwards.
|
|
240
448
|
*/
|
|
241
449
|
clear() {
|
|
242
|
-
this.world = new
|
|
243
|
-
gravity: new
|
|
450
|
+
this.world = new World({
|
|
451
|
+
gravity: new PhysicsVec2(0, this.gravity),
|
|
452
|
+
velocityThreshold: this.world.velocityThreshold,
|
|
244
453
|
});
|
|
245
454
|
this._accumulator = 0;
|
|
455
|
+
this._dispatchContacts();
|
|
246
456
|
}
|
|
247
457
|
|
|
248
458
|
/**
|
package/src/Scene.js
CHANGED
|
@@ -51,7 +51,7 @@ class Scene {
|
|
|
51
51
|
* @method dispose
|
|
52
52
|
* @description Destroys every object in the scene (freeing their GPU
|
|
53
53
|
* resources and physics bodies) and empties it. Call when a level/screen is
|
|
54
|
-
* torn down for good
|
|
54
|
+
* torn down for good; removing objects without disposing leaks GL buffers
|
|
55
55
|
* over repeated scene swaps.
|
|
56
56
|
*/
|
|
57
57
|
dispose() {
|
package/src/Shaders.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
const STANDARD_VERTEX_SHADER = `
|
|
2
2
|
attribute vec4 aVertexPosition;
|
|
3
3
|
attribute vec2 aTextureCoord;
|
|
4
|
-
|
|
4
|
+
|
|
5
5
|
attribute vec4 aInstanceMatrix0;
|
|
6
6
|
attribute vec4 aInstanceMatrix1;
|
|
7
7
|
attribute vec4 aInstanceMatrix2;
|
|
8
8
|
attribute vec4 aInstanceMatrix3;
|
|
9
|
-
|
|
9
|
+
|
|
10
10
|
attribute vec2 aInstanceTexCoord0;
|
|
11
11
|
attribute vec2 aInstanceTexCoord1;
|
|
12
12
|
attribute vec2 aInstanceTexCoord2;
|
|
@@ -30,14 +30,14 @@ const STANDARD_VERTEX_SHADER = `
|
|
|
30
30
|
aInstanceMatrix2,
|
|
31
31
|
aInstanceMatrix3
|
|
32
32
|
);
|
|
33
|
-
|
|
33
|
+
|
|
34
34
|
if(useInstances) {
|
|
35
35
|
gl_Position = uProjectionMatrix * uInstancedModelViewMatrix * instanceMatrix * aVertexPosition;
|
|
36
36
|
vFragPos = (uInstancedModelViewMatrix * instanceMatrix * aVertexPosition).xy;
|
|
37
37
|
vInstanceColor = aInstanceColor;
|
|
38
38
|
|
|
39
39
|
int vertexIndex = int(aVertexPosition.x > 0.0 ? (aVertexPosition.y > 0.0 ? 0 : 2) : (aVertexPosition.y > 0.0 ? 1 : 3));
|
|
40
|
-
|
|
40
|
+
|
|
41
41
|
if(vertexIndex == 0) {
|
|
42
42
|
vTexCoord = aInstanceTexCoord0;
|
|
43
43
|
} else if(vertexIndex == 1) {
|
|
@@ -70,14 +70,14 @@ const STANDARD_FRAGMENT_SHADER = `
|
|
|
70
70
|
uniform float uLightIntensity[4];
|
|
71
71
|
uniform float uLightRadius[4];
|
|
72
72
|
uniform int uActiveLights;
|
|
73
|
-
|
|
73
|
+
|
|
74
74
|
uniform vec2 uDirLightPosition[4];
|
|
75
75
|
uniform vec2 uDirLightDirection[4];
|
|
76
76
|
uniform vec3 uDirLightColor[4];
|
|
77
77
|
uniform float uDirLightIntensity[4];
|
|
78
78
|
uniform float uDirLightWidth[4];
|
|
79
79
|
uniform int uActiveDirLights;
|
|
80
|
-
|
|
80
|
+
|
|
81
81
|
uniform vec3 uAmbientLightValues;
|
|
82
82
|
uniform bool uUseLighting;
|
|
83
83
|
|
|
@@ -98,44 +98,44 @@ const STANDARD_FRAGMENT_SHADER = `
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
texColor *= vInstanceColor;
|
|
101
|
-
|
|
101
|
+
|
|
102
102
|
vec3 lighting = ambientLight.rgb;
|
|
103
|
-
|
|
103
|
+
|
|
104
104
|
for(int i = 0; i < 4; i++) {
|
|
105
105
|
if(i >= uActiveLights) break;
|
|
106
|
-
|
|
106
|
+
|
|
107
107
|
float distance = length(uLightPosition[i] - vFragPos);
|
|
108
|
-
|
|
108
|
+
|
|
109
109
|
if(distance < uLightRadius[i]) {
|
|
110
110
|
float attenuation = 1.0 - distance / uLightRadius[i];
|
|
111
|
-
|
|
111
|
+
|
|
112
112
|
lighting += uLightColor[i] * attenuation * uLightIntensity[i];
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
|
-
|
|
115
|
+
|
|
116
116
|
for(int i = 0; i < 4; i++) {
|
|
117
117
|
if(i >= uActiveDirLights) break;
|
|
118
|
-
|
|
118
|
+
|
|
119
119
|
vec2 lightPos = uDirLightPosition[i];
|
|
120
120
|
vec2 lightDir = normalize(uDirLightDirection[i]);
|
|
121
121
|
vec2 toFrag = vFragPos - lightPos;
|
|
122
|
-
|
|
122
|
+
|
|
123
123
|
float projection = dot(toFrag, lightDir);
|
|
124
124
|
if(projection < 0.0) continue;
|
|
125
|
-
|
|
125
|
+
|
|
126
126
|
float perpDistance = abs(dot(toFrag, vec2(-lightDir.y, lightDir.x)));
|
|
127
|
-
|
|
127
|
+
|
|
128
128
|
if(perpDistance > uDirLightWidth[i] * 0.5) continue;
|
|
129
|
-
|
|
129
|
+
|
|
130
130
|
float widthFactor = 1.0 - (perpDistance / (uDirLightWidth[i] * 0.5));
|
|
131
|
-
|
|
131
|
+
|
|
132
132
|
float distance = length(toFrag);
|
|
133
133
|
float maxDistance = 1000.0;
|
|
134
134
|
float distanceFactor = max(0.0, 1.0 - (distance / maxDistance));
|
|
135
|
-
|
|
135
|
+
|
|
136
136
|
lighting += uDirLightColor[i] * uDirLightIntensity[i] * widthFactor * distanceFactor;
|
|
137
137
|
}
|
|
138
|
-
|
|
138
|
+
|
|
139
139
|
vec4 outColor;
|
|
140
140
|
if (uUseLighting) {
|
|
141
141
|
outColor = vec4(texColor.rgb * lighting, texColor.a);
|
package/src/Tilemap.js
CHANGED
|
@@ -132,7 +132,7 @@ class Tilemap {
|
|
|
132
132
|
* @method buildColliders
|
|
133
133
|
* @description Generates static physics colliders from the current map. Solid
|
|
134
134
|
* cells are merged greedily into horizontal runs, so a row of N tiles becomes
|
|
135
|
-
* one box collider instead of N
|
|
135
|
+
* one box collider instead of N, far fewer bodies for the physics engine.
|
|
136
136
|
* The bodies are tagged so collision callbacks resolve back to `ownerObject`
|
|
137
137
|
* (defaults to the tilemap's GameObject). Call again after setMap to rebuild.
|
|
138
138
|
*
|
package/src/UI.js
CHANGED
|
@@ -297,7 +297,7 @@ class UI {
|
|
|
297
297
|
|
|
298
298
|
/**
|
|
299
299
|
* @method isOver
|
|
300
|
-
* @description Whether an interactive element is under the given page point
|
|
300
|
+
* @description Whether an interactive element is under the given page point,
|
|
301
301
|
* useful to suppress game clicks behind the UI.
|
|
302
302
|
*/
|
|
303
303
|
isOver(clientX, clientY) {
|
|
@@ -74,7 +74,7 @@ class Behaviour {
|
|
|
74
74
|
* @description Called when the owner's body starts touching another. Requires
|
|
75
75
|
* the Physics world to be ticked. Override in subclasses.
|
|
76
76
|
* @param {Object} other - The other GameObject/Instance (or null)
|
|
77
|
-
* @param {Object} contact - The
|
|
77
|
+
* @param {Object} contact - The physics contact
|
|
78
78
|
*/
|
|
79
79
|
onCollisionEnter(other, contact) {}
|
|
80
80
|
|
|
@@ -82,7 +82,7 @@ class Behaviour {
|
|
|
82
82
|
* @method onCollisionExit
|
|
83
83
|
* @description Called when the owner's body stops touching another.
|
|
84
84
|
* @param {Object} other - The other GameObject/Instance (or null)
|
|
85
|
-
* @param {Object} contact - The
|
|
85
|
+
* @param {Object} contact - The physics contact
|
|
86
86
|
*/
|
|
87
87
|
onCollisionExit(other, contact) {}
|
|
88
88
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Box } from "../physics/index.js";
|
|
2
2
|
import BoxColliderDebug from "./BoxColliderDebug.js";
|
|
3
3
|
import SceneManager from "../managers/SceneManager.js";
|
|
4
4
|
import Collider from "./Collider.js";
|
|
@@ -26,14 +26,12 @@ class BoxCollider extends Collider {
|
|
|
26
26
|
filter = null
|
|
27
27
|
) {
|
|
28
28
|
super(rigidbody, isSensor, parentObject);
|
|
29
|
-
this.collider = rigidbody
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
isSensor: isSensor,
|
|
36
|
-
});
|
|
29
|
+
this.collider = rigidbody.createFixture(Box(fixtureSize.x, fixtureSize.y), {
|
|
30
|
+
density: density,
|
|
31
|
+
friction: friction,
|
|
32
|
+
restitution: restitution,
|
|
33
|
+
isSensor: isSensor,
|
|
34
|
+
});
|
|
37
35
|
this.fixtureSize = fixtureSize;
|
|
38
36
|
this.rigidbody.setCollider(this);
|
|
39
37
|
this.name = "BoxCollider" + this.id;
|
|
@@ -14,13 +14,12 @@ class BoxColliderDebug {
|
|
|
14
14
|
this.physics = this.rigidbody.physics;
|
|
15
15
|
this.scale = this.physics.getScale();
|
|
16
16
|
this.color = color || new Color(255, 0, 0, 255);
|
|
17
|
+
const body = this.rigidbody.getBody();
|
|
17
18
|
this.gameObject = new GameObject(
|
|
18
19
|
"BoxColliderDebug",
|
|
19
20
|
new Vector3(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
this.rigidbody.getBody().getPosition().y *
|
|
23
|
-
this.rigidbody.getPhysics().getScale(),
|
|
21
|
+
body.getPosition().x * this.scale,
|
|
22
|
+
body.getPosition().y * this.scale,
|
|
24
23
|
100
|
|
25
24
|
),
|
|
26
25
|
this.rigidbody.getAngle(),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Circle } from "../physics/index.js";
|
|
2
2
|
import SceneManager from "../managers/SceneManager.js";
|
|
3
3
|
import CircleColliderDebug from "./CircleColliderDebug.js";
|
|
4
4
|
import Collider from "./Collider.js";
|
|
@@ -26,14 +26,12 @@ class CircleCollider extends Collider {
|
|
|
26
26
|
filter = null
|
|
27
27
|
) {
|
|
28
28
|
super(rigidbody, isSensor, parentObject);
|
|
29
|
-
this.collider = rigidbody
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
isSensor: isSensor,
|
|
36
|
-
});
|
|
29
|
+
this.collider = rigidbody.createFixture(Circle(radius), {
|
|
30
|
+
density: density,
|
|
31
|
+
friction: friction,
|
|
32
|
+
restitution: restitution,
|
|
33
|
+
isSensor: isSensor,
|
|
34
|
+
});
|
|
37
35
|
this.radius = radius;
|
|
38
36
|
this.rigidbody.setCollider(this);
|
|
39
37
|
this.name = "CircleCollider" + this.id;
|