diff --git a/doc/bridge_packages/flame_forge2d/joints.md b/doc/bridge_packages/flame_forge2d/joints.md index 4bdc34aaf..47a502433 100644 --- a/doc/bridge_packages/flame_forge2d/joints.md +++ b/doc/bridge_packages/flame_forge2d/joints.md @@ -21,7 +21,7 @@ Currently, Forge2D supports the following joints: - [`DistanceJoint`](#distancejoint) - [`FrictionJoint`](#frictionjoint) - GearJoint -- MotorJoint +- [`MotorJoint`](#motorjoint) - MouseJoint - PrismaticJoint - PulleyJoint @@ -160,3 +160,57 @@ values: In other words, the former simulates the friction, when the body is sliding and the latter simulates the friction when the body is spinning. + + +### `MotorJoint` + +A `MotorJoint` is used to control the relative motion between two bodies. A typical usage is to +control the movement of a dynamic body with respect to the fixed point, for example to create +animations. + +A `MotorJoint` lets you control the motion of a body by specifying target position and rotation +offsets. You can set the maximum motor force and torque that will be applied to reach the target +position and rotation. If the body is blocked, it will stop and the contact forces will be +proportional the maximum motor force and torque. + +```dart +final motorJointDef = MotorJointDef() + ..initialize(first, second) + ..maxTorque = 1000 + ..maxForce = 1000 + ..correctionFactor = 0.1; + + world.createJoint(MotorJoint(motorJointDef)); +``` + +A `MotorJointDef` has three optional parameters: + +- `maxForce`: the maximum translational force which will be applied to the joined body to reach the +target position. + +- `maxTorque`: the maximum angular force which will be applied to the joined body to reach the +target rotation. + +- `correctionFactor`: position correction factor in range [0, 1]. It adjusts the joint's response to +deviation from target position. A higher value makes the joint respond faster, while a lower value +makes it respond slower. If the value is set too high, the joint may overcompensate and oscillate, +becoming unstable. If set too low, it may respond too slowly. + +The linear and angular offsets are the target distance and angle that the bodies should achieve +relative to each other's position and rotation. By default, the linear target will be the distance +between the two body centers and the angular target will be the relative rotation of the bodies. +Use the `setLinearOffset(Vector2)` and `setLinearOffset(double)` methods of the `MotorJoint` to set +the desired relative translation and rotate between the bodies. + +For example, this code increments the angular offset of the joint every update cycle, causing the +body to rotate. + +```dart +@override +void update(double dt) { + super.update(dt); + + final angularOffset = joint.getAngularOffset() + motorSpeed * dt; + joint.setAngularOffset(angularOffset); +} +``` diff --git a/examples/lib/main.dart b/examples/lib/main.dart index b930bdbad..7ebea9388 100644 --- a/examples/lib/main.dart +++ b/examples/lib/main.dart @@ -8,6 +8,7 @@ import 'package:examples/stories/bridge_libraries/forge2d/flame_forge2d.dart'; import 'package:examples/stories/bridge_libraries/forge2d/joints/constant_volume_joint.dart'; import 'package:examples/stories/bridge_libraries/forge2d/joints/distance_joint.dart'; 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/camera_and_viewport/camera_and_viewport.dart'; import 'package:examples/stories/collision_detection/collision_detection.dart'; import 'package:examples/stories/components/components.dart'; @@ -37,6 +38,7 @@ void main() { 'constant_volume_joint': ConstantVolumeJointExample.new, 'distance_joint': DistanceJointExample.new, 'friction_joint': FrictionJointExample.new, + 'motor_joint': MotorJointExample.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 cfbadea63..16d0c618e 100644 --- a/examples/lib/stories/bridge_libraries/forge2d/flame_forge2d.dart +++ b/examples/lib/stories/bridge_libraries/forge2d/flame_forge2d.dart @@ -11,6 +11,7 @@ import 'package:examples/stories/bridge_libraries/forge2d/joint_example.dart'; import 'package:examples/stories/bridge_libraries/forge2d/joints/constant_volume_joint.dart'; import 'package:examples/stories/bridge_libraries/forge2d/joints/distance_joint.dart'; 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/mouse_joint_example.dart'; import 'package:examples/stories/bridge_libraries/forge2d/raycast_example.dart'; import 'package:examples/stories/bridge_libraries/forge2d/revolute_joint_example.dart'; @@ -117,18 +118,24 @@ void addJointsStories(Dashbook dashbook) { 'ConstantVolumeJoint', (DashbookContext ctx) => GameWidget(game: ConstantVolumeJointExample()), codeLink: link('constant_volume_joint.dart'), - info: BlobExample.description, + info: ConstantVolumeJointExample.description, ) .add( 'DistanceJoint', (DashbookContext ctx) => GameWidget(game: DistanceJointExample()), codeLink: link('distance_joint.dart'), - info: BlobExample.description, + info: DistanceJointExample.description, ) .add( 'FrictionJoint', (DashbookContext ctx) => GameWidget(game: FrictionJointExample()), codeLink: link('friction_joint.dart'), - info: BlobExample.description, + info: FrictionJointExample.description, + ) + .add( + 'MotorJoint', + (DashbookContext ctx) => GameWidget(game: MotorJointExample()), + codeLink: link('motor_joint.dart'), + info: MotorJointExample.description, ); } diff --git a/examples/lib/stories/bridge_libraries/forge2d/joints/constant_volume_joint.dart b/examples/lib/stories/bridge_libraries/forge2d/joints/constant_volume_joint.dart index ebc6e0496..6d2c955ba 100644 --- a/examples/lib/stories/bridge_libraries/forge2d/joints/constant_volume_joint.dart +++ b/examples/lib/stories/bridge_libraries/forge2d/joints/constant_volume_joint.dart @@ -6,6 +6,11 @@ import 'package:flame/input.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; class ConstantVolumeJointExample extends Forge2DGame with TapDetector { + static const description = ''' + This example shows how to use a `ConstantVolumeJoint`. Tap the screen to add + a bunch off balls, that maintain a constant volume within them. + '''; + @override Future onLoad() async { super.onLoad(); diff --git a/examples/lib/stories/bridge_libraries/forge2d/joints/distance_joint.dart b/examples/lib/stories/bridge_libraries/forge2d/joints/distance_joint.dart index 39e82faf6..795b4c248 100644 --- a/examples/lib/stories/bridge_libraries/forge2d/joints/distance_joint.dart +++ b/examples/lib/stories/bridge_libraries/forge2d/joints/distance_joint.dart @@ -4,6 +4,11 @@ import 'package:flame/input.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; class DistanceJointExample extends Forge2DGame with TapDetector { + static const description = ''' + This example shows how to use a `DistanceJoint`. Tap the screen to add a + pair of balls joined with a `DistanceJoint`. + '''; + @override Future onLoad() async { super.onLoad(); diff --git a/examples/lib/stories/bridge_libraries/forge2d/joints/friction_joint.dart b/examples/lib/stories/bridge_libraries/forge2d/joints/friction_joint.dart index 41b6a0204..443c6ac8c 100644 --- a/examples/lib/stories/bridge_libraries/forge2d/joints/friction_joint.dart +++ b/examples/lib/stories/bridge_libraries/forge2d/joints/friction_joint.dart @@ -4,6 +4,10 @@ import 'package:flame/input.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; class FrictionJointExample extends Forge2DGame with TapDetector { + static const description = ''' + This example shows how to use a `FrictionJoint`. Tap the screen to move the + ball around and observe it slows down due to the friction force. + '''; FrictionJointExample() : super(gravity: Vector2.all(0)); late Wall border; diff --git a/examples/lib/stories/bridge_libraries/forge2d/joints/motor_joint.dart b/examples/lib/stories/bridge_libraries/forge2d/joints/motor_joint.dart new file mode 100644 index 000000000..6025c83e7 --- /dev/null +++ b/examples/lib/stories/bridge_libraries/forge2d/joints/motor_joint.dart @@ -0,0 +1,82 @@ +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 MotorJointExample extends Forge2DGame with TapDetector, HasDraggables { + static const description = ''' + This example shows how to use a `MotorJoint`. The ball spins around the + center point. Tap the screen to change the direction. + '''; + + late Ball ball; + late MotorJoint joint; + final motorSpeed = 1; + + bool clockWise = true; + + MotorJointExample() : super(gravity: Vector2.zero()); + + @override + Future onLoad() async { + super.onLoad(); + + final box = Box(size / 2, 2, 1); + add(box); + + ball = Ball(Vector2(size.x / 2, size.y / 2 - 5)); + add(ball); + + await Future.wait([ball.loaded, box.loaded]); + + joint = createJoint(ball.body, box.body); + } + + @override + Future onTapDown(TapDownInfo details) async { + super.onTapDown(details); + clockWise = !clockWise; + } + + MotorJoint createJoint(Body first, Body second) { + final motorJointDef = MotorJointDef() + ..initialize(first, second) + ..maxForce = 1000 + ..maxTorque = 1000 + ..correctionFactor = 0.1; + + final joint = MotorJoint(motorJointDef); + world.createJoint(joint); + return joint; + } + + @override + void update(double dt) { + super.update(dt); + + var deltaOffset = motorSpeed * dt; + if (clockWise) { + deltaOffset = -deltaOffset; + } + + final linearOffsetX = joint.getLinearOffset().x + deltaOffset; + final linearOffsetY = joint.getLinearOffset().y + deltaOffset; + final linearOffset = Vector2(linearOffsetX, linearOffsetY); + final angularOffset = joint.getAngularOffset() + deltaOffset; + + joint.setLinearOffset(linearOffset); + joint.setAngularOffset(angularOffset); + } + + @override + void render(Canvas canvas) { + super.render(canvas); + + final p1 = worldToScreen(joint.anchorA); + final p2 = worldToScreen(joint.anchorB); + canvas.drawLine(p1.toOffset(), p2.toOffset(), debugPaint); + } +} diff --git a/examples/lib/stories/bridge_libraries/forge2d/utils/boxes.dart b/examples/lib/stories/bridge_libraries/forge2d/utils/boxes.dart new file mode 100644 index 000000000..f88d637ff --- /dev/null +++ b/examples/lib/stories/bridge_libraries/forge2d/utils/boxes.dart @@ -0,0 +1,22 @@ +import 'package:flame_forge2d/flame_forge2d.dart'; + +class Box extends BodyComponent { + final Vector2 _position; + final double _width; + final double _height; + + Box(this._position, this._width, this._height); + + @override + Body createBody() { + final shape = PolygonShape() + ..setAsBox(_width / 2, _height / 2, Vector2.zero(), 0); + final fixtureDef = FixtureDef(shape, friction: 0.3); + final bodyDef = BodyDef( + userData: this, // To be able to determine object in collision + position: _position, + ); + + return world.createBody(bodyDef)..createFixture(fixtureDef); + } +}