From 8daf4b878c57d1c9a61398278a364e64d5f1655e Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Sun, 26 Oct 2025 18:44:27 +0100 Subject: [PATCH] fix: Create and destroy joints after world has been locked --- packages/forge2d/lib/src/dynamics/world.dart | 34 +++++++++----- packages/forge2d/test/dynamics/body_test.dart | 29 ++++++++++++ .../test/dynamics/joints/joint_test.dart | 47 +++++++++++++++++++ 3 files changed, 98 insertions(+), 12 deletions(-) diff --git a/packages/forge2d/lib/src/dynamics/world.dart b/packages/forge2d/lib/src/dynamics/world.dart index 935a6a9..06c062c 100644 --- a/packages/forge2d/lib/src/dynamics/world.dart +++ b/packages/forge2d/lib/src/dynamics/world.dart @@ -25,10 +25,10 @@ class World { late ContactManager contactManager; final List bodies = []; final List joints = []; - final List bodiesToCreate = []; - final List bodiesToDestroy = []; - final List jointsToCreate = []; - final List jointsToDestroy = []; + final List _bodiesToCreate = []; + final List _bodiesToDestroy = []; + final List _jointsToCreate = []; + final List _jointsToDestroy = []; final Vector2 _gravity; @@ -139,7 +139,7 @@ class World { Body createBody(BodyDef def) { final body = Body(def, this); if (isLocked) { - bodiesToCreate.add(body); + _bodiesToCreate.add(body); return body; } bodies.add(body); @@ -153,7 +153,7 @@ class World { /// Warning: This function is locked during callbacks. void destroyBody(Body body) { if (isLocked) { - bodiesToDestroy.add(body); + _bodiesToDestroy.add(body); return; } @@ -184,7 +184,7 @@ class World { /// Adding a joint doesn't wake up the bodies. void createJoint(Joint joint) { if (isLocked) { - jointsToCreate.add(joint); + _jointsToCreate.add(joint); return; } joints.add(joint); @@ -209,7 +209,7 @@ class World { /// Destroys a joint. This may cause the connected bodies to begin colliding. void destroyJoint(Joint joint) { if (isLocked) { - jointsToDestroy.add(joint); + _jointsToDestroy.add(joint); return; } @@ -308,15 +308,25 @@ class World { flags &= ~locked; - for (final body in bodiesToCreate) { + for (final body in _bodiesToCreate) { bodies.add(body); } - bodiesToCreate.clear(); + _bodiesToCreate.clear(); - for (final body in bodiesToDestroy) { + for (final body in _bodiesToDestroy) { destroyBody(body); } - bodiesToDestroy.clear(); + _bodiesToDestroy.clear(); + + for (final joint in _jointsToCreate) { + createJoint(joint); + } + _jointsToCreate.clear(); + + for (final joint in _jointsToDestroy) { + destroyJoint(joint); + } + _jointsToDestroy.clear(); _profile.step.record(_stepTimer.getMilliseconds()); } diff --git a/packages/forge2d/test/dynamics/body_test.dart b/packages/forge2d/test/dynamics/body_test.dart index 5805eea..cb5946b 100644 --- a/packages/forge2d/test/dynamics/body_test.dart +++ b/packages/forge2d/test/dynamics/body_test.dart @@ -83,5 +83,34 @@ void main() { expect(bodyInitialPosition.y, isNot(equals(body.position.y))); }); }); + + test('creation and destruction of body while world is locked', () { + final world = World(Vector2(0.0, -10.0)); + final bodyDef = BodyDef(); + + world.flags = World.locked; + + // Attempt to create a body while the world is locked + final body = world.createBody(bodyDef); + expect(world.bodies.contains(body), isFalse); + + world.flags = 0; + world.stepDt(1 / 60); + + // Verify the body is created after unlocking the world + expect(world.bodies.contains(body), isTrue); + + world.flags = World.locked; + + // Attempt to destroy the body while the world is locked + world.destroyBody(body); + expect(world.bodies.contains(body), isTrue); + + world.flags = 0; + world.stepDt(1 / 60); + + // Verify the body is destroyed after unlocking the world + expect(world.bodies.contains(body), isFalse); + }); }); } diff --git a/packages/forge2d/test/dynamics/joints/joint_test.dart b/packages/forge2d/test/dynamics/joints/joint_test.dart index 2bb7bf1..134f3cf 100644 --- a/packages/forge2d/test/dynamics/joints/joint_test.dart +++ b/packages/forge2d/test/dynamics/joints/joint_test.dart @@ -39,5 +39,52 @@ void main() { expect(body1.joints.length, 0); expect(body2.joints.length, 0); }); + + test('destruction of body with joint while world is locked', () { + final world = World(Vector2(0.0, -10.0)); + final bodyDef = BodyDef(); + final body1 = world.createBody(bodyDef); + final body2 = world.createBody(bodyDef..position = Vector2.all(2)); + final shape = CircleShape(radius: 1.2, position: Vector2.all(10)); + final fixtureDef = FixtureDef( + shape, + density: 50.0, + friction: 0.1, + restitution: 0.9, + ); + + body1.createFixture(fixtureDef); + body2.createFixture(fixtureDef); + + world.flags = World.locked; + + final revoluteJointDef = RevoluteJointDef() + ..initialize(body1, body2, body1.position); + final revoluteJoint = RevoluteJoint(revoluteJointDef); + world.createJoint(revoluteJoint); + + expect(body1.joints.length, 0); + expect(body2.joints.length, 0); + + world.flags = 0; + world.stepDt(1 / 60); + + expect(body1.joints.length, 1); + expect(body2.joints.length, 1); + + world.flags = World.locked; + + // Attempt to destroy the body while the world is locked + world.destroyBody(body1); + expect(body1.joints.length, 1); + expect(body2.joints.length, 1); + + // Step the world again to process the destruction + world.flags = 0; + world.stepDt(1 / 60); + + expect(body1.joints.length, 0); + expect(body2.joints.length, 0); + }); }); }