From f0127f4fe6bbccf98c4d3e3938d58f2909ae80e3 Mon Sep 17 00:00:00 2001 From: Ali Shakiba Date: Tue, 24 Dec 2024 17:41:58 +0330 Subject: [PATCH] Add world.queueUpdate --- .changeset/sour-news-float.md | 5 +++++ example/8-Ball.ts | 10 ++++----- example/Asteroid.ts | 16 ++++++++------ example/Breakable.ts | 8 +++---- example/Breakout.ts | 24 ++++++++++++++------- example/Shuffle.ts | 9 ++++---- example/Soccer.ts | 10 ++++----- src/dynamics/Body.ts | 16 +++++++++++--- src/dynamics/World.ts | 40 +++++++++++++++++++++++++++-------- 9 files changed, 92 insertions(+), 46 deletions(-) create mode 100644 .changeset/sour-news-float.md diff --git a/.changeset/sour-news-float.md b/.changeset/sour-news-float.md new file mode 100644 index 00000000..0e8bc986 --- /dev/null +++ b/.changeset/sour-news-float.md @@ -0,0 +1,5 @@ +--- +"planck": minor +--- + +Add world.queueUpdate() to queue and defer updates after current simulation step diff --git a/example/8-Ball.ts b/example/8-Ball.ts index cd5f8348..a7555a67 100644 --- a/example/8-Ball.ts +++ b/example/8-Ball.ts @@ -221,13 +221,13 @@ class BilliardPhysics { const ball = fA.getUserData() === BALL ? bA : fB.getUserData() === BALL ? bB : null; const pocket = fA.getUserData() === POCKET ? bA : fB.getUserData() === POCKET ? bB : null; - // do not change world immediately - setTimeout(() => { - if (ball && pocket) { + if (ball && pocket) { + // do not change world immediately + this.world.queueUpdate(() => { this.world.destroyBody(ball); this.client?.onBallInPocket(ball, pocket); - } - }, 1); + }); + } }; } diff --git a/example/Asteroid.ts b/example/Asteroid.ts index 688311cd..880d2b0e 100644 --- a/example/Asteroid.ts +++ b/example/Asteroid.ts @@ -230,15 +230,19 @@ class AsteroidPhysics { const asteroid = dataA?.type == "asteroid" ? bodyA : dataB?.type == "asteroid" ? bodyB : null; - setTimeout(() => { - if (ship && asteroid) { + if (ship && asteroid) { + // do not change world immediately + this.world.queueUpdate(() => { this.client?.collideShipAsteroid(ship, asteroid); - } + }); + } - if (bullet && asteroid) { + if (bullet && asteroid) { + // do not change world immediately + this.world.queueUpdate(() => { this.client?.collideBulletAsteroid(bullet, asteroid); - } - }, 1); + }); + } } deleteShip(): boolean { diff --git a/example/Breakable.ts b/example/Breakable.ts index fcb4db32..8f0767ce 100644 --- a/example/Breakable.ts +++ b/example/Breakable.ts @@ -53,14 +53,12 @@ world.on("post-solve", function (contact, impulse) { } if (maxImpulse > 40.0) { - setTimeout(function () { - Break(); - broke = true; - }); + broke = true; + world.queueUpdate(breakIt); } }); -function Break() { +function breakIt() { // Create two bodies from one. const center = body1.getWorldCenter(); diff --git a/example/Breakout.ts b/example/Breakout.ts index 7c8b987f..f3710401 100644 --- a/example/Breakout.ts +++ b/example/Breakout.ts @@ -198,19 +198,27 @@ class BreakoutPhysics { const drop = typeA === "drop" ? dataA : typeB === "drop" ? dataB : null; // do not change world immediately - setTimeout(() => { - if (ball && brick) { + if (ball && brick) { + this.world.queueUpdate(() => { this.client?.collideBallBrick(ball as BallData, brick as BrickData); - } else if (ball && bottom) { + }); + } else if (ball && bottom) { + this.world.queueUpdate(() => { this.client?.collideBallBottom(ball as BallData); - } else if (ball && paddle) { + }); + } else if (ball && paddle) { + this.world.queueUpdate(() => { this.client?.collideBallPaddle(ball as BallData); - } else if (drop && paddle) { + }); + } else if (drop && paddle) { + this.world.queueUpdate(() => { this.client?.collideDropPaddle(drop as DropData); - } else if (drop && bottom) { + }); + } else if (drop && bottom) { + this.world.queueUpdate(() => { this.client?.collideDropBottom(drop as DropData); - } - }, 1); + }); + } }; createBoardPhysics() { diff --git a/example/Shuffle.ts b/example/Shuffle.ts index 3d72c64e..08e1aa76 100644 --- a/example/Shuffle.ts +++ b/example/Shuffle.ts @@ -87,12 +87,11 @@ world.on("post-solve", function (contact) { ? bB : null; - // do not change world immediately - setTimeout(function () { - if (ball && wall) { + if (ball && wall) { + world.queueUpdate(() => { world.destroyBody(ball); - } - }, 1); + }); + } }); function row(n: number, m: number, r: number, l: number) { diff --git a/example/Soccer.ts b/example/Soccer.ts index bf3ca3a6..64ea8c5a 100644 --- a/example/Soccer.ts +++ b/example/Soccer.ts @@ -131,14 +131,14 @@ world.on("post-solve", function (contact) { ? bB : null; - // do not change world immediately - setTimeout(function () { - if (ball && goal) { + if (ball && goal) { + // do not change world immediately + world.queueUpdate(function () { ball.setPosition({ x: 0, y: 0 }); ball.setLinearVelocity({ x: 0, y: 0 }); // world.destroyBody(ball); - } - }, 1); + }); + } }); function team() { diff --git a/src/dynamics/Body.ts b/src/dynamics/Body.ts index 87e976d1..5c9a267c 100644 --- a/src/dynamics/Body.ts +++ b/src/dynamics/Body.ts @@ -394,6 +394,8 @@ export class Body { /** * Set the type of the body to "static", "kinematic" or "dynamic". * @param type The type of the body. + * + * Warning: This function is locked when a world simulation step is in progress. Use queueUpdate to schedule a function to be called after the step. */ setType(type: BodyType): void { if (_ASSERT) console.assert(type === STATIC || type === KINEMATIC || type === DYNAMIC); @@ -502,6 +504,8 @@ export class Body { * in collisions, ray-casts, or queries. Joints connected to an inactive body * are implicitly inactive. An inactive body is still owned by a World object * and remains + * + * Warning: This function is locked when a world simulation step is in progress. Use queueUpdate to schedule a function to be called after the step. */ setActive(flag: boolean): void { if (_ASSERT) console.assert(this.isWorldLocked() == false); @@ -568,6 +572,8 @@ export class Body { * Set the position of the body's origin and rotation. Manipulating a body's * transform may cause non-physical behavior. Note: contacts are updated on the * next call to World.step. + * + * Warning: This function is locked when a world simulation step is in progress. Use queueUpdate to schedule a function to be called after the step. * * @param position The world position of the body's local origin. * @param angle The world rotation in radians. @@ -577,6 +583,8 @@ export class Body { * Set the position of the body's origin and rotation. Manipulating a body's * transform may cause non-physical behavior. Note: contacts are updated on the * next call to World.step. + * + * Warning: This function is locked when a world simulation step is in progress. Use queueUpdate to schedule a function to be called after the step. */ setTransform(xf: Transform): void; setTransform(a: Vec2Value | Transform, b?: number): void { @@ -863,6 +871,8 @@ export class Body { * that this changes the center of mass position. Note that creating or * destroying fixtures can also alter the mass. This function has no effect if * the body isn't dynamic. + * + * Warning: This function is locked when a world simulation step is in progress. Use queueUpdate to schedule a function to be called after the step. * * @param massData The mass properties. */ @@ -1068,7 +1078,7 @@ export class Body { * * Contacts are not created until the next time step. * - * Warning: This function is locked during callbacks. + * Warning: This function is locked when a world simulation step is in progress. Use queueUpdate to schedule a function to be called after the step. */ createFixture(def: FixtureDef): Fixture; createFixture(shape: Shape, opt?: FixtureOpt): Fixture; @@ -1092,8 +1102,8 @@ export class Body { * mass of the body if the body is dynamic and the fixture has positive density. * All fixtures attached to a body are implicitly destroyed when the body is * destroyed. - * - * Warning: This function is locked during callbacks. + * + * Warning: This function is locked when a world simulation step is in progress. Use queueUpdate to schedule a function to be called after the step. * * @param fixture The fixture to be removed. */ diff --git a/src/dynamics/World.ts b/src/dynamics/World.ts index a1025a26..3956b4bb 100644 --- a/src/dynamics/World.ts +++ b/src/dynamics/World.ts @@ -128,6 +128,8 @@ export class World { /** @internal */ m_positionIterations: number; /** @internal */ m_t: number; + /** @internal */ m_step_callback: ((world: World) => unknown)[] = []; + // TODO /** @internal */ _listeners: { [key: string]: any[] @@ -469,10 +471,12 @@ export class World { * position -= newOrigin * * @param newOrigin The new origin with respect to the old origin + * + * Warning: This function is locked when a world simulation step is in progress. Use queueUpdate to schedule a function to be called after the step. */ shiftOrigin(newOrigin: Vec2Value): void { - if (_ASSERT) console.assert(this.m_locked == false); - if (this.m_locked) { + if (_ASSERT) console.assert(this.isLocked() == false); + if (this.isLocked()) { return; } @@ -510,7 +514,7 @@ export class World { * Create a rigid body given a definition. No reference to the definition is * retained. * - * Warning: This function is locked during callbacks. + * Warning: This function is locked when a world simulation step is in progress. Use queueUpdate to schedule a function to be called after the step. */ createBody(def?: BodyDef): Body; createBody(position: Vec2Value, angle?: number): Body; @@ -565,12 +569,11 @@ export class World { } /** - * Destroy a rigid body given a definition. No reference to the definition is - * retained. + * Destroy a body from the world. * * Warning: This automatically deletes all associated shapes and joints. * - * Warning: This function is locked during callbacks. + * Warning: This function is locked when a world simulation step is in progress. Use queueUpdate to schedule a function to be called after the step. */ destroyBody(b: Body): boolean { if (_ASSERT) console.assert(this.m_bodyCount > 0); @@ -647,7 +650,7 @@ export class World { * Create a joint to constrain bodies together. No reference to the definition * is retained. This may cause the connected bodies to cease colliding. * - * Warning: This function is locked during callbacks. + * Warning: This function is locked when a world simulation step is in progress. Use queueUpdate to schedule a function to be called after the step. */ createJoint(joint: T): T | null { if (_ASSERT) console.assert(!!joint.m_bodyA); @@ -700,8 +703,11 @@ export class World { } /** - * Destroy a joint. This may cause the connected bodies to begin colliding. - * Warning: This function is locked during callbacks. + * Destroy a joint. + * + * Warning: This may cause the connected bodies to begin colliding. + * + * Warning: This function is locked when a world simulation step is in progress. Use queueUpdate to schedule a function to be called after the step. */ destroyJoint(joint: Joint): void { if (_ASSERT) console.assert(this.isLocked() == false); @@ -854,9 +860,25 @@ export class World { this.m_locked = false; + let callback: (world: World) => unknown; + while(callback = this.m_step_callback.pop()) { + callback(this); + } + this.publish("post-step", timeStep); } + /** + * Queue a function to be called after ongoing simulation step. If no simulation is in progress call it immediately. + */ + queueUpdate(callback: (world: World) => unknown): void { + if (!this.isLocked()) { + callback(this); + } else { + this.m_step_callback.push(callback); + } + } + /** * @internal * Call this method to find new contacts.