diff --git a/doc/bridge_packages/flame_forge2d/joints.md b/doc/bridge_packages/flame_forge2d/joints.md index 3bef5a1e6..dd5b798e8 100644 --- a/doc/bridge_packages/flame_forge2d/joints.md +++ b/doc/bridge_packages/flame_forge2d/joints.md @@ -24,7 +24,7 @@ Currently, Forge2D supports the following joints: - [`MotorJoint`](#motorjoint) - [`MouseJoint`](#mousejoint) - PrismaticJoint -- PulleyJoint +- [`PulleyJoint`](#pulleyjoint) - RevoluteJoint - RopeJoint - WeldJoint @@ -272,3 +272,69 @@ final mouseJointDef = MouseJointDef() - `target`: The initial world target point. This is assumed to coincide with the body anchor initially. + + +### `PulleyJoint` + +A `PulleyJoint` is used to create an idealized pulley. The pulley connects two bodies to the ground +and to each other. As one body goes up, the other goes down. The total length of the pulley rope is +conserved according to the initial configuration: + +```text +length1 + length2 == constant +``` + +You can supply a ratio that simulates a block and tackle. This causes one side of the pulley to +extend faster than the other. At the same time the constraint force is smaller on one side than the +other. You can use this to create a mechanical leverage. + +```text +length1 + ratio * length2 == constant +``` + +For example, if the ratio is 2, then `length1` will vary at twice the rate of `length2`. Also the +force in the rope attached to the first body will have half the constraint force as the rope +attached to the second body. + +```dart +final pulleyJointDef = PulleyJointDef() + ..initialize( + firstBody, + secondBody, + firstPulley.worldCenter, + secondPulley.worldCenter, + firstBody.worldCenter, + secondBody.worldCenter, + 1, + ); + +world.createJoint(PulleyJoint(pulleyJointDef)); +``` + +```{flutter-app} +:sources: ../../examples +:page: pulley_joint +:subfolder: stories/bridge_libraries/forge2d/joints +:show: code popup +``` + +The `initialize` method of `PulleyJointDef` requires two ground anchors, two dynamic bodies and +their anchor points, and a pulley ratio. + +- `b1`, `b2`: Two dynamic bodies connected with the joint +- `ga1`, `ga2`: Two ground anchors +- `anchor1`, `anchor2`: Anchors on the dynamic bodies the joint will be attached to +- `r`: Pulley ratio to simulate a block and tackle + +`PulleyJoint` also provides the current lengths: + +```dart +joint.getCurrentLengthA() +joint.getCurrentLengthB() +``` + +```{warning} +`PulleyJoint` can get a bit troublesome by itself. They often work better when +combined with prismatic joints. You should also cover the the anchor points +with static shapes to prevent one side from going to zero length. +``` diff --git a/examples/lib/main.dart b/examples/lib/main.dart index a599a75fd..1a302c6fc 100644 --- a/examples/lib/main.dart +++ b/examples/lib/main.dart @@ -11,6 +11,7 @@ import 'package:examples/stories/bridge_libraries/forge2d/joints/distance_joint. import 'package:examples/stories/bridge_libraries/forge2d/joints/friction_joint.dart'; import 'package:examples/stories/bridge_libraries/forge2d/joints/motor_joint.dart'; import 'package:examples/stories/bridge_libraries/forge2d/joints/mouse_joint.dart'; +import 'package:examples/stories/bridge_libraries/forge2d/joints/pulley_joint.dart'; import 'package:examples/stories/camera_and_viewport/camera_and_viewport.dart'; import 'package:examples/stories/collision_detection/collision_detection.dart'; import 'package:examples/stories/components/components.dart'; @@ -39,6 +40,7 @@ void main() { 'friction_joint': FrictionJointExample.new, 'motor_joint': MotorJointExample.new, 'mouse_joint': MouseJointExample.new, + 'pulley_joint': PulleyJointExample.new, }; final game = routes[page]?.call(); if (game != null) { diff --git a/examples/lib/stories/bridge_libraries/forge2d/flame_forge2d.dart b/examples/lib/stories/bridge_libraries/forge2d/flame_forge2d.dart index b44196b3a..ffdfa05f6 100644 --- a/examples/lib/stories/bridge_libraries/forge2d/flame_forge2d.dart +++ b/examples/lib/stories/bridge_libraries/forge2d/flame_forge2d.dart @@ -13,6 +13,7 @@ import 'package:examples/stories/bridge_libraries/forge2d/joints/distance_joint. import 'package:examples/stories/bridge_libraries/forge2d/joints/friction_joint.dart'; import 'package:examples/stories/bridge_libraries/forge2d/joints/motor_joint.dart'; import 'package:examples/stories/bridge_libraries/forge2d/joints/mouse_joint.dart'; +import 'package:examples/stories/bridge_libraries/forge2d/joints/pulley_joint.dart'; import 'package:examples/stories/bridge_libraries/forge2d/raycast_example.dart'; import 'package:examples/stories/bridge_libraries/forge2d/revolute_joint_example.dart'; import 'package:examples/stories/bridge_libraries/forge2d/sprite_body_example.dart'; @@ -137,5 +138,11 @@ void addJointsStories(Dashbook dashbook) { (DashbookContext ctx) => GameWidget(game: MouseJointExample()), codeLink: link('mouse_joint.dart'), info: MouseJointExample.description, + ) + .add( + 'PulleyJoint', + (DashbookContext ctx) => GameWidget(game: PulleyJointExample()), + codeLink: link('pulley_joint.dart'), + info: PulleyJointExample.description, ); } diff --git a/examples/lib/stories/bridge_libraries/forge2d/joints/motor_joint.dart b/examples/lib/stories/bridge_libraries/forge2d/joints/motor_joint.dart index 6025c83e7..491590871 100644 --- a/examples/lib/stories/bridge_libraries/forge2d/joints/motor_joint.dart +++ b/examples/lib/stories/bridge_libraries/forge2d/joints/motor_joint.dart @@ -24,7 +24,12 @@ class MotorJointExample extends Forge2DGame with TapDetector, HasDraggables { Future onLoad() async { super.onLoad(); - final box = Box(size / 2, 2, 1); + final box = Box( + startPosition: size / 2, + width: 2, + height: 1, + bodyType: BodyType.static, + ); add(box); ball = Ball(Vector2(size.x / 2, size.y / 2 - 5)); diff --git a/examples/lib/stories/bridge_libraries/forge2d/joints/pulley_joint.dart b/examples/lib/stories/bridge_libraries/forge2d/joints/pulley_joint.dart new file mode 100644 index 000000000..c8fa2886a --- /dev/null +++ b/examples/lib/stories/bridge_libraries/forge2d/joints/pulley_joint.dart @@ -0,0 +1,79 @@ +import 'dart:ui'; + +import 'package:examples/stories/bridge_libraries/forge2d/utils/balls.dart'; +import 'package:examples/stories/bridge_libraries/forge2d/utils/boxes.dart'; +import 'package:flame/events.dart'; +import 'package:flame/input.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; + +class PulleyJointExample extends Forge2DGame with TapDetector, HasDraggables { + static const description = ''' + This example shows how to use a `PulleyJoint`. Drag one of the boxes and see + how the other one gets moved by the pulley + '''; + + late final Ball firstPulley; + late final Ball secondPulley; + late final PulleyJoint joint; + + @override + Future onLoad() async { + super.onLoad(); + + firstPulley = Ball(Vector2(size.x * 0.33, 10), bodyType: BodyType.static); + secondPulley = Ball(Vector2(size.x * 0.66, 10), bodyType: BodyType.static); + + final firstBox = DraggableBox( + startPosition: Vector2(size.x * 0.33, size.y / 2), + width: 5, + height: 10, + ); + final secondBox = DraggableBox( + startPosition: Vector2(size.x * 0.66, size.y / 2), + width: 7, + height: 10, + ); + addAll([firstBox, secondBox, firstPulley, secondPulley]); + + await Future.wait([ + firstBox.loaded, + secondBox.loaded, + firstPulley.loaded, + secondPulley.loaded + ]); + + createJoint(firstBox, secondBox); + } + + void createJoint(Box first, Box second) { + final pulleyJointDef = PulleyJointDef() + ..initialize( + first.body, + second.body, + firstPulley.center, + secondPulley.center, + first.body.worldPoint(Vector2(0, -first.height / 2)), + second.body.worldPoint(Vector2(0, -second.height / 2)), + 1, + ); + joint = PulleyJoint(pulleyJointDef); + world.createJoint(joint); + } + + @override + void render(Canvas canvas) { + super.render(canvas); + + final firstBodyAnchor = worldToScreen(joint.anchorA).toOffset(); + final firstPulleyAnchor = + worldToScreen(joint.getGroundAnchorA()).toOffset(); + canvas.drawLine(firstBodyAnchor, firstPulleyAnchor, debugPaint); + + final secondBodyAnchor = worldToScreen(joint.anchorB).toOffset(); + final secondPulleyAnchor = + worldToScreen(joint.getGroundAnchorB()).toOffset(); + canvas.drawLine(secondBodyAnchor, secondPulleyAnchor, debugPaint); + + canvas.drawLine(firstPulleyAnchor, secondPulleyAnchor, debugPaint); + } +} diff --git a/examples/lib/stories/bridge_libraries/forge2d/utils/balls.dart b/examples/lib/stories/bridge_libraries/forge2d/utils/balls.dart index 2d5d0cf26..15c77a0d9 100644 --- a/examples/lib/stories/bridge_libraries/forge2d/utils/balls.dart +++ b/examples/lib/stories/bridge_libraries/forge2d/utils/balls.dart @@ -7,13 +7,14 @@ class Ball extends BodyComponent with ContactCallbacks { late Paint originalPaint; bool giveNudge = false; final double radius; + final BodyType bodyType; final Vector2 _position; double _timeSinceNudge = 0.0; static const double _minNudgeRest = 2.0; final Paint _blue = BasicPalette.blue.paint(); - Ball(this._position, {this.radius = 2}) { + Ball(this._position, {this.radius = 2, this.bodyType = BodyType.dynamic}) { originalPaint = randomPaint(); paint = originalPaint; } @@ -36,7 +37,7 @@ class Ball extends BodyComponent with ContactCallbacks { userData: this, angularDamping: 0.8, position: _position, - type: BodyType.dynamic, + type: bodyType, ); return world.createBody(bodyDef)..createFixture(fixtureDef); diff --git a/examples/lib/stories/bridge_libraries/forge2d/utils/boxes.dart b/examples/lib/stories/bridge_libraries/forge2d/utils/boxes.dart index f88d637ff..77dad4dc6 100644 --- a/examples/lib/stories/bridge_libraries/forge2d/utils/boxes.dart +++ b/examples/lib/stories/bridge_libraries/forge2d/utils/boxes.dart @@ -1,22 +1,72 @@ +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; class Box extends BodyComponent { - final Vector2 _position; - final double _width; - final double _height; + final Vector2 startPosition; + final double width; + final double height; + final BodyType bodyType; - Box(this._position, this._width, this._height); + Box({ + required this.startPosition, + required this.width, + required this.height, + this.bodyType = BodyType.dynamic, + }); @override Body createBody() { final shape = PolygonShape() - ..setAsBox(_width / 2, _height / 2, Vector2.zero(), 0); - final fixtureDef = FixtureDef(shape, friction: 0.3); + ..setAsBox(width / 2, height / 2, Vector2.zero(), 0); + final fixtureDef = FixtureDef(shape, friction: 0.3, density: 10); final bodyDef = BodyDef( userData: this, // To be able to determine object in collision - position: _position, + position: startPosition, + type: bodyType, ); return world.createBody(bodyDef)..createFixture(fixtureDef); } } + +class DraggableBox extends Box with Draggable { + MouseJoint? mouseJoint; + late final groundBody = world.createBody(BodyDef()); + + DraggableBox({ + required super.startPosition, + required super.width, + required super.height, + }); + + @override + bool onDragUpdate(DragUpdateInfo info) { + final mouseJointDef = MouseJointDef() + ..maxForce = 3000 * body.mass * 10 + ..dampingRatio = 0 + ..frequencyHz = 20 + ..target.setFrom(info.eventPosition.game) + ..collideConnected = false + ..bodyA = groundBody + ..bodyB = body; + + if (mouseJoint == null) { + mouseJoint = MouseJoint(mouseJointDef); + world.createJoint(mouseJoint!); + } + + mouseJoint?.setTarget(info.eventPosition.game); + return false; + } + + @override + bool onDragEnd(DragEndInfo info) { + if (mouseJoint == null) { + return true; + } + world.destroyJoint(mouseJoint!); + mouseJoint = null; + return false; + } +}