docs: PulleyJoint documentation and example (#2425)

PulleyJoint documentation and example
This commit is contained in:
Eugene Kleshnin
2023-03-24 15:50:34 +00:00
committed by GitHub
parent 4ff25ffb1f
commit e1049470fa
7 changed files with 221 additions and 11 deletions

View File

@ -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.
```

View File

@ -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) {

View File

@ -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,
);
}

View File

@ -24,7 +24,12 @@ class MotorJointExample extends Forge2DGame with TapDetector, HasDraggables {
Future<void> 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));

View File

@ -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<void> 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);
}
}

View File

@ -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);

View File

@ -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;
}
}