Joints

Joints are used to connect two different bodies together in various ways. They help to simulate interactions between objects to create hinges, wheels, ropes, chains etc.

One Body in a joint may be of type BodyType.static. Joints between BodyType.static and/or BodyType.kinematic are allowed, but have no effect and use some processing time.

To construct a Joint, you need to create a corresponding subclass of JointDefand initialize it with its parameters.

To register a Joint use world.createJointand later use world.destroyJoint when you want to remove it.

Built-in joints

Currently, Forge2D supports the following joints:

ConstantVolumeJoint

This type of joint connects a group of bodies together and maintains a constant volume within them. Essentially, it is a set of DistanceJoints, that connects all bodies one after another.

It can for example be useful when simulating “soft-bodies”.

  final constantVolumeJoint = ConstantVolumeJointDef()
    ..frequencyHz = 10
    ..dampingRatio = 0.8;

  bodies.forEach((body) {
    constantVolumeJoint.addBody(body);
  });
    
  world.createJoint(ConstantVolumeJoint(world, constantVolumeJoint));
constant_volume_joint.dart
 1import 'dart:math';
 2
 3import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart';
 4import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart';
 5import 'package:flame/components.dart';
 6import 'package:flame/events.dart';
 7import 'package:flame_forge2d/flame_forge2d.dart';
 8
 9class ConstantVolumeJointExample extends Forge2DGame {
10  static const description = '''
11    This example shows how to use a `ConstantVolumeJoint`. Tap the screen to add
12    a bunch off balls, that maintain a constant volume within them.
13  ''';
14
15  ConstantVolumeJointExample() : super(world: SpriteBodyWorld());
16}
17
18class SpriteBodyWorld extends Forge2DWorld
19    with TapCallbacks, HasGameReference<Forge2DGame> {
20  @override
21  Future<void> onLoad() async {
22    super.onLoad();
23    addAll(createBoundaries(game));
24  }
25
26  @override
27  Future<void> onTapDown(TapDownEvent info) async {
28    super.onTapDown(info);
29    final center = info.localPosition;
30
31    const numPieces = 20;
32    const radius = 5.0;
33    final balls = <Ball>[];
34
35    for (var i = 0; i < numPieces; i++) {
36      final x = radius * cos(2 * pi * (i / numPieces));
37      final y = radius * sin(2 * pi * (i / numPieces));
38
39      final ball = Ball(Vector2(x + center.x, y + center.y), radius: 0.5);
40
41      add(ball);
42      balls.add(ball);
43    }
44
45    await Future.wait(balls.map((e) => e.loaded));
46
47    final constantVolumeJoint = ConstantVolumeJointDef()
48      ..frequencyHz = 10
49      ..dampingRatio = 0.8;
50
51    balls.forEach((ball) {
52      constantVolumeJoint.addBody(ball.body);
53    });
54
55    createJoint(
56      ConstantVolumeJoint(
57        physicsWorld,
58        constantVolumeJoint,
59      ),
60    );
61  }
62}

ConstantVolumeJointDef requires at least 3 bodies to be added using the addBody method. It also has two optional parameters:

  • frequencyHz: This parameter sets the frequency of oscillation of the joint. If it is not set to 0, the higher the value, the less springy each of the compound DistantJoints are.

  • dampingRatio: This parameter defines how quickly the oscillation comes to rest. It ranges from 0 to 1, where 0 means no damping and 1 indicates critical damping.

DistanceJoint

A DistanceJoint constrains two points on two bodies to remain at a fixed distance from each other.

You can view this as a massless, rigid rod.

final distanceJointDef = DistanceJointDef()
  ..initialize(firstBody, secondBody, firstBody.worldCenter, secondBody.worldCenter)
  ..length = 10
  ..frequencyHz = 3
  ..dampingRatio = 0.2;

world.createJoint(DistanceJoint(distanceJointDef));
distance_joint.dart
 1import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart';
 2import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart';
 3import 'package:flame/components.dart';
 4import 'package:flame/events.dart';
 5import 'package:flame_forge2d/flame_forge2d.dart';
 6
 7class DistanceJointExample extends Forge2DGame {
 8  static const description = '''
 9    This example shows how to use a `DistanceJoint`. Tap the screen to add a
10    pair of balls joined with a `DistanceJoint`.
11  ''';
12
13  DistanceJointExample() : super(world: DistanceJointWorld());
14}
15
16class DistanceJointWorld extends Forge2DWorld
17    with TapCallbacks, HasGameReference<Forge2DGame> {
18  @override
19  Future<void> onLoad() async {
20    super.onLoad();
21    addAll(createBoundaries(game));
22  }
23
24  @override
25  Future<void> onTapDown(TapDownEvent info) async {
26    super.onTapDown(info);
27    final tap = info.localPosition;
28
29    final first = Ball(tap);
30    final second = Ball(Vector2(tap.x + 3, tap.y + 3));
31    addAll([first, second]);
32
33    await Future.wait([first.loaded, second.loaded]);
34
35    final distanceJointDef = DistanceJointDef()
36      ..initialize(
37        first.body,
38        second.body,
39        first.body.worldCenter,
40        second.center,
41      )
42      ..length = 10
43      ..frequencyHz = 3
44      ..dampingRatio = 0.2;
45
46    createJoint(DistanceJoint(distanceJointDef));
47  }
48}

To create a DistanceJointDef, you can use the initialize method, which requires two bodies and a world anchor point on each body. The definition uses local anchor points, allowing for a slight violation of the constraint in the initial configuration. This is useful when saving and loading a game.

The DistanceJointDef has three optional parameters that you can set:

  • length: This parameter determines the distance between the two anchor points and must be greater than 0. The default value is 1.

  • frequencyHz: This parameter sets the frequency of oscillation of the joint. If it is not set to 0, the higher the value, the less springy the joint becomes.

  • dampingRatio: This parameter defines how quickly the oscillation comes to rest. It ranges from 0 to 1, where 0 means no damping and 1 indicates critical damping.

Warning

Do not use a zero or short length.

FrictionJoint

A FrictionJoint is used for simulating friction in a top-down game. It provides 2D translational friction and angular friction.

The FrictionJoint isn’t related to the friction that occurs when two shapes collide in the x-y plane of the screen. Instead, it’s designed to simulate friction along the z-axis, which is perpendicular to the screen. The most common use-case for it is applying the friction force between a moving body and the game floor.

The initialize method of the FrictionJointDef method requires two bodies that will have friction force applied to them, and an anchor.

The third parameter is the anchor point in the world coordinates where the friction force will be applied. In most cases, it would be the center of the first object. However, for more complex physics interactions between bodies, you can set the anchor point to a specific location on one or both of the bodies.

final frictionJointDef = FrictionJointDef()
  ..initialize(ballBody, floorBody, ballBody.worldCenter)
  ..maxForce = 50
  ..maxTorque = 50;

  world.createJoint(FrictionJoint(frictionJointDef));
friction_joint.dart
 1import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart';
 2import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart';
 3import 'package:flame/components.dart';
 4import 'package:flame/events.dart';
 5import 'package:flame_forge2d/flame_forge2d.dart';
 6
 7class FrictionJointExample extends Forge2DGame {
 8  static const description = '''
 9    This example shows how to use a `FrictionJoint`. Tap the screen to move the
10    ball around and observe it slows down due to the friction force.
11  ''';
12
13  FrictionJointExample()
14      : super(gravity: Vector2.all(0), world: FrictionJointWorld());
15}
16
17class FrictionJointWorld extends Forge2DWorld
18    with TapCallbacks, HasGameReference<Forge2DGame> {
19  late Wall border;
20  late Ball ball;
21
22  @override
23  Future<void> onLoad() async {
24    super.onLoad();
25    final boundaries = createBoundaries(game);
26    border = boundaries.first;
27    addAll(boundaries);
28
29    ball = Ball(Vector2.zero(), radius: 3);
30    add(ball);
31
32    await Future.wait([ball.loaded, border.loaded]);
33
34    createFrictionJoint(ball.body, border.body);
35  }
36
37  @override
38  Future<void> onTapDown(TapDownEvent info) async {
39    super.onTapDown(info);
40    ball.body.applyLinearImpulse(Vector2.random() * 5000);
41  }
42
43  void createFrictionJoint(Body first, Body second) {
44    final frictionJointDef = FrictionJointDef()
45      ..initialize(first, second, first.worldCenter)
46      ..collideConnected = true
47      ..maxForce = 500
48      ..maxTorque = 500;
49
50    createJoint(FrictionJoint(frictionJointDef));
51  }
52}

When creating a FrictionJoint, simulated friction can be applied via maximum force and torque values:

  • maxForce: the maximum translational friction which applied to the joined body. A higher value

  • simulates higher friction.

  • maxTorque: the maximum angular friction which may be applied to the joined body. A higher value

  • simulates higher friction.

In other words, the former simulates the friction, when the body is sliding and the latter simulates the friction when the body is spinning.

GearJoint

The GearJoint is used to connect two joints together. Joints are required to be a RevoluteJoint or a PrismaticJoint in any combination.

Warning

The connected joints must attach a dynamic body to a static body. The static body is expected to be a bodyA on those joints

final gearJointDef = GearJointDef()
  ..bodyA = firstJoint.bodyA
  ..bodyB = secondJoint.bodyA
  ..joint1 = firstJoint
  ..joint2 = secondJoint
  ..ratio = 1;

world.createJoint(GearJoint(gearJointDef));
gear_joint.dart
  1import 'dart:ui';
  2
  3import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart';
  4import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boxes.dart';
  5import 'package:flame/components.dart';
  6import 'package:flame_forge2d/flame_forge2d.dart';
  7
  8class GearJointExample extends Forge2DGame {
  9  static const description = '''
 10    This example shows how to use a `GearJoint`.
 11
 12    Drag the box along the specified axis and observe gears respond to the
 13    translation.
 14  ''';
 15
 16  GearJointExample() : super(world: GearJointWorld());
 17}
 18
 19class GearJointWorld extends Forge2DWorld with HasGameReference<Forge2DGame> {
 20  late PrismaticJoint prismaticJoint;
 21  Vector2 boxAnchor = Vector2.zero();
 22
 23  double boxWidth = 2;
 24  double ball1Radius = 4;
 25  double ball2Radius = 2;
 26
 27  @override
 28  Future<void> onLoad() async {
 29    super.onLoad();
 30
 31    final box =
 32        DraggableBox(startPosition: boxAnchor, width: boxWidth, height: 20);
 33    add(box);
 34
 35    final ball1Anchor = boxAnchor - Vector2(boxWidth / 2 + ball1Radius, 0);
 36    final ball1 = Ball(ball1Anchor, radius: ball1Radius);
 37    add(ball1);
 38
 39    final ball2Anchor = ball1Anchor - Vector2(ball1Radius + ball2Radius, 0);
 40    final ball2 = Ball(ball2Anchor, radius: ball2Radius);
 41    add(ball2);
 42
 43    await Future.wait([box.loaded, ball1.loaded, ball2.loaded]);
 44
 45    prismaticJoint = createPrismaticJoint(box.body, boxAnchor);
 46    final revoluteJoint1 = createRevoluteJoint(ball1.body, ball1Anchor);
 47    final revoluteJoint2 = createRevoluteJoint(ball2.body, ball2Anchor);
 48
 49    createGearJoint(prismaticJoint, revoluteJoint1, 1);
 50    createGearJoint(revoluteJoint1, revoluteJoint2, 0.5);
 51    add(JointRenderer(joint: prismaticJoint, anchor: boxAnchor));
 52  }
 53
 54  PrismaticJoint createPrismaticJoint(Body box, Vector2 anchor) {
 55    final groundBody = createBody(BodyDef());
 56
 57    final prismaticJointDef = PrismaticJointDef()
 58      ..initialize(
 59        groundBody,
 60        box,
 61        anchor,
 62        Vector2(0, 1),
 63      )
 64      ..enableLimit = true
 65      ..lowerTranslation = -10
 66      ..upperTranslation = 10;
 67
 68    final joint = PrismaticJoint(prismaticJointDef);
 69    createJoint(joint);
 70    return joint;
 71  }
 72
 73  RevoluteJoint createRevoluteJoint(Body ball, Vector2 anchor) {
 74    final groundBody = createBody(BodyDef());
 75
 76    final revoluteJointDef = RevoluteJointDef()
 77      ..initialize(
 78        groundBody,
 79        ball,
 80        anchor,
 81      );
 82
 83    final joint = RevoluteJoint(revoluteJointDef);
 84    createJoint(joint);
 85    return joint;
 86  }
 87
 88  void createGearJoint(Joint first, Joint second, double gearRatio) {
 89    final gearJointDef = GearJointDef()
 90      ..bodyA = first.bodyA
 91      ..bodyB = second.bodyA
 92      ..joint1 = first
 93      ..joint2 = second
 94      ..ratio = gearRatio;
 95
 96    final joint = GearJoint(gearJointDef);
 97    createJoint(joint);
 98  }
 99}
100
101class JointRenderer extends Component {
102  JointRenderer({required this.joint, required this.anchor});
103
104  final PrismaticJoint joint;
105  final Vector2 anchor;
106  final Vector2 p1 = Vector2.zero();
107  final Vector2 p2 = Vector2.zero();
108
109  @override
110  void render(Canvas canvas) {
111    p1
112      ..setFrom(joint.getLocalAxisA())
113      ..scale(joint.getLowerLimit())
114      ..add(anchor);
115    p2
116      ..setFrom(joint.getLocalAxisA())
117      ..scale(joint.getUpperLimit())
118      ..add(anchor);
119
120    canvas.drawLine(p1.toOffset(), p2.toOffset(), debugPaint);
121  }
122}
  • joint1, joint2: Connected revolute or prismatic joints

  • bodyA, bodyB: Any bodies form the connected joints, as long as they are not the same body.

  • ratio: Gear ratio

Similarly to PulleyJoint, you can specify a gear ratio to bind the motions together:

coordinate1 + ratio * coordinate2 == constant 

The ratio can be negative or positive. If one joint is a RevoluteJoint and the other joint is a PrismaticJoint, then the ratio will have units of length or units of 1/length.

Since the GearJoint depends on two other joints, if these are destroyed, the GearJoint needs to be destroyed as well.

Warning

Manually destroy the GearJoint if joint1 or joint2 is destroyed

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.

final motorJointDef = MotorJointDef()
  ..initialize(first, second)
  ..maxTorque = 1000
  ..maxForce = 1000
  ..correctionFactor = 0.1;

  world.createJoint(MotorJoint(motorJointDef));
motor_joint.dart
 1import 'dart:ui';
 2
 3import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart';
 4import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boxes.dart';
 5import 'package:flame/components.dart';
 6import 'package:flame/events.dart';
 7import 'package:flame_forge2d/flame_forge2d.dart';
 8
 9class MotorJointExample extends Forge2DGame {
10  static const description = '''
11    This example shows how to use a `MotorJoint`. The ball spins around the
12    center point. Tap the screen to change the direction.
13  ''';
14
15  MotorJointExample()
16      : super(gravity: Vector2.zero(), world: MotorJointWorld());
17}
18
19class MotorJointWorld extends Forge2DWorld with TapCallbacks {
20  late Ball ball;
21  late MotorJoint joint;
22  final motorSpeed = 1;
23
24  bool clockWise = true;
25
26  @override
27  Future<void> onLoad() async {
28    super.onLoad();
29
30    final box = Box(
31      startPosition: Vector2.zero(),
32      width: 2,
33      height: 1,
34      bodyType: BodyType.static,
35    );
36    add(box);
37
38    ball = Ball(Vector2(0, -5));
39    add(ball);
40
41    await Future.wait([ball.loaded, box.loaded]);
42
43    joint = createMotorJoint(ball.body, box.body);
44    add(JointRenderer(joint: joint));
45  }
46
47  @override
48  void onTapDown(TapDownEvent info) {
49    super.onTapDown(info);
50    clockWise = !clockWise;
51  }
52
53  MotorJoint createMotorJoint(Body first, Body second) {
54    final motorJointDef = MotorJointDef()
55      ..initialize(first, second)
56      ..maxForce = 1000
57      ..maxTorque = 1000
58      ..correctionFactor = 0.1;
59
60    final joint = MotorJoint(motorJointDef);
61    createJoint(joint);
62    return joint;
63  }
64
65  final linearOffset = Vector2.zero();
66
67  @override
68  void update(double dt) {
69    super.update(dt);
70
71    var deltaOffset = motorSpeed * dt;
72    if (clockWise) {
73      deltaOffset = -deltaOffset;
74    }
75
76    final linearOffsetX = joint.getLinearOffset().x + deltaOffset;
77    final linearOffsetY = joint.getLinearOffset().y + deltaOffset;
78    linearOffset.setValues(linearOffsetX, linearOffsetY);
79    final angularOffset = joint.getAngularOffset() + deltaOffset;
80
81    joint.setLinearOffset(linearOffset);
82    joint.setAngularOffset(angularOffset);
83  }
84}
85
86class JointRenderer extends Component {
87  JointRenderer({required this.joint});
88
89  final MotorJoint joint;
90
91  @override
92  void render(Canvas canvas) {
93    canvas.drawLine(
94      joint.anchorA.toOffset(),
95      joint.anchorB.toOffset(),
96      debugPaint,
97    );
98  }
99}

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.

@override
void update(double dt) {
  super.update(dt);
  
  final angularOffset = joint.getAngularOffset() + motorSpeed * dt;
  joint.setAngularOffset(angularOffset);
}

MouseJoint

The MouseJoint is used to manipulate bodies with the mouse. It attempts to drive a point on a body towards the current position of the cursor. There is no restriction on rotation.

The MouseJoint definition has a target point, maximum force, frequency, and damping ratio. The target point initially coincides with the body’s anchor point. The maximum force is used to prevent violent reactions when multiple dynamic bodies interact. You can make this as large as you like. The frequency and damping ratio are used to create a spring/damper effect similar to the distance joint.

Warning

Many users have tried to adapt the mouse joint for game play. Users often want to achieve precise positioning and instantaneous response. The mouse joint doesn’t work very well in that context. You may wish to consider using kinematic bodies instead.

final mouseJointDef = MouseJointDef()
  ..maxForce = 3000 * ballBody.mass * 10
  ..dampingRatio = 1
  ..frequencyHz = 5
  ..target.setFrom(ballBody.position)
  ..collideConnected = false
  ..bodyA = groundBody
  ..bodyB = ballBody;

  mouseJoint = MouseJoint(mouseJointDef);
  world.createJoint(mouseJoint);
}
mouse_joint.dart
 1import 'package:examples/stories/bridge_libraries/flame_forge2d/revolute_joint_with_motor_example.dart';
 2import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart';
 3import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart';
 4import 'package:flame/components.dart';
 5import 'package:flame/events.dart';
 6import 'package:flame_forge2d/flame_forge2d.dart';
 7
 8class MouseJointExample extends Forge2DGame {
 9  static const description = '''
10    In this example we use a `MouseJoint` to make the ball follow the mouse
11    when you drag it around.
12  ''';
13
14  MouseJointExample()
15      : super(gravity: Vector2(0, 10.0), world: MouseJointWorld());
16}
17
18class MouseJointWorld extends Forge2DWorld
19    with DragCallbacks, HasGameReference<Forge2DGame> {
20  late Ball ball;
21  late Body groundBody;
22  MouseJoint? mouseJoint;
23
24  @override
25  Future<void> onLoad() async {
26    super.onLoad();
27    final boundaries = createBoundaries(game);
28    addAll(boundaries);
29
30    final center = Vector2.zero();
31    groundBody = createBody(BodyDef());
32    ball = Ball(center, radius: 5);
33    add(ball);
34    add(CornerRamp(center));
35    add(CornerRamp(center, isMirrored: true));
36  }
37
38  @override
39  void onDragStart(DragStartEvent info) {
40    super.onDragStart(info);
41    final mouseJointDef = MouseJointDef()
42      ..maxForce = 3000 * ball.body.mass * 10
43      ..dampingRatio = 0.1
44      ..frequencyHz = 5
45      ..target.setFrom(ball.body.position)
46      ..collideConnected = false
47      ..bodyA = groundBody
48      ..bodyB = ball.body;
49
50    if (mouseJoint == null) {
51      mouseJoint = MouseJoint(mouseJointDef);
52      createJoint(mouseJoint!);
53    }
54  }
55
56  @override
57  void onDragUpdate(DragUpdateEvent info) {
58    mouseJoint?.setTarget(info.localEndPosition);
59  }
60
61  @override
62  void onDragEnd(DragEndEvent info) {
63    super.onDragEnd(info);
64    destroyJoint(mouseJoint!);
65    mouseJoint = null;
66  }
67}
  • maxForce: This parameter defines the maximum constraint force that can be exerted to move the candidate body. Usually you will express as some multiple of the weight (multiplier mass gravity).

  • dampingRatio: This parameter defines how quickly the oscillation comes to rest. It ranges from 0 to 1, where 0 means no damping and 1 indicates critical damping.

  • frequencyHz: This parameter defines the response speed of the body, i.e. how quickly it tries to reach the target position

  • target: The initial world target point. This is assumed to coincide with the body anchor initially.

PrismaticJoint

The PrismaticJoint provides a single degree of freedom, allowing for a relative translation of two bodies along an axis fixed in bodyA. Relative rotation is prevented.

PrismaticJointDef requires defining a line of motion using an axis and an anchor point. The definition uses local anchor points and a local axis so that the initial configuration can violate the constraint slightly.

The joint translation is zero when the local anchor points coincide in world space. Using local anchors and a local axis helps when saving and loading a game.

Warning

At least one body should by dynamic with a non-fixed rotation.

The PrismaticJoint definition is similar to the RevoluteJoint definition, but instead of rotation, it uses translation.

final prismaticJointDef = PrismaticJointDef()
  ..initialize(
    dynamicBody,
    groundBody,
    dynamicBody.worldCenter,
    Vector2(1, 0),
  )
prismatic_joint.dart
 1import 'dart:ui';
 2
 3import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boxes.dart';
 4import 'package:flame/components.dart';
 5import 'package:flame_forge2d/flame_forge2d.dart';
 6
 7class PrismaticJointExample extends Forge2DGame {
 8  static const description = '''
 9    This example shows how to use a `PrismaticJoint`.
10
11    Drag the box along the specified axis, bound between lower and upper limits.
12    Also, there's a motor enabled that's pulling the box to the lower limit.
13  ''';
14
15  final Vector2 anchor = Vector2.zero();
16
17  @override
18  Future<void> onLoad() async {
19    super.onLoad();
20
21    final box = DraggableBox(startPosition: anchor, width: 6, height: 6);
22    world.add(box);
23    await Future.wait([box.loaded]);
24
25    final joint = createJoint(box.body, anchor);
26    world.add(JointRenderer(joint: joint, anchor: anchor));
27  }
28
29  PrismaticJoint createJoint(Body box, Vector2 anchor) {
30    final groundBody = world.createBody(BodyDef());
31
32    final prismaticJointDef = PrismaticJointDef()
33      ..initialize(
34        box,
35        groundBody,
36        anchor,
37        Vector2(1, 0),
38      )
39      ..enableLimit = true
40      ..lowerTranslation = -20
41      ..upperTranslation = 20
42      ..enableMotor = true
43      ..motorSpeed = 1
44      ..maxMotorForce = 100;
45
46    final joint = PrismaticJoint(prismaticJointDef);
47    world.createJoint(joint);
48    return joint;
49  }
50}
51
52class JointRenderer extends Component {
53  JointRenderer({required this.joint, required this.anchor});
54
55  final PrismaticJoint joint;
56  final Vector2 anchor;
57  final Vector2 p1 = Vector2.zero();
58  final Vector2 p2 = Vector2.zero();
59
60  @override
61  void render(Canvas canvas) {
62    p1
63      ..setFrom(joint.getLocalAxisA())
64      ..scale(joint.getLowerLimit())
65      ..add(anchor);
66    p2
67      ..setFrom(joint.getLocalAxisA())
68      ..scale(joint.getUpperLimit())
69      ..add(anchor);
70
71    canvas.drawLine(p1.toOffset(), p2.toOffset(), debugPaint);
72  }
73}
  • b1, b2: Bodies connected by the joint.

  • anchor: World anchor point, to put the axis through. Usually the center of the first body.

  • axis: World translation axis, along which the translation will be fixed.

In some cases you might wish to control the range of motion. For this, the PrismaticJointDef has optional parameters that allow you to simulate a joint limit and/or a motor.

Prismatic Joint Limit

You can limit the relative rotation with a joint limit that specifies a lower and upper translation.

jointDef
  ..enableLimit = true
  ..lowerTranslation = -20
  ..upperTranslation = 20;
  • enableLimit: Set to true to enable translation limits

  • lowerTranslation: The lower translation limit in meters

  • upperTranslation: The upper translation limit in meters

You change the limits after the joint was created with this method:

prismaticJoint.setLimits(-10, 10);

Prismatic Joint Motor

You can use a motor to drive the motion or to model joint friction. A maximum motor force is provided so that infinite forces are not generated.

jointDef
  ..enableMotor = true
  ..motorSpeed = 1
  ..maxMotorForce = 100;
  • enableMotor: Set to true to enable the motor

  • motorSpeed: The desired motor speed in radians per second

  • maxMotorForce: The maximum motor torque used to achieve the desired motor speed in N-m.

You change the motor’s speed and force after the joint was created using these methods:

prismaticJoint.setMotorSpeed(2);
prismaticJoint.setMaxMotorForce(200);

Also, you can get the joint angle and speed using the following methods:

prismaticJoint.getJointTranslation();
prismaticJoint.getJointSpeed();

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:

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.

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.

final pulleyJointDef = PulleyJointDef()
  ..initialize(
    firstBody,
    secondBody,
    firstPulley.worldCenter,
    secondPulley.worldCenter,
    firstBody.worldCenter,     
    secondBody.worldCenter,
    1,
  );

world.createJoint(PulleyJoint(pulleyJointDef));
pulley_joint.dart
 1import 'dart:ui';
 2
 3import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart';
 4import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boxes.dart';
 5import 'package:flame/components.dart';
 6import 'package:flame_forge2d/flame_forge2d.dart';
 7
 8class PulleyJointExample extends Forge2DGame {
 9  static const description = '''
10    This example shows how to use a `PulleyJoint`. Drag one of the boxes and see
11    how the other one gets moved by the pulley
12  ''';
13
14  @override
15  Future<void> onLoad() async {
16    super.onLoad();
17    final distanceFromCenter = camera.visibleWorldRect.width / 5;
18
19    final firstPulley = Ball(
20      Vector2(-distanceFromCenter, -10),
21      bodyType: BodyType.static,
22    );
23    final secondPulley = Ball(
24      Vector2(distanceFromCenter, -10),
25      bodyType: BodyType.static,
26    );
27
28    final firstBox = DraggableBox(
29      startPosition: Vector2(-distanceFromCenter, 20),
30      width: 5,
31      height: 10,
32    );
33    final secondBox = DraggableBox(
34      startPosition: Vector2(distanceFromCenter, 20),
35      width: 7,
36      height: 10,
37    );
38    world.addAll([firstBox, secondBox, firstPulley, secondPulley]);
39
40    await Future.wait([
41      firstBox.loaded,
42      secondBox.loaded,
43      firstPulley.loaded,
44      secondPulley.loaded,
45    ]);
46
47    final joint = createJoint(firstBox, secondBox, firstPulley, secondPulley);
48    world.add(PulleyRenderer(joint: joint));
49  }
50
51  PulleyJoint createJoint(
52    Box firstBox,
53    Box secondBox,
54    Ball firstPulley,
55    Ball secondPulley,
56  ) {
57    final pulleyJointDef = PulleyJointDef()
58      ..initialize(
59        firstBox.body,
60        secondBox.body,
61        firstPulley.center,
62        secondPulley.center,
63        firstBox.body.worldPoint(Vector2(0, -firstBox.height / 2)),
64        secondBox.body.worldPoint(Vector2(0, -secondBox.height / 2)),
65        1,
66      );
67    final joint = PulleyJoint(pulleyJointDef);
68    world.createJoint(joint);
69    return joint;
70  }
71}
72
73class PulleyRenderer extends Component {
74  PulleyRenderer({required this.joint});
75
76  final PulleyJoint joint;
77
78  @override
79  void render(Canvas canvas) {
80    canvas.drawLine(
81      joint.anchorA.toOffset(),
82      joint.getGroundAnchorA().toOffset(),
83      debugPaint,
84    );
85
86    canvas.drawLine(
87      joint.anchorB.toOffset(),
88      joint.getGroundAnchorB().toOffset(),
89      debugPaint,
90    );
91
92    canvas.drawLine(
93      joint.getGroundAnchorA().toOffset(),
94      joint.getGroundAnchorB().toOffset(),
95      debugPaint,
96    );
97  }
98}

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:

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.

RevoluteJoint

A RevoluteJoint forces two bodies to share a common anchor point, often called a hinge point. The revolute joint has a single degree of freedom: the relative rotation of the two bodies.

To create a RevoluteJoint, provide two bodies and a common point to the initialize method. The definition uses local anchor points so that the initial configuration can violate the constraint slightly.

final jointDef = RevoluteJointDef()
  ..initialize(firstBody, secondBody, firstBody.position);
world.createJoint(RevoluteJoint(jointDef));
revolute_joint.dart
 1import 'dart:math';
 2
 3import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart';
 4import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart';
 5import 'package:flame/components.dart';
 6import 'package:flame/events.dart';
 7import 'package:flame_forge2d/flame_forge2d.dart';
 8
 9class RevoluteJointExample extends Forge2DGame {
10  static const description = '''
11    In this example we use a joint to keep a body with several fixtures stuck
12    to another body.
13
14    Tap the screen to add more of these combined bodies.
15  ''';
16
17  RevoluteJointExample()
18      : super(gravity: Vector2(0, 10.0), world: RevoluteJointWorld());
19}
20
21class RevoluteJointWorld extends Forge2DWorld
22    with TapCallbacks, HasGameReference<Forge2DGame> {
23  @override
24  Future<void> onLoad() async {
25    super.onLoad();
26    addAll(createBoundaries(game));
27  }
28
29  @override
30  void onTapDown(TapDownEvent info) {
31    super.onTapDown(info);
32    final ball = Ball(info.localPosition);
33    add(ball);
34    add(CircleShuffler(ball));
35  }
36}
37
38class CircleShuffler extends BodyComponent {
39  final Ball ball;
40
41  CircleShuffler(this.ball);
42
43  @override
44  Body createBody() {
45    final bodyDef = BodyDef(
46      type: BodyType.dynamic,
47      position: ball.body.position.clone(),
48    );
49    const numPieces = 5;
50    const radius = 6.0;
51    final body = world.createBody(bodyDef);
52
53    for (var i = 0; i < numPieces; i++) {
54      final xPos = radius * cos(2 * pi * (i / numPieces));
55      final yPos = radius * sin(2 * pi * (i / numPieces));
56
57      final shape = CircleShape()
58        ..radius = 1.2
59        ..position.setValues(xPos, yPos);
60
61      final fixtureDef = FixtureDef(
62        shape,
63        density: 50.0,
64        friction: 0.1,
65        restitution: 0.9,
66      );
67
68      body.createFixture(fixtureDef);
69    }
70
71    final jointDef = RevoluteJointDef()
72      ..initialize(body, ball.body, body.position);
73    world.createJoint(RevoluteJoint(jointDef));
74
75    return body;
76  }
77}

In some cases you might wish to control the joint angle. For this, the RevoluteJointDef has optional parameters that allow you to simulate a joint limit and/or a motor.

Revolute Joint Limit

You can limit the relative rotation with a joint limit that specifies a lower and upper angle.

jointDef
  ..enableLimit = true
  ..lowerAngle = 0
  ..upperAngle = pi / 2;
  • enableLimit: Set to true to enable angle limits

  • lowerAngle: The lower angle in radians

  • upperAngle: The upper angle in radians

You change the limits after the joint was created with this method:

revoluteJoint.setLimits(0, pi);

Revolute Joint Motor

You can use a motor to drive the relative rotation about the shared point. A maximum motor torque is provided so that infinite forces are not generated.

jointDef
  ..enableMotor = true
  ..motorSpeed = 5
  ..maxMotorTorque = 100;
  • enableMotor: Set to true to enable the motor

  • motorSpeed: The desired motor speed in radians per second

  • maxMotorTorque: The maximum motor torque used to achieve the desired motor speed in N-m.

You change the motor’s speed and torque after the joint was created using these methods:

revoluteJoint.setMotorSpeed(2);
revoluteJoint.setMaxMotorTorque(200);

Also, you can get the joint angle and speed using the following methods:

revoluteJoint.jointAngle();
revoluteJoint.jointSpeed();

RopeJoint

A RopeJoint restricts the maximum distance between two points on two bodies.

RopeJointDef requires two body anchor points and the maximum length.

final ropeJointDef = RopeJointDef()
  ..bodyA = firstBody
  ..localAnchorA.setFrom(firstBody.getLocalCenter())
  ..bodyB = secondBody
  ..localAnchorB.setFrom(secondBody.getLocalCenter())
  ..maxLength = (secondBody.worldCenter - firstBody.worldCenter).length;

world.createJoint(RopeJoint(ropeJointDef));
rope_joint.dart
 1import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart';
 2import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boxes.dart';
 3import 'package:flame/components.dart';
 4import 'package:flame/events.dart';
 5import 'package:flame_forge2d/flame_forge2d.dart';
 6import 'package:flutter/material.dart';
 7
 8class RopeJointExample extends Forge2DGame {
 9  static const description = '''
10    This example shows how to use a `RopeJoint`.
11
12    Drag the box handle along the axis and observe the rope respond to the
13    movement.
14  ''';
15
16  RopeJointExample() : super(world: RopeJointWorld());
17}
18
19class RopeJointWorld extends Forge2DWorld
20    with DragCallbacks, HasGameReference<Forge2DGame> {
21  double handleWidth = 6;
22
23  @override
24  Future<void> onLoad() async {
25    super.onLoad();
26
27    final handleBody = await createHandle();
28    createRope(handleBody);
29  }
30
31  Future<Body> createHandle() async {
32    final anchor = game.screenToWorld(Vector2(0, 100))..x = 0;
33
34    final box = DraggableBox(
35      startPosition: anchor,
36      width: handleWidth,
37      height: 3,
38    );
39    await add(box);
40
41    createPrismaticJoint(box.body, anchor);
42    return box.body;
43  }
44
45  Future<void> createRope(Body handle) async {
46    const length = 50;
47    var prevBody = handle;
48
49    for (var i = 0; i < length; i++) {
50      final newPosition = prevBody.worldCenter + Vector2(0, 1);
51      final ball = Ball(newPosition, radius: 0.5, color: Colors.white);
52      await add(ball);
53
54      createRopeJoint(ball.body, prevBody);
55      prevBody = ball.body;
56    }
57  }
58
59  void createPrismaticJoint(Body box, Vector2 anchor) {
60    final groundBody = createBody(BodyDef());
61    final halfWidth = game.screenToWorld(Vector2.zero()).x.abs();
62
63    final prismaticJointDef = PrismaticJointDef()
64      ..initialize(
65        box,
66        groundBody,
67        anchor,
68        Vector2(1, 0),
69      )
70      ..enableLimit = true
71      ..lowerTranslation = -halfWidth + handleWidth / 2
72      ..upperTranslation = halfWidth - handleWidth / 2;
73
74    final joint = PrismaticJoint(prismaticJointDef);
75    createJoint(joint);
76  }
77
78  void createRopeJoint(Body first, Body second) {
79    final ropeJointDef = RopeJointDef()
80      ..bodyA = first
81      ..localAnchorA.setFrom(first.getLocalCenter())
82      ..bodyB = second
83      ..localAnchorB.setFrom(second.getLocalCenter())
84      ..maxLength = (second.worldCenter - first.worldCenter).length;
85
86    createJoint(RopeJoint(ropeJointDef));
87  }
88}
  • bodyA, bodyB: Connected bodies

  • localAnchorA, localAnchorB: Optional parameter, anchor point relative to the body’s origin.

  • maxLength: The maximum length of the rope. This must be larger than linearSlop, or the joint will have no effect.

Warning

The joint assumes that the maximum length doesn’t change during simulation. See DistanceJoint if you want to dynamically control length.

WeldJoint

A WeldJoint is used to restrict all relative motion between two bodies, effectively joining them together.

WeldJointDef requires two bodies that will be connected, and a world anchor:

final weldJointDef = WeldJointDef()
  ..initialize(bodyA, bodyB, anchor);

world.createJoint(WeldJoint(weldJointDef));
weld_joint.dart
  1import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart';
  2import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boxes.dart';
  3import 'package:flame/components.dart';
  4import 'package:flame/events.dart';
  5import 'package:flame_forge2d/flame_forge2d.dart';
  6import 'package:flutter/material.dart';
  7
  8class WeldJointExample extends Forge2DGame {
  9  static const description = '''
 10    This example shows how to use a `WeldJoint`. Tap the screen to add a
 11    ball to test the bridge built using a `WeldJoint`
 12  ''';
 13
 14  WeldJointExample() : super(world: WeldJointWorld());
 15}
 16
 17class WeldJointWorld extends Forge2DWorld
 18    with TapCallbacks, HasGameReference<Forge2DGame> {
 19  final pillarHeight = 20.0;
 20  final pillarWidth = 5.0;
 21
 22  @override
 23  Future<void> onLoad() async {
 24    super.onLoad();
 25
 26    final leftPillar = Box(
 27      startPosition: game.screenToWorld(Vector2(50, game.size.y))
 28        ..y -= pillarHeight / 2,
 29      width: pillarWidth,
 30      height: pillarHeight,
 31      bodyType: BodyType.static,
 32      color: Colors.white,
 33    );
 34    final rightPillar = Box(
 35      startPosition: game.screenToWorld(Vector2(game.size.x - 50, game.size.y))
 36        ..y -= pillarHeight / 2,
 37      width: pillarWidth,
 38      height: pillarHeight,
 39      bodyType: BodyType.static,
 40      color: Colors.white,
 41    );
 42
 43    await addAll([leftPillar, rightPillar]);
 44
 45    createBridge(leftPillar, rightPillar);
 46  }
 47
 48  Future<void> createBridge(
 49    Box leftPillar,
 50    Box rightPillar,
 51  ) async {
 52    const sectionsCount = 10;
 53    // Vector2.zero is used here since 0,0 is in the middle and 0,0 in the
 54    // screen space then gives us the coordinates of the upper left corner in
 55    // world space.
 56    final halfSize = game.screenToWorld(Vector2.zero())..absolute();
 57    final sectionWidth = ((leftPillar.center.x.abs() +
 58                rightPillar.center.x.abs() +
 59                pillarWidth) /
 60            sectionsCount)
 61        .ceilToDouble();
 62    Body? prevSection;
 63
 64    for (var i = 0; i < sectionsCount; i++) {
 65      final section = Box(
 66        startPosition: Vector2(
 67          sectionWidth * i - halfSize.x + sectionWidth / 2,
 68          halfSize.y - pillarHeight,
 69        ),
 70        width: sectionWidth,
 71        height: 1,
 72      );
 73      await add(section);
 74
 75      if (prevSection != null) {
 76        createWeldJoint(
 77          prevSection,
 78          section.body,
 79          Vector2(
 80            sectionWidth * i - halfSize.x + sectionWidth,
 81            halfSize.y - pillarHeight,
 82          ),
 83        );
 84      }
 85
 86      prevSection = section.body;
 87    }
 88  }
 89
 90  void createWeldJoint(Body first, Body second, Vector2 anchor) {
 91    final weldJointDef = WeldJointDef()..initialize(first, second, anchor);
 92
 93    createJoint(WeldJoint(weldJointDef));
 94  }
 95
 96  @override
 97  Future<void> onTapDown(TapDownEvent info) async {
 98    super.onTapDown(info);
 99    final ball = Ball(info.localPosition, radius: 5);
100    add(ball);
101  }
102}
  • bodyA, bodyB: Two bodies that will be connected

  • anchor: Anchor point in world coordinates, at which two bodies will be welded together to 0, the higher the value, the less springy the joint becomes.

Breakable Bodies and WeldJoint

Since the Forge2D constraint solver is iterative, joints are somewhat flexible. This means that the bodies connected by a WeldJoint may bend slightly. If you want to simulate a breakable body, it’s better to create a single body with multiple fixtures. When the body breaks, you can destroy a fixture and recreate it on a new body instead of relying on a WeldJoint.