diff --git a/doc/bridge_packages/flame_forge2d/forge2d.md b/doc/bridge_packages/flame_forge2d/forge2d.md index a0c92849a..17a52aa11 100644 --- a/doc/bridge_packages/flame_forge2d/forge2d.md +++ b/doc/bridge_packages/flame_forge2d/forge2d.md @@ -1,7 +1,7 @@ # Forge2D -We (the Flame organization) maintain a ported version of the Box2D physics engine and our version -is called Forge2D. +Blue Fire maintains a ported version of the Box2D physics engine and our +version is called Forge2D. If you want to use Forge2D specifically for Flame you should use our bridge library [flame_forge2d](https://github.com/flame-engine/flame/tree/main/packages/flame_forge2d) and if you @@ -20,23 +20,24 @@ instructions](https://pub.dev/packages/flame_forge2d)]( with ContactCallbacks { final double radius; - final Vector2 position; + final Vector2 initialPosition; final double rotation; final bool isMovable; final rng = Random(); late final Paint _shaderPaint; Ball({ - required this.position, + required this.initialPosition, this.radius = 80.0, this.rotation = 1.0, this.isMovable = true, @@ -48,7 +48,7 @@ class Ball extends BodyComponent with ContactCallbacks { final def = BodyDef() ..userData = this ..type = isMovable ? BodyType.dynamic : BodyType.kinematic - ..position = position; + ..position = initialPosition; final body = world.createBody(def)..angularVelocity = rotation; final shape = CircleShape()..radius = radius; @@ -72,7 +72,7 @@ class Ball extends BodyComponent with ContactCallbacks { } late Rect asRect = Rect.fromCircle( - center: position.toOffset(), + center: initialPosition.toOffset(), radius: radius, ); } @@ -82,12 +82,13 @@ List createBalls(Vector2 trackSize, List walls, Ball bigBall) { final rng = Random(); while (balls.length < 20) { final ball = Ball( - position: Vector2.random(rng)..multiply(trackSize), + initialPosition: Vector2.random(rng)..multiply(trackSize), radius: 3.0 + rng.nextInt(5), rotation: (rng.nextBool() ? 1 : -1) * rng.nextInt(5).toDouble(), ); - final touchesBall = ball.position.distanceTo(bigBall.position) < - ball.radius + bigBall.radius; + final touchesBall = + ball.initialPosition.distanceTo(bigBall.initialPosition) < + ball.radius + bigBall.radius; if (!touchesBall) { final touchesWall = walls.any((wall) => wall.asRect.overlaps(ball.asRect)); diff --git a/examples/games/padracing/lib/car.dart b/examples/games/padracing/lib/car.dart index 2a97dc3ad..322d9b92d 100644 --- a/examples/games/padracing/lib/car.dart +++ b/examples/games/padracing/lib/car.dart @@ -102,7 +102,7 @@ class Car extends BodyComponent { final isLeftTire = i.isEven; return Tire( car: this, - pressedKeys: gameRef.pressedKeySets[playerNumber], + pressedKeys: game.pressedKeySets[playerNumber], isFrontTire: isFrontTire, isLeftTire: isLeftTire, jointDef: jointDef, @@ -110,7 +110,7 @@ class Car extends BodyComponent { ); }); - gameRef.cameraWorld.addAll(tires); + game.world.addAll(tires); return body; } diff --git a/examples/games/padracing/lib/lap_line.dart b/examples/games/padracing/lib/lap_line.dart index 6ec56f16b..8e4758d6a 100644 --- a/examples/games/padracing/lib/lap_line.dart +++ b/examples/games/padracing/lib/lap_line.dart @@ -10,12 +10,12 @@ import 'package:padracing/car.dart'; import 'package:padracing/game_colors.dart'; class LapLine extends BodyComponent with ContactCallbacks { - LapLine(this.id, this.position, this.size, {required this.isFinish}) + LapLine(this.id, this.initialPosition, this.size, {required this.isFinish}) : super(priority: 1); final int id; final bool isFinish; - final Vector2 position; + final Vector2 initialPosition; final Vector2 size; late final Rect rect = size.toRect(); Image? _finishOverlay; @@ -45,7 +45,7 @@ class LapLine extends BodyComponent with ContactCallbacks { final groundBody = world.createBody( BodyDef( - position: position, + position: initialPosition, userData: this, ), ); diff --git a/examples/games/padracing/lib/lap_text.dart b/examples/games/padracing/lib/lap_text.dart index 08226610a..1714e631f 100644 --- a/examples/games/padracing/lib/lap_text.dart +++ b/examples/games/padracing/lib/lap_text.dart @@ -1,4 +1,5 @@ import 'package:flame/components.dart'; +import 'package:flame/experimental.dart'; import 'package:flame/extensions.dart'; import 'package:flutter/material.dart' hide Image, Gradient; import 'package:google_fonts/google_fonts.dart'; @@ -6,7 +7,7 @@ import 'package:google_fonts/google_fonts.dart'; import 'package:padracing/car.dart'; import 'package:padracing/padracing_game.dart'; -class LapText extends PositionComponent with HasGameRef { +class LapText extends PositionComponent with HasGameReference { LapText({required this.car, required Vector2 position}) : super(position: position); @@ -66,10 +67,10 @@ class LapText extends PositionComponent with HasGameRef { @override void update(double dt) { - if (gameRef.isGameOver) { + if (game.isGameOver) { return; } - _timePassedComponent.text = gameRef.timePassed; + _timePassedComponent.text = game.timePassed; } final _backgroundRect = RRect.fromRectAndRadius( diff --git a/examples/games/padracing/lib/padracing_game.dart b/examples/games/padracing/lib/padracing_game.dart index f159c2a8f..cb385ade5 100644 --- a/examples/games/padracing/lib/padracing_game.dart +++ b/examples/games/padracing/lib/padracing_game.dart @@ -47,7 +47,6 @@ class PadRacingGame extends Forge2DGame with KeyboardEvents { static final Vector2 trackSize = Vector2.all(500); static const double playZoom = 8.0; static const int numberOfLaps = 3; - late final World cameraWorld; late CameraComponent startCamera; late List> activeKeyMaps; late List> pressedKeySets; @@ -58,13 +57,13 @@ class PadRacingGame extends Forge2DGame with KeyboardEvents { @override Future onLoad() async { + super.onLoad(); + cameraComponent.removeFromParent(); children.register(); - cameraWorld = World(); - add(cameraWorld); final walls = createWalls(trackSize); - final bigBall = Ball(position: Vector2(200, 245), isMovable: false); - cameraWorld.addAll([ + final bigBall = Ball(initialPosition: Vector2(200, 245), isMovable: false); + world.addAll([ LapLine(1, Vector2(25, 50), Vector2(50, 5), isFinish: false), LapLine(2, Vector2(25, 70), Vector2(50, 5), isFinish: false), LapLine(3, Vector2(52.5, 25), Vector2(5, 50), isFinish: true), @@ -82,9 +81,7 @@ class PadRacingGame extends Forge2DGame with KeyboardEvents { canvasSize.x / trackSize.x, canvasSize.y / trackSize.y, ); - startCamera = CameraComponent( - world: cameraWorld, - ) + startCamera = CameraComponent(world: world) ..viewfinder.position = trackSize / 2 ..viewfinder.anchor = Anchor.center ..viewfinder.zoom = zoomLevel - 0.2; @@ -136,7 +133,7 @@ class PadRacingGame extends Forge2DGame with KeyboardEvents { ..paint.style = PaintingStyle.stroke; final cameras = List.generate(numberOfPlayers, (i) { return CameraComponent( - world: cameraWorld, + world: world, viewport: FixedSizeViewport(viewportSize.x, viewportSize.y) ..position = alignedVector( longMultiplier: i == 0 ? 0.0 : 1 / (i + 1), @@ -152,7 +149,7 @@ class PadRacingGame extends Forge2DGame with KeyboardEvents { const mapCameraZoom = 0.5; final mapCameras = List.generate(numberOfPlayers, (i) { return CameraComponent( - world: cameraWorld, + world: world, viewport: FixedSizeViewport(mapCameraSize.x, mapCameraSize.y) ..position = Vector2( viewportSize.x - mapCameraSize.x * mapCameraZoom - 50, @@ -193,7 +190,7 @@ class PadRacingGame extends Forge2DGame with KeyboardEvents { } }); cars.add(car); - cameraWorld.add(car); + world.add(car); cameras[i].viewport.addAll([lapText, mapCameras[i]]); } diff --git a/examples/games/padracing/lib/tire.dart b/examples/games/padracing/lib/tire.dart index 82af7a635..7816176ea 100644 --- a/examples/games/padracing/lib/tire.dart +++ b/examples/games/padracing/lib/tire.dart @@ -64,7 +64,7 @@ class Tire extends BodyComponent { @override Future onLoad() async { await super.onLoad(); - gameRef.cameraWorld.add(Trail(car: car, tire: this)); + game.world.add(Trail(car: car, tire: this)); } @override @@ -94,7 +94,7 @@ class Tire extends BodyComponent { if (body.isAwake || pressedKeys.isNotEmpty) { _updateTurn(dt); _updateFriction(); - if (!gameRef.isGameOver) { + if (!game.isGameOver) { _updateDrive(); } } diff --git a/examples/games/padracing/lib/wall.dart b/examples/games/padracing/lib/wall.dart index 5038fd937..454d220ec 100644 --- a/examples/games/padracing/lib/wall.dart +++ b/examples/games/padracing/lib/wall.dart @@ -33,9 +33,9 @@ List createWalls(Vector2 size) { } class Wall extends BodyComponent { - Wall(this.position, this.size) : super(priority: 3); + Wall(this._position, this.size) : super(priority: 3); - final Vector2 position; + final Vector2 _position; final Vector2 size; final Random rng = Random(); @@ -94,7 +94,7 @@ class Wall extends BodyComponent { Body createBody() { final def = BodyDef() ..type = BodyType.static - ..position = position; + ..position = _position; final body = world.createBody(def) ..userData = this ..angularDamping = 3.0; @@ -105,7 +105,7 @@ class Wall extends BodyComponent { } late Rect asRect = Rect.fromCenter( - center: position.toOffset(), + center: _position.toOffset(), width: size.x, height: size.y, ); diff --git a/examples/lib/main.dart b/examples/lib/main.dart index d00bce088..04b89c003 100644 --- a/examples/lib/main.dart +++ b/examples/lib/main.dart @@ -3,21 +3,21 @@ import 'package:examples/platform/stub_provider.dart' if (dart.library.html) 'platform/web_provider.dart'; import 'package:examples/stories/animations/animations.dart'; import 'package:examples/stories/bridge_libraries/audio/audio.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/flame_forge2d.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/constant_volume_joint.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/distance_joint.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/friction_joint.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/gear_joint.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/motor_joint.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/mouse_joint.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/prismatic_joint.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/pulley_joint.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/revolute_joint.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/rope_joint.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/weld_joint.dart'; import 'package:examples/stories/bridge_libraries/flame_isolate/isolate.dart'; import 'package:examples/stories/bridge_libraries/flame_lottie/lottie.dart'; import 'package:examples/stories/bridge_libraries/flame_spine/flame_spine.dart'; -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/gear_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/prismatic_joint.dart'; -import 'package:examples/stories/bridge_libraries/forge2d/joints/pulley_joint.dart'; -import 'package:examples/stories/bridge_libraries/forge2d/joints/revolute_joint.dart'; -import 'package:examples/stories/bridge_libraries/forge2d/joints/rope_joint.dart'; -import 'package:examples/stories/bridge_libraries/forge2d/joints/weld_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'; diff --git a/examples/lib/stories/bridge_libraries/forge2d/animated_body_example.dart b/examples/lib/stories/bridge_libraries/flame_forge2d/animated_body_example.dart similarity index 68% rename from examples/lib/stories/bridge_libraries/forge2d/animated_body_example.dart rename to examples/lib/stories/bridge_libraries/flame_forge2d/animated_body_example.dart index ca9b03f47..abbfb33c7 100644 --- a/examples/lib/stories/bridge_libraries/forge2d/animated_body_example.dart +++ b/examples/lib/stories/bridge_libraries/flame_forge2d/animated_body_example.dart @@ -1,11 +1,13 @@ import 'dart:ui'; -import 'package:examples/stories/bridge_libraries/forge2d/utils/boundaries.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart'; import 'package:flame/components.dart'; -import 'package:flame/input.dart'; +import 'package:flame/events.dart'; +import 'package:flame/experimental.dart'; +import 'package:flame/flame.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; -class AnimatedBodyExample extends Forge2DGame with TapDetector { +class AnimatedBodyExample extends Forge2DGame { static const String description = ''' In this example we show how to add an animated chopper, which is created with a SpriteAnimationComponent, on top of a BodyComponent. @@ -13,14 +15,22 @@ class AnimatedBodyExample extends Forge2DGame with TapDetector { Tap the screen to add more choppers. '''; - AnimatedBodyExample() : super(gravity: Vector2.zero()); + AnimatedBodyExample() + : super( + gravity: Vector2.zero(), + world: AnimatedBodyWorld(), + ); +} +class AnimatedBodyWorld extends Forge2DWorld + with TapCallbacks, HasGameReference { late Image chopper; late SpriteAnimation animation; @override Future onLoad() async { - chopper = await images.load('animations/chopper.png'); + super.onLoad(); + chopper = await Flame.images.load('animations/chopper.png'); animation = SpriteAnimation.fromFrameData( chopper, @@ -31,14 +41,14 @@ class AnimatedBodyExample extends Forge2DGame with TapDetector { ), ); - final boundaries = createBoundaries(this); - boundaries.forEach(add); + final boundaries = createBoundaries(game); + addAll(boundaries); } @override - void onTapDown(TapDownInfo info) { + void onTapDown(TapDownEvent info) { super.onTapDown(info); - final position = info.eventPosition.game; + final position = info.localPosition; final spriteSize = Vector2.all(10); final animationComponent = SpriteAnimationComponent( animation: animation, @@ -50,11 +60,11 @@ class AnimatedBodyExample extends Forge2DGame with TapDetector { } class ChopperBody extends BodyComponent { - final Vector2 position; + final Vector2 _position; final Vector2 size; ChopperBody( - this.position, + this._position, PositionComponent component, ) : size = component.size { renderBody = false; @@ -74,7 +84,7 @@ class ChopperBody extends BodyComponent { final velocity = (Vector2.random() - Vector2.random()) * 200; final bodyDef = BodyDef( - position: position, + position: _position, angle: velocity.angleTo(Vector2(1, 0)), linearVelocity: velocity, type: BodyType.dynamic, diff --git a/examples/lib/stories/bridge_libraries/forge2d/blob_example.dart b/examples/lib/stories/bridge_libraries/flame_forge2d/blob_example.dart similarity index 78% rename from examples/lib/stories/bridge_libraries/forge2d/blob_example.dart rename to examples/lib/stories/bridge_libraries/flame_forge2d/blob_example.dart index a134765bf..768762c26 100644 --- a/examples/lib/stories/bridge_libraries/forge2d/blob_example.dart +++ b/examples/lib/stories/bridge_libraries/flame_forge2d/blob_example.dart @@ -2,25 +2,30 @@ import 'dart:math' as math; -import 'package:examples/stories/bridge_libraries/forge2d/utils/boundaries.dart'; -import 'package:flame/input.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart'; +import 'package:flame/events.dart'; +import 'package:flame/experimental.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; -class BlobExample extends Forge2DGame with TapDetector { +class BlobExample extends Forge2DGame { static const String description = ''' In this example we show the power of joints by showing interactions between bodies tied together. Tap the screen to add boxes that will bounce on the "blob" in the center. '''; + BlobExample() : super(world: BlobWorld()); +} +class BlobWorld extends Forge2DWorld + with TapCallbacks, HasGameReference { @override Future onLoad() async { - final worldCenter = screenToWorld(size * camera.zoom / 2); - final blobCenter = worldCenter + Vector2(0, -30); + super.onLoad(); + final blobCenter = Vector2(0, -30); final blobRadius = Vector2.all(6.0); - addAll(createBoundaries(this)); - add(Ground(worldCenter)); + addAll(createBoundaries(game)); + add(Ground(Vector2.zero())); final jointDef = ConstantVolumeJointDef() ..frequencyHz = 20.0 ..dampingRatio = 1.0 @@ -30,13 +35,13 @@ class BlobExample extends Forge2DGame with TapDetector { for (var i = 0; i < 20; i++) BlobPart(i, jointDef, blobRadius, blobCenter), ]); - world.createJoint(ConstantVolumeJoint(world, jointDef)); + createJoint(ConstantVolumeJoint(physicsWorld, jointDef)); } @override - void onTapDown(TapDownInfo info) { + void onTapDown(TapDownEvent info) { super.onTapDown(info); - add(FallingBox(info.eventPosition.game)); + add(FallingBox(info.localPosition)); } } @@ -104,15 +109,15 @@ class BlobPart extends BodyComponent { } class FallingBox extends BodyComponent { - final Vector2 position; + final Vector2 _position; - FallingBox(this.position); + FallingBox(this._position); @override Body createBody() { final bodyDef = BodyDef( type: BodyType.dynamic, - position: position, + position: _position, ); final shape = PolygonShape()..setAsBoxXY(2, 4); final body = world.createBody(bodyDef); diff --git a/examples/lib/stories/bridge_libraries/flame_forge2d/camera_example.dart b/examples/lib/stories/bridge_libraries/flame_forge2d/camera_example.dart new file mode 100644 index 000000000..55c6ba227 --- /dev/null +++ b/examples/lib/stories/bridge_libraries/flame_forge2d/camera_example.dart @@ -0,0 +1,23 @@ +import 'package:examples/stories/bridge_libraries/flame_forge2d/domino_example.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/sprite_body_example.dart'; +import 'package:flame/events.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; + +class CameraExample extends Forge2DGame { + static const String description = ''' + This example showcases the possibility to follow BodyComponents with the + camera. When the screen is tapped a pizza is added, which the camera will + follow. Other than that it is the same as the domino example. + '''; + CameraExample() : super(world: CameraExampleWorld()); +} + +class CameraExampleWorld extends DominoExampleWorld { + @override + void onTapDown(TapDownEvent info) { + final position = info.localPosition; + final pizza = Pizza(position); + add(pizza); + pizza.mounted.whenComplete(() => game.cameraComponent.follow(pizza)); + } +} diff --git a/examples/lib/stories/bridge_libraries/forge2d/composition_example.dart b/examples/lib/stories/bridge_libraries/flame_forge2d/composition_example.dart similarity index 68% rename from examples/lib/stories/bridge_libraries/forge2d/composition_example.dart rename to examples/lib/stories/bridge_libraries/flame_forge2d/composition_example.dart index 1246abfaf..f68962bbb 100644 --- a/examples/lib/stories/bridge_libraries/forge2d/composition_example.dart +++ b/examples/lib/stories/bridge_libraries/flame_forge2d/composition_example.dart @@ -1,17 +1,14 @@ -// ignore_for_file: deprecated_member_use - -import 'package:examples/stories/bridge_libraries/forge2d/utils/balls.dart'; -import 'package:examples/stories/bridge_libraries/forge2d/utils/boundaries.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart'; import 'package:flame/components.dart'; import 'package:flame/effects.dart'; -import 'package:flame/game.dart'; -import 'package:flame/input.dart'; +import 'package:flame/events.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/material.dart'; const TextStyle _textStyle = TextStyle(color: Colors.white, fontSize: 2); -class CompositionExample extends Forge2DGame with HasTappables { +class CompositionExample extends Forge2DGame { static const description = ''' This example shows how to compose a `BodyComponent` together with a normal Flame component. Click the ball to see the number increment. @@ -21,15 +18,15 @@ class CompositionExample extends Forge2DGame with HasTappables { @override Future onLoad() async { + super.onLoad(); final boundaries = createBoundaries(this); - boundaries.forEach(add); - final viewportCenter = camera.viewport.effectiveSize / 2; - add(TappableText(screenToFlameWorld(viewportCenter)..y = 5)); - add(TappableBall(screenToWorld(viewportCenter))); + world.addAll(boundaries); + world.add(TappableText(Vector2(0, 5))); + world.add(TappableBall(Vector2.zero())); } } -class TappableText extends TextComponent with Tappable { +class TappableText extends TextComponent with TapCallbacks { TappableText(Vector2 position) : super( text: 'A normal tappable Flame component', @@ -52,7 +49,7 @@ class TappableText extends TextComponent with Tappable { } @override - bool onTapDown(TapDownInfo info) { + void onTapDown(TapDownEvent event) { add( MoveEffect.by( Vector2.all(5), @@ -62,11 +59,10 @@ class TappableText extends TextComponent with Tappable { ), ), ); - return true; } } -class TappableBall extends Ball with Tappable { +class TappableBall extends Ball with TapCallbacks { late final TextComponent textComponent; int counter = 0; late final TextPaint _textPaint; @@ -90,10 +86,6 @@ class TappableBall extends Ball with Tappable { @override void update(double dt) { super.update(dt); - // This is unfortunately needed since [BodyComponent] will set all its - // children to `debugMode = true` currently, we should come up with a - // nicer solution to this. - textComponent.debugMode = false; textComponent.text = counter.toString(); } diff --git a/examples/lib/stories/bridge_libraries/flame_forge2d/contact_callbacks_example.dart b/examples/lib/stories/bridge_libraries/flame_forge2d/contact_callbacks_example.dart new file mode 100644 index 000000000..b7b5a5d3d --- /dev/null +++ b/examples/lib/stories/bridge_libraries/flame_forge2d/contact_callbacks_example.dart @@ -0,0 +1,40 @@ +import 'dart:math' as math; + +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart'; +import 'package:flame/events.dart'; +import 'package:flame/experimental.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; + +class ContactCallbacksExample extends Forge2DGame { + static const description = ''' + This example shows how `BodyComponent`s can react to collisions with other + bodies. + Tap the screen to add balls, the white balls will give an impulse to the + balls that it collides with. + '''; + + ContactCallbacksExample() + : super(gravity: Vector2(0, 10.0), world: ContactCallbackWorld()); +} + +class ContactCallbackWorld extends Forge2DWorld + with TapCallbacks, HasGameReference { + @override + Future onLoad() async { + super.onLoad(); + final boundaries = createBoundaries(game); + addAll(boundaries); + } + + @override + void onTapDown(TapDownEvent info) { + super.onTapDown(info); + final position = info.localPosition; + if (math.Random().nextInt(10) < 2) { + add(WhiteBall(position)); + } else { + add(Ball(position)); + } + } +} diff --git a/examples/lib/stories/bridge_libraries/forge2d/domino_example.dart b/examples/lib/stories/bridge_libraries/flame_forge2d/domino_example.dart similarity index 53% rename from examples/lib/stories/bridge_libraries/forge2d/domino_example.dart rename to examples/lib/stories/bridge_libraries/flame_forge2d/domino_example.dart index b3fca5065..80b3df1d5 100644 --- a/examples/lib/stories/bridge_libraries/forge2d/domino_example.dart +++ b/examples/lib/stories/bridge_libraries/flame_forge2d/domino_example.dart @@ -2,72 +2,77 @@ import 'dart:ui'; -import 'package:examples/stories/bridge_libraries/forge2d/sprite_body_example.dart'; -import 'package:examples/stories/bridge_libraries/forge2d/utils/boundaries.dart'; -import 'package:flame/input.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/sprite_body_example.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart'; +import 'package:flame/events.dart'; +import 'package:flame/experimental.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; -class DominoExample extends Forge2DGame with TapDetector { +class DominoExample extends Forge2DGame { static const description = ''' In this example we can see some domino tiles lined up. If you tap on the screen a pizza is added which can tip the tiles over and cause a chain reaction. '''; - DominoExample() : super(gravity: Vector2(0, 10.0)); + DominoExample() + : super(gravity: Vector2(0, 10.0), world: DominoExampleWorld()); +} +class DominoExampleWorld extends Forge2DWorld + with TapCallbacks, HasGameReference { late Image pizzaImage; @override Future onLoad() async { - final boundaries = createBoundaries(this); - boundaries.forEach(add); - final center = screenToWorld(camera.viewport.effectiveSize / 2); + super.onLoad(); + final boundaries = createBoundaries(game); + addAll(boundaries); const numberOfRows = 7; for (var i = 0; i < numberOfRows - 2; i++) { - final position = center + Vector2(0.0, 5.0 * i); - add(Platform(position)); + add(Platform(Vector2(0.0, 5.0 * i))); } const numberPerRow = 25; for (var i = 0; i < numberOfRows; ++i) { for (var j = 0; j < numberPerRow; j++) { - final position = center + - Vector2(-14.75 + j * (29.5 / (numberPerRow - 1)), -12.7 + 5 * i); + final position = Vector2( + -14.75 + j * (29.5 / (numberPerRow - 1)), + -12.7 + 5 * i, + ); add(DominoBrick(position)); } } } @override - void onTapDown(TapDownInfo info) { - super.onTapDown(info); - final position = info.eventPosition.game; - add(Pizza(position)..renderBody = true); + void onTapDown(TapDownEvent info) { + final position = info.localPosition; + add(Pizza(position)); } } class Platform extends BodyComponent { - final Vector2 position; + final Vector2 _position; - Platform(this.position); + Platform(this._position); @override Body createBody() { final shape = PolygonShape()..setAsBoxXY(14.8, 0.125); final fixtureDef = FixtureDef(shape); - final bodyDef = BodyDef(position: position); + final bodyDef = BodyDef(position: _position); final body = world.createBody(bodyDef); return body..createFixture(fixtureDef); } } class DominoBrick extends BodyComponent { - final Vector2 position; + final Vector2 _position; - DominoBrick(this.position); + DominoBrick(this._position); @override Body createBody() { @@ -79,7 +84,7 @@ class DominoBrick extends BodyComponent { friction: 0.5, ); - final bodyDef = BodyDef(type: BodyType.dynamic, position: position); + final bodyDef = BodyDef(type: BodyType.dynamic, position: _position); return world.createBody(bodyDef)..createFixture(fixtureDef); } } diff --git a/examples/lib/stories/bridge_libraries/flame_forge2d/drag_callbacks_example.dart b/examples/lib/stories/bridge_libraries/flame_forge2d/drag_callbacks_example.dart new file mode 100644 index 000000000..e75bb8eed --- /dev/null +++ b/examples/lib/stories/bridge_libraries/flame_forge2d/drag_callbacks_example.dart @@ -0,0 +1,47 @@ +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart'; +import 'package:flame/events.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/material.dart' hide Draggable; + +class DragCallbacksExample extends Forge2DGame { + static const description = ''' + In this example we use Flame's normal `DragCallbacks` mixin to give impulses + to a ball when we are dragging it around. If you are interested in dragging + bodies around, also have a look at the MouseJointExample. + '''; + + DragCallbacksExample() : super(gravity: Vector2.all(0.0)); + + @override + Future onLoad() async { + super.onLoad(); + final boundaries = createBoundaries(this); + world.addAll(boundaries); + world.add(DraggableBall(Vector2.zero())); + } +} + +class DraggableBall extends Ball with DragCallbacks { + DraggableBall(super.position) : super(radius: 5) { + originalPaint = Paint()..color = Colors.amber; + paint = originalPaint; + } + + @override + void onDragStart(DragStartEvent event) { + super.onDragStart(event); + paint = randomPaint(); + } + + @override + void onDragUpdate(DragUpdateEvent event) { + body.applyLinearImpulse(event.delta * 1000); + } + + @override + void onDragEnd(DragEndEvent event) { + super.onDragEnd(event); + paint = originalPaint; + } +} diff --git a/examples/lib/stories/bridge_libraries/forge2d/flame_forge2d.dart b/examples/lib/stories/bridge_libraries/flame_forge2d/flame_forge2d.dart similarity index 67% rename from examples/lib/stories/bridge_libraries/forge2d/flame_forge2d.dart rename to examples/lib/stories/bridge_libraries/flame_forge2d/flame_forge2d.dart index 5e9c1689d..ab14be633 100644 --- a/examples/lib/stories/bridge_libraries/forge2d/flame_forge2d.dart +++ b/examples/lib/stories/bridge_libraries/flame_forge2d/flame_forge2d.dart @@ -1,28 +1,28 @@ import 'package:dashbook/dashbook.dart'; import 'package:examples/commons/commons.dart'; -import 'package:examples/stories/bridge_libraries/forge2d/animated_body_example.dart'; -import 'package:examples/stories/bridge_libraries/forge2d/blob_example.dart'; -import 'package:examples/stories/bridge_libraries/forge2d/camera_example.dart'; -import 'package:examples/stories/bridge_libraries/forge2d/composition_example.dart'; -import 'package:examples/stories/bridge_libraries/forge2d/contact_callbacks_example.dart'; -import 'package:examples/stories/bridge_libraries/forge2d/domino_example.dart'; -import 'package:examples/stories/bridge_libraries/forge2d/draggable_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/gear_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/prismatic_joint.dart'; -import 'package:examples/stories/bridge_libraries/forge2d/joints/pulley_joint.dart'; -import 'package:examples/stories/bridge_libraries/forge2d/joints/revolute_joint.dart'; -import 'package:examples/stories/bridge_libraries/forge2d/joints/rope_joint.dart'; -import 'package:examples/stories/bridge_libraries/forge2d/joints/weld_joint.dart'; -import 'package:examples/stories/bridge_libraries/forge2d/raycast_example.dart'; -import 'package:examples/stories/bridge_libraries/forge2d/revolute_joint_with_motor_example.dart'; -import 'package:examples/stories/bridge_libraries/forge2d/sprite_body_example.dart'; -import 'package:examples/stories/bridge_libraries/forge2d/tap_callbacks_example.dart'; -import 'package:examples/stories/bridge_libraries/forge2d/widget_example.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/animated_body_example.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/blob_example.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/camera_example.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/composition_example.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/contact_callbacks_example.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/domino_example.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/drag_callbacks_example.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/constant_volume_joint.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/distance_joint.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/friction_joint.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/gear_joint.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/motor_joint.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/mouse_joint.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/prismatic_joint.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/pulley_joint.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/revolute_joint.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/rope_joint.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/weld_joint.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/raycast_example.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/revolute_joint_with_motor_example.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/sprite_body_example.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/tap_callbacks_example.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/widget_example.dart'; import 'package:flame/game.dart'; String link(String example) => baseLink('bridge_libraries/forge2d/$example'); @@ -74,15 +74,15 @@ void addForge2DStories(Dashbook dashbook) { ) ..add( 'Tappable Body', - (DashbookContext ctx) => GameWidget(game: TappableExample()), + (DashbookContext ctx) => GameWidget(game: TapCallbacksExample()), codeLink: link('tap_callbacks_example.dart'), - info: TappableExample.description, + info: TapCallbacksExample.description, ) ..add( 'Draggable Body', - (DashbookContext ctx) => GameWidget(game: DraggableExample()), - codeLink: link('draggable_example.dart'), - info: DraggableExample.description, + (DashbookContext ctx) => GameWidget(game: DragCallbacksExample()), + codeLink: link('drag_callbacks_example.dart'), + info: DragCallbacksExample.description, ) ..add( 'Camera', diff --git a/examples/lib/stories/bridge_libraries/forge2d/joints/constant_volume_joint.dart b/examples/lib/stories/bridge_libraries/flame_forge2d/joints/constant_volume_joint.dart similarity index 58% rename from examples/lib/stories/bridge_libraries/forge2d/joints/constant_volume_joint.dart rename to examples/lib/stories/bridge_libraries/flame_forge2d/joints/constant_volume_joint.dart index c3e43dbdf..32fc18ca9 100644 --- a/examples/lib/stories/bridge_libraries/forge2d/joints/constant_volume_joint.dart +++ b/examples/lib/stories/bridge_libraries/flame_forge2d/joints/constant_volume_joint.dart @@ -1,26 +1,32 @@ import 'dart:math'; -import 'package:examples/stories/bridge_libraries/forge2d/utils/balls.dart'; -import 'package:examples/stories/bridge_libraries/forge2d/utils/boundaries.dart'; -import 'package:flame/input.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart'; +import 'package:flame/events.dart'; +import 'package:flame/experimental.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; -class ConstantVolumeJointExample extends Forge2DGame with TapDetector { +class ConstantVolumeJointExample extends Forge2DGame { 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. '''; + ConstantVolumeJointExample() : super(world: SpriteBodyWorld()); +} + +class SpriteBodyWorld extends Forge2DWorld + with TapCallbacks, HasGameReference { @override Future onLoad() async { super.onLoad(); - addAll(createBoundaries(this)); + addAll(createBoundaries(game)); } @override - Future onTapDown(TapDownInfo info) async { + Future onTapDown(TapDownEvent info) async { super.onTapDown(info); - final center = info.eventPosition.game; + final center = info.localPosition; const numPieces = 20; const radius = 5.0; @@ -38,10 +44,6 @@ class ConstantVolumeJointExample extends Forge2DGame with TapDetector { await Future.wait(balls.map((e) => e.loaded)); - createJoint(balls); - } - - void createJoint(List balls) { final constantVolumeJoint = ConstantVolumeJointDef() ..frequencyHz = 10 ..dampingRatio = 0.8; @@ -50,6 +52,11 @@ class ConstantVolumeJointExample extends Forge2DGame with TapDetector { constantVolumeJoint.addBody(ball.body); }); - world.createJoint(ConstantVolumeJoint(world, constantVolumeJoint)); + createJoint( + ConstantVolumeJoint( + physicsWorld, + constantVolumeJoint, + ), + ); } } diff --git a/examples/lib/stories/bridge_libraries/forge2d/joints/distance_joint.dart b/examples/lib/stories/bridge_libraries/flame_forge2d/joints/distance_joint.dart similarity index 54% rename from examples/lib/stories/bridge_libraries/forge2d/joints/distance_joint.dart rename to examples/lib/stories/bridge_libraries/flame_forge2d/joints/distance_joint.dart index 3bf71ae31..9b0b2c0f8 100644 --- a/examples/lib/stories/bridge_libraries/forge2d/joints/distance_joint.dart +++ b/examples/lib/stories/bridge_libraries/flame_forge2d/joints/distance_joint.dart @@ -1,24 +1,30 @@ -import 'package:examples/stories/bridge_libraries/forge2d/utils/balls.dart'; -import 'package:examples/stories/bridge_libraries/forge2d/utils/boundaries.dart'; -import 'package:flame/input.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart'; +import 'package:flame/events.dart'; +import 'package:flame/experimental.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; -class DistanceJointExample extends Forge2DGame with TapDetector { +class DistanceJointExample extends Forge2DGame { static const description = ''' This example shows how to use a `DistanceJoint`. Tap the screen to add a pair of balls joined with a `DistanceJoint`. '''; + DistanceJointExample() : super(world: DistanceJointWorld()); +} + +class DistanceJointWorld extends Forge2DWorld + with TapCallbacks, HasGameReference { @override Future onLoad() async { super.onLoad(); - addAll(createBoundaries(this)); + addAll(createBoundaries(game)); } @override - Future onTapDown(TapDownInfo info) async { + Future onTapDown(TapDownEvent info) async { super.onTapDown(info); - final tap = info.eventPosition.game; + final tap = info.localPosition; final first = Ball(tap); final second = Ball(Vector2(tap.x + 3, tap.y + 3)); @@ -26,10 +32,6 @@ class DistanceJointExample extends Forge2DGame with TapDetector { await Future.wait([first.loaded, second.loaded]); - createJoint(first, second); - } - - void createJoint(Ball first, Ball second) { final distanceJointDef = DistanceJointDef() ..initialize( first.body, @@ -41,6 +43,6 @@ class DistanceJointExample extends Forge2DGame with TapDetector { ..frequencyHz = 3 ..dampingRatio = 0.2; - world.createJoint(DistanceJoint(distanceJointDef)); + createJoint(DistanceJoint(distanceJointDef)); } } diff --git a/examples/lib/stories/bridge_libraries/flame_forge2d/joints/friction_joint.dart b/examples/lib/stories/bridge_libraries/flame_forge2d/joints/friction_joint.dart new file mode 100644 index 000000000..bca991fa3 --- /dev/null +++ b/examples/lib/stories/bridge_libraries/flame_forge2d/joints/friction_joint.dart @@ -0,0 +1,52 @@ +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart'; +import 'package:flame/events.dart'; +import 'package:flame/experimental.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; + +class FrictionJointExample extends Forge2DGame { + 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), world: FrictionJointWorld()); +} + +class FrictionJointWorld extends Forge2DWorld + with TapCallbacks, HasGameReference { + late Wall border; + late Ball ball; + + @override + Future onLoad() async { + super.onLoad(); + final boundaries = createBoundaries(game); + border = boundaries.first; + addAll(boundaries); + + ball = Ball(Vector2.zero(), radius: 3); + add(ball); + + await Future.wait([ball.loaded, border.loaded]); + + createFrictionJoint(ball.body, border.body); + } + + @override + Future onTapDown(TapDownEvent info) async { + super.onTapDown(info); + ball.body.applyLinearImpulse(Vector2.random() * 5000); + } + + void createFrictionJoint(Body first, Body second) { + final frictionJointDef = FrictionJointDef() + ..initialize(first, second, first.worldCenter) + ..collideConnected = true + ..maxForce = 500 + ..maxTorque = 500; + + createJoint(FrictionJoint(frictionJointDef)); + } +} diff --git a/examples/lib/stories/bridge_libraries/forge2d/joints/gear_joint.dart b/examples/lib/stories/bridge_libraries/flame_forge2d/joints/gear_joint.dart similarity index 66% rename from examples/lib/stories/bridge_libraries/forge2d/joints/gear_joint.dart rename to examples/lib/stories/bridge_libraries/flame_forge2d/joints/gear_joint.dart index ce7470a73..e93009294 100644 --- a/examples/lib/stories/bridge_libraries/forge2d/joints/gear_joint.dart +++ b/examples/lib/stories/bridge_libraries/flame_forge2d/joints/gear_joint.dart @@ -1,22 +1,25 @@ 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:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boxes.dart'; +import 'package:flame/components.dart'; +import 'package:flame/experimental.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; -// ignore: deprecated_member_use -class GearJointExample extends Forge2DGame with TapDetector, HasDraggables { +class GearJointExample extends Forge2DGame { static const description = ''' This example shows how to use a `GearJoint`. Drag the box along the specified axis and observe gears respond to the - translation + translation. '''; + GearJointExample() : super(world: GearJointWorld()); +} + +class GearJointWorld extends Forge2DWorld with HasGameReference { late PrismaticJoint prismaticJoint; - late Vector2 boxAnchor = size / 2; + Vector2 boxAnchor = Vector2.zero(); double boxWidth = 2; double ball1Radius = 4; @@ -46,10 +49,11 @@ class GearJointExample extends Forge2DGame with TapDetector, HasDraggables { createGearJoint(prismaticJoint, revoluteJoint1, 1); createGearJoint(revoluteJoint1, revoluteJoint2, 0.5); + add(JointRenderer(joint: prismaticJoint, anchor: boxAnchor)); } PrismaticJoint createPrismaticJoint(Body box, Vector2 anchor) { - final groundBody = world.createBody(BodyDef()); + final groundBody = createBody(BodyDef()); final prismaticJointDef = PrismaticJointDef() ..initialize( @@ -63,12 +67,12 @@ class GearJointExample extends Forge2DGame with TapDetector, HasDraggables { ..upperTranslation = 10; final joint = PrismaticJoint(prismaticJointDef); - world.createJoint(joint); + createJoint(joint); return joint; } RevoluteJoint createRevoluteJoint(Body ball, Vector2 anchor) { - final groundBody = world.createBody(BodyDef()); + final groundBody = createBody(BodyDef()); final revoluteJointDef = RevoluteJointDef() ..initialize( @@ -78,7 +82,7 @@ class GearJointExample extends Forge2DGame with TapDetector, HasDraggables { ); final joint = RevoluteJoint(revoluteJointDef); - world.createJoint(joint); + createJoint(joint); return joint; } @@ -91,21 +95,28 @@ class GearJointExample extends Forge2DGame with TapDetector, HasDraggables { ..ratio = gearRatio; final joint = GearJoint(gearJointDef); - world.createJoint(joint); + createJoint(joint); } +} + +class JointRenderer extends Component { + JointRenderer({required this.joint, required this.anchor}); + + final PrismaticJoint joint; + final Vector2 anchor; + final Vector2 p1 = Vector2.zero(); + final Vector2 p2 = Vector2.zero(); @override void render(Canvas canvas) { - super.render(canvas); - - final p1 = worldToScreen( - boxAnchor + - prismaticJoint.getLocalAxisA() * prismaticJoint.getLowerLimit(), - ); - final p2 = worldToScreen( - boxAnchor + - prismaticJoint.getLocalAxisA() * prismaticJoint.getUpperLimit(), - ); + p1 + ..setFrom(joint.getLocalAxisA()) + ..scale(joint.getLowerLimit()) + ..add(anchor); + p2 + ..setFrom(joint.getLocalAxisA()) + ..scale(joint.getUpperLimit()) + ..add(anchor); canvas.drawLine(p1.toOffset(), p2.toOffset(), debugPaint); } diff --git a/examples/lib/stories/bridge_libraries/forge2d/joints/motor_joint.dart b/examples/lib/stories/bridge_libraries/flame_forge2d/joints/motor_joint.dart similarity index 58% rename from examples/lib/stories/bridge_libraries/forge2d/joints/motor_joint.dart rename to examples/lib/stories/bridge_libraries/flame_forge2d/joints/motor_joint.dart index d67eca405..64a214561 100644 --- a/examples/lib/stories/bridge_libraries/forge2d/joints/motor_joint.dart +++ b/examples/lib/stories/bridge_libraries/flame_forge2d/joints/motor_joint.dart @@ -1,53 +1,56 @@ 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:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boxes.dart'; +import 'package:flame/components.dart'; import 'package:flame/events.dart'; -import 'package:flame/input.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; -// ignore: deprecated_member_use -class MotorJointExample extends Forge2DGame with TapDetector, HasDraggables { +class MotorJointExample extends Forge2DGame { 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. '''; + MotorJointExample() + : super(gravity: Vector2.zero(), world: MotorJointWorld()); +} + +class MotorJointWorld extends Forge2DWorld with TapCallbacks { 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( - startPosition: size / 2, + startPosition: Vector2.zero(), width: 2, height: 1, bodyType: BodyType.static, ); add(box); - ball = Ball(Vector2(size.x / 2, size.y / 2 - 5)); + ball = Ball(Vector2(0, -5)); add(ball); await Future.wait([ball.loaded, box.loaded]); - joint = createJoint(ball.body, box.body); + joint = createMotorJoint(ball.body, box.body); + add(JointRenderer(joint: joint)); } @override - Future onTapDown(TapDownInfo info) async { + void onTapDown(TapDownEvent info) { super.onTapDown(info); clockWise = !clockWise; } - MotorJoint createJoint(Body first, Body second) { + MotorJoint createMotorJoint(Body first, Body second) { final motorJointDef = MotorJointDef() ..initialize(first, second) ..maxForce = 1000 @@ -55,10 +58,12 @@ class MotorJointExample extends Forge2DGame with TapDetector, HasDraggables { ..correctionFactor = 0.1; final joint = MotorJoint(motorJointDef); - world.createJoint(joint); + createJoint(joint); return joint; } + final linearOffset = Vector2.zero(); + @override void update(double dt) { super.update(dt); @@ -70,19 +75,25 @@ class MotorJointExample extends Forge2DGame with TapDetector, HasDraggables { final linearOffsetX = joint.getLinearOffset().x + deltaOffset; final linearOffsetY = joint.getLinearOffset().y + deltaOffset; - final linearOffset = Vector2(linearOffsetX, linearOffsetY); + linearOffset.setValues(linearOffsetX, linearOffsetY); final angularOffset = joint.getAngularOffset() + deltaOffset; joint.setLinearOffset(linearOffset); joint.setAngularOffset(angularOffset); } +} + +class JointRenderer extends Component { + JointRenderer({required this.joint}); + + final MotorJoint joint; @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); + canvas.drawLine( + joint.anchorA.toOffset(), + joint.anchorB.toOffset(), + debugPaint, + ); } } diff --git a/examples/lib/stories/bridge_libraries/flame_forge2d/joints/mouse_joint.dart b/examples/lib/stories/bridge_libraries/flame_forge2d/joints/mouse_joint.dart new file mode 100644 index 000000000..06d23cc33 --- /dev/null +++ b/examples/lib/stories/bridge_libraries/flame_forge2d/joints/mouse_joint.dart @@ -0,0 +1,67 @@ +import 'package:examples/stories/bridge_libraries/flame_forge2d/revolute_joint_with_motor_example.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart'; +import 'package:flame/events.dart'; +import 'package:flame/experimental.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; + +class MouseJointExample extends Forge2DGame { + static const description = ''' + In this example we use a `MouseJoint` to make the ball follow the mouse + when you drag it around. + '''; + + MouseJointExample() + : super(gravity: Vector2(0, 10.0), world: MouseJointWorld()); +} + +class MouseJointWorld extends Forge2DWorld + with DragCallbacks, HasGameReference { + late Ball ball; + late Body groundBody; + MouseJoint? mouseJoint; + + @override + Future onLoad() async { + super.onLoad(); + final boundaries = createBoundaries(game); + addAll(boundaries); + + final center = Vector2.zero(); + groundBody = createBody(BodyDef()); + ball = Ball(center, radius: 5); + add(ball); + add(CornerRamp(center)); + add(CornerRamp(center, isMirrored: true)); + } + + @override + void onDragStart(DragStartEvent info) { + super.onDragStart(info); + final mouseJointDef = MouseJointDef() + ..maxForce = 3000 * ball.body.mass * 10 + ..dampingRatio = 0.1 + ..frequencyHz = 5 + ..target.setFrom(ball.body.position) + ..collideConnected = false + ..bodyA = groundBody + ..bodyB = ball.body; + + if (mouseJoint == null) { + mouseJoint = MouseJoint(mouseJointDef); + createJoint(mouseJoint!); + } + } + + @override + void onDragUpdate(DragUpdateEvent info) { + mouseJoint?.setTarget(info.localPosition); + } + + @override + void onDragEnd(DragEndEvent info) { + super.onDragEnd(info); + destroyJoint(mouseJoint!); + mouseJoint = null; + } +} diff --git a/examples/lib/stories/bridge_libraries/flame_forge2d/joints/prismatic_joint.dart b/examples/lib/stories/bridge_libraries/flame_forge2d/joints/prismatic_joint.dart new file mode 100644 index 000000000..823223454 --- /dev/null +++ b/examples/lib/stories/bridge_libraries/flame_forge2d/joints/prismatic_joint.dart @@ -0,0 +1,73 @@ +import 'dart:ui'; + +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boxes.dart'; +import 'package:flame/components.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; + +class PrismaticJointExample extends Forge2DGame { + static const description = ''' + This example shows how to use a `PrismaticJoint`. + + Drag the box along the specified axis, bound between lower and upper limits. + Also, there's a motor enabled that's pulling the box to the lower limit. + '''; + + final Vector2 anchor = Vector2.zero(); + + @override + Future onLoad() async { + super.onLoad(); + + final box = DraggableBox(startPosition: anchor, width: 6, height: 6); + world.add(box); + await Future.wait([box.loaded]); + + final joint = createJoint(box.body, anchor); + world.add(JointRenderer(joint: joint, anchor: anchor)); + } + + PrismaticJoint createJoint(Body box, Vector2 anchor) { + final groundBody = world.createBody(BodyDef()); + + final prismaticJointDef = PrismaticJointDef() + ..initialize( + box, + groundBody, + anchor, + Vector2(1, 0), + ) + ..enableLimit = true + ..lowerTranslation = -20 + ..upperTranslation = 20 + ..enableMotor = true + ..motorSpeed = 1 + ..maxMotorForce = 100; + + final joint = PrismaticJoint(prismaticJointDef); + world.createJoint(joint); + return joint; + } +} + +class JointRenderer extends Component { + JointRenderer({required this.joint, required this.anchor}); + + final PrismaticJoint joint; + final Vector2 anchor; + final Vector2 p1 = Vector2.zero(); + final Vector2 p2 = Vector2.zero(); + + @override + void render(Canvas canvas) { + p1 + ..setFrom(joint.getLocalAxisA()) + ..scale(joint.getLowerLimit()) + ..add(anchor); + p2 + ..setFrom(joint.getLocalAxisA()) + ..scale(joint.getUpperLimit()) + ..add(anchor); + + canvas.drawLine(p1.toOffset(), p2.toOffset(), debugPaint); + } +} diff --git a/examples/lib/stories/bridge_libraries/flame_forge2d/joints/pulley_joint.dart b/examples/lib/stories/bridge_libraries/flame_forge2d/joints/pulley_joint.dart new file mode 100644 index 000000000..56c9771fe --- /dev/null +++ b/examples/lib/stories/bridge_libraries/flame_forge2d/joints/pulley_joint.dart @@ -0,0 +1,98 @@ +import 'dart:ui'; + +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boxes.dart'; +import 'package:flame/components.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; + +class PulleyJointExample extends Forge2DGame { + 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 + '''; + + @override + Future onLoad() async { + super.onLoad(); + final distanceFromCenter = cameraComponent.visibleWorldRect.width / 5; + + final firstPulley = Ball( + Vector2(-distanceFromCenter, -10), + bodyType: BodyType.static, + ); + final secondPulley = Ball( + Vector2(distanceFromCenter, -10), + bodyType: BodyType.static, + ); + + final firstBox = DraggableBox( + startPosition: Vector2(-distanceFromCenter, 20), + width: 5, + height: 10, + ); + final secondBox = DraggableBox( + startPosition: Vector2(distanceFromCenter, 20), + width: 7, + height: 10, + ); + world.addAll([firstBox, secondBox, firstPulley, secondPulley]); + + await Future.wait([ + firstBox.loaded, + secondBox.loaded, + firstPulley.loaded, + secondPulley.loaded, + ]); + + final joint = createJoint(firstBox, secondBox, firstPulley, secondPulley); + world.add(PulleyRenderer(joint: joint)); + } + + PulleyJoint createJoint( + Box firstBox, + Box secondBox, + Ball firstPulley, + Ball secondPulley, + ) { + final pulleyJointDef = PulleyJointDef() + ..initialize( + firstBox.body, + secondBox.body, + firstPulley.center, + secondPulley.center, + firstBox.body.worldPoint(Vector2(0, -firstBox.height / 2)), + secondBox.body.worldPoint(Vector2(0, -secondBox.height / 2)), + 1, + ); + final joint = PulleyJoint(pulleyJointDef); + world.createJoint(joint); + return joint; + } +} + +class PulleyRenderer extends Component { + PulleyRenderer({required this.joint}); + + final PulleyJoint joint; + + @override + void render(Canvas canvas) { + canvas.drawLine( + joint.anchorA.toOffset(), + joint.getGroundAnchorA().toOffset(), + debugPaint, + ); + + canvas.drawLine( + joint.anchorB.toOffset(), + joint.getGroundAnchorB().toOffset(), + debugPaint, + ); + + canvas.drawLine( + joint.getGroundAnchorA().toOffset(), + joint.getGroundAnchorB().toOffset(), + debugPaint, + ); + } +} diff --git a/examples/lib/stories/bridge_libraries/forge2d/joints/revolute_joint.dart b/examples/lib/stories/bridge_libraries/flame_forge2d/joints/revolute_joint.dart similarity index 68% rename from examples/lib/stories/bridge_libraries/forge2d/joints/revolute_joint.dart rename to examples/lib/stories/bridge_libraries/flame_forge2d/joints/revolute_joint.dart index 5a5a8825f..88efd1d44 100644 --- a/examples/lib/stories/bridge_libraries/forge2d/joints/revolute_joint.dart +++ b/examples/lib/stories/bridge_libraries/flame_forge2d/joints/revolute_joint.dart @@ -1,11 +1,12 @@ import 'dart:math'; -import 'package:examples/stories/bridge_libraries/forge2d/utils/balls.dart'; -import 'package:examples/stories/bridge_libraries/forge2d/utils/boundaries.dart'; -import 'package:flame/input.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart'; +import 'package:flame/events.dart'; +import 'package:flame/experimental.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; -class RevoluteJointExample extends Forge2DGame with TapDetector { +class RevoluteJointExample extends Forge2DGame { static const description = ''' In this example we use a joint to keep a body with several fixtures stuck to another body. @@ -13,17 +14,22 @@ class RevoluteJointExample extends Forge2DGame with TapDetector { Tap the screen to add more of these combined bodies. '''; - RevoluteJointExample() : super(gravity: Vector2(0, 10.0)); + RevoluteJointExample() + : super(gravity: Vector2(0, 10.0), world: RevoluteJointWorld()); +} +class RevoluteJointWorld extends Forge2DWorld + with TapCallbacks, HasGameReference { @override Future onLoad() async { - addAll(createBoundaries(this)); + super.onLoad(); + addAll(createBoundaries(game)); } @override - void onTapDown(TapDownInfo info) { + void onTapDown(TapDownEvent info) { super.onTapDown(info); - final ball = Ball(info.eventPosition.game); + final ball = Ball(info.localPosition); add(ball); add(CircleShuffler(ball)); } diff --git a/examples/lib/stories/bridge_libraries/forge2d/joints/rope_joint.dart b/examples/lib/stories/bridge_libraries/flame_forge2d/joints/rope_joint.dart similarity index 64% rename from examples/lib/stories/bridge_libraries/forge2d/joints/rope_joint.dart rename to examples/lib/stories/bridge_libraries/flame_forge2d/joints/rope_joint.dart index 9b741ccee..1860fa371 100644 --- a/examples/lib/stories/bridge_libraries/forge2d/joints/rope_joint.dart +++ b/examples/lib/stories/bridge_libraries/flame_forge2d/joints/rope_joint.dart @@ -1,19 +1,23 @@ -import 'package:examples/stories/bridge_libraries/forge2d/utils/balls.dart'; -import 'package:examples/stories/bridge_libraries/forge2d/utils/boxes.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boxes.dart'; import 'package:flame/events.dart'; -import 'package:flame/input.dart'; +import 'package:flame/experimental.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/material.dart'; -// ignore: deprecated_member_use -class RopeJointExample extends Forge2DGame with TapDetector, HasDraggables { +class RopeJointExample extends Forge2DGame { static const description = ''' This example shows how to use a `RopeJoint`. Drag the box handle along the axis and observe the rope respond to the - movement + movement. '''; + RopeJointExample() : super(world: RopeJointWorld()); +} + +class RopeJointWorld extends Forge2DWorld + with DragCallbacks, HasGameReference { double handleWidth = 6; @override @@ -25,10 +29,13 @@ class RopeJointExample extends Forge2DGame with TapDetector, HasDraggables { } Future createHandle() async { - final anchor = Vector2(size.x / 2, 5); + final anchor = game.screenToWorld(Vector2(0, 100))..x = 0; - final box = - DraggableBox(startPosition: anchor, width: handleWidth, height: 3); + final box = DraggableBox( + startPosition: anchor, + width: handleWidth, + height: 3, + ); await add(box); createPrismaticJoint(box.body, anchor); @@ -50,7 +57,8 @@ class RopeJointExample extends Forge2DGame with TapDetector, HasDraggables { } void createPrismaticJoint(Body box, Vector2 anchor) { - final groundBody = world.createBody(BodyDef()); + final groundBody = createBody(BodyDef()); + final halfWidth = game.screenToWorld(Vector2.zero()).x.abs(); final prismaticJointDef = PrismaticJointDef() ..initialize( @@ -60,11 +68,11 @@ class RopeJointExample extends Forge2DGame with TapDetector, HasDraggables { Vector2(1, 0), ) ..enableLimit = true - ..lowerTranslation = -size.x / 2 + handleWidth / 2 - ..upperTranslation = size.x / 2 - handleWidth / 2; + ..lowerTranslation = -halfWidth + handleWidth / 2 + ..upperTranslation = halfWidth - handleWidth / 2; final joint = PrismaticJoint(prismaticJointDef); - world.createJoint(joint); + createJoint(joint); } void createRopeJoint(Body first, Body second) { @@ -75,6 +83,6 @@ class RopeJointExample extends Forge2DGame with TapDetector, HasDraggables { ..localAnchorB.setFrom(second.getLocalCenter()) ..maxLength = (second.worldCenter - first.worldCenter).length; - world.createJoint(RopeJoint(ropeJointDef)); + createJoint(RopeJoint(ropeJointDef)); } } diff --git a/examples/lib/stories/bridge_libraries/flame_forge2d/joints/weld_joint.dart b/examples/lib/stories/bridge_libraries/flame_forge2d/joints/weld_joint.dart new file mode 100644 index 000000000..8c7642326 --- /dev/null +++ b/examples/lib/stories/bridge_libraries/flame_forge2d/joints/weld_joint.dart @@ -0,0 +1,102 @@ +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boxes.dart'; +import 'package:flame/events.dart'; +import 'package:flame/experimental.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/material.dart'; + +class WeldJointExample extends Forge2DGame { + static const description = ''' + This example shows how to use a `WeldJoint`. Tap the screen to add a + ball to test the bridge built using a `WeldJoint` + '''; + + WeldJointExample() : super(world: WeldJointWorld()); +} + +class WeldJointWorld extends Forge2DWorld + with TapCallbacks, HasGameReference { + final pillarHeight = 20.0; + final pillarWidth = 5.0; + + @override + Future onLoad() async { + super.onLoad(); + + final leftPillar = Box( + startPosition: game.screenToWorld(Vector2(50, game.size.y)) + ..y -= pillarHeight / 2, + width: pillarWidth, + height: pillarHeight, + bodyType: BodyType.static, + color: Colors.white, + ); + final rightPillar = Box( + startPosition: game.screenToWorld(Vector2(game.size.x - 50, game.size.y)) + ..y -= pillarHeight / 2, + width: pillarWidth, + height: pillarHeight, + bodyType: BodyType.static, + color: Colors.white, + ); + + await addAll([leftPillar, rightPillar]); + + createBridge(leftPillar, rightPillar); + } + + Future createBridge( + Box leftPillar, + Box rightPillar, + ) async { + const sectionsCount = 10; + // Vector2.zero is used here since 0,0 is in the middle and 0,0 in the + // screen space then gives us the coordinates of the upper left corner in + // world space. + final halfSize = game.screenToWorld(Vector2.zero())..absolute(); + final sectionWidth = ((leftPillar.center.x.abs() + + rightPillar.center.x.abs() + + pillarWidth) / + sectionsCount) + .ceilToDouble(); + Body? prevSection; + + for (var i = 0; i < sectionsCount; i++) { + final section = Box( + startPosition: Vector2( + sectionWidth * i - halfSize.x + sectionWidth / 2, + halfSize.y - pillarHeight, + ), + width: sectionWidth, + height: 1, + ); + await add(section); + + if (prevSection != null) { + createWeldJoint( + prevSection, + section.body, + Vector2( + sectionWidth * i - halfSize.x + sectionWidth, + halfSize.y - pillarHeight, + ), + ); + } + + prevSection = section.body; + } + } + + void createWeldJoint(Body first, Body second, Vector2 anchor) { + final weldJointDef = WeldJointDef()..initialize(first, second, anchor); + + createJoint(WeldJoint(weldJointDef)); + } + + @override + Future onTapDown(TapDownEvent info) async { + super.onTapDown(info); + final ball = Ball(info.localPosition, radius: 5); + add(ball); + } +} diff --git a/examples/lib/stories/bridge_libraries/forge2d/raycast_example.dart b/examples/lib/stories/bridge_libraries/flame_forge2d/raycast_example.dart similarity index 66% rename from examples/lib/stories/bridge_libraries/forge2d/raycast_example.dart rename to examples/lib/stories/bridge_libraries/flame_forge2d/raycast_example.dart index 2cd7476be..c2bf23666 100644 --- a/examples/lib/stories/bridge_libraries/forge2d/raycast_example.dart +++ b/examples/lib/stories/bridge_libraries/flame_forge2d/raycast_example.dart @@ -1,14 +1,15 @@ // ignore_for_file: deprecated_member_use import 'dart:math'; +import 'dart:ui'; -import 'package:examples/stories/bridge_libraries/forge2d/utils/boundaries.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart'; +import 'package:flame/components.dart'; import 'package:flame/input.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/material.dart' show Colors, Paint, Canvas; -class RaycastExample extends Forge2DGame - with TapDetector, MouseMovementDetector { +class RaycastExample extends Forge2DGame with MouseMovementDetector { static const String description = ''' This example shows how raycasts can be used to find nearest and farthest fixtures. @@ -17,8 +18,8 @@ class RaycastExample extends Forge2DGame final random = Random(); - final redPoints = List.empty(growable: true); - final bluePoints = List.empty(growable: true); + final redPoints = []; + final bluePoints = []; Box? nearestBox; Box? farthestBox; @@ -27,69 +28,86 @@ class RaycastExample extends Forge2DGame @override Future onLoad() async { - addAll(createBoundaries(this)); - - final worldCenter = screenToWorld(camera.viewport.effectiveSize / 2); + super.onLoad(); + world.addAll(createBoundaries(this)); const numberOfRows = 3; const numberOfBoxes = 4; for (var i = 0; i < numberOfBoxes; ++i) { for (var j = 0; j < numberOfRows; ++j) { - final position = worldCenter + Vector2(i * 10, j * 20 - 20); - add(Box(position)); + world.add(Box(Vector2(i * 10, j * 20 - 20))); } } + world.add( + LineComponent( + redPoints, + Paint() + ..color = Colors.red + ..strokeWidth = 1, + ), + ); + world.add( + LineComponent( + bluePoints, + Paint() + ..color = Colors.blue + ..strokeWidth = 1, + ), + ); } @override void onMouseMove(PointerHoverInfo info) { - bluePoints.clear(); - final rayStart = screenToWorld( - camera.viewport.effectiveSize / 2 - - Vector2(camera.viewport.effectiveSize.x / 4, 0), + Vector2( + cameraComponent.viewport.size.x / 4, + cameraComponent.viewport.size.y / 2, + ), ); - final redRayTarget = info.eventPosition.game + Vector2(0, 2); + final worldPosition = screenToWorld(info.eventPosition.widget); + final redRayTarget = worldPosition + Vector2(0, 2); fireRedRay(rayStart, redRayTarget); - final blueRayTarget = info.eventPosition.game - Vector2(0, 2); + final blueRayTarget = worldPosition - Vector2(0, 2); fireBlueRay(rayStart, blueRayTarget); super.onMouseMove(info); } - void fireBlueRay(Vector2 rayStart, Vector2 blueRayTarget) { - bluePoints.add(worldToScreen(rayStart)); + void fireBlueRay(Vector2 rayStart, Vector2 rayTarget) { + bluePoints.clear(); + bluePoints.add(rayStart); final farthestCallback = FarthestBoxRayCastCallback(); - world.raycast(farthestCallback, rayStart, blueRayTarget); + world.raycast(farthestCallback, rayStart, rayTarget); if (farthestCallback.farthestPoint != null) { - bluePoints.add(worldToScreen(farthestCallback.farthestPoint!)); + bluePoints.add(farthestCallback.farthestPoint!); } else { - bluePoints.add(worldToScreen(blueRayTarget)); + bluePoints.add(rayTarget); } farthestBox = farthestCallback.box; } void fireRedRay(Vector2 rayStart, Vector2 rayTarget) { redPoints.clear(); - redPoints.add(worldToScreen(rayStart)); + redPoints.add(rayStart); final nearestCallback = NearestBoxRayCastCallback(); world.raycast(nearestCallback, rayStart, rayTarget); if (nearestCallback.nearestPoint != null) { - redPoints.add(worldToScreen(nearestCallback.nearestPoint!)); + redPoints.add(nearestCallback.nearestPoint!); } else { - redPoints.add(worldToScreen(rayTarget)); + redPoints.add(rayTarget); } nearestBox = nearestCallback.box; } @override void update(double dt) { + super.update(dt); children.whereType().forEach((component) { if ((component == nearestBox) && (component == farthestBox)) { component.paint.color = Colors.yellow; @@ -101,45 +119,48 @@ class RaycastExample extends Forge2DGame component.paint.color = Colors.white; } }); - super.update(dt); + } +} + +class LineComponent extends Component { + LineComponent(this.points, this.paint); + + final List points; + final Paint paint; + final Path path = Path(); + + @override + void update(double dt) { + path + ..reset() + ..addPolygon( + points.map((p) => p.toOffset()).toList(growable: false), + false, + ); } @override void render(Canvas canvas) { - super.render(canvas); - - for (var i = 0; i < redPoints.length - 1; ++i) { + for (var i = 0; i < points.length - 1; ++i) { canvas.drawLine( - redPoints[i].toOffset(), - redPoints[i + 1].toOffset(), - Paint() - ..color = Colors.red - ..strokeWidth = 4, - ); - } - - for (var i = 0; i < bluePoints.length - 1; ++i) { - canvas.drawLine( - bluePoints[i].toOffset(), - bluePoints[i + 1].toOffset(), - Paint() - ..color = Colors.blue - ..strokeWidth = 4, + points[i].toOffset(), + points[i + 1].toOffset(), + paint, ); } } } class Box extends BodyComponent { - final Vector2 position; + Box(this.initialPosition); - Box(this.position); + final Vector2 initialPosition; @override Body createBody() { final shape = PolygonShape()..setAsBoxXY(2.0, 4.0); final fixtureDef = FixtureDef(shape, userData: this); - final bodyDef = BodyDef(position: position); + final bodyDef = BodyDef(position: initialPosition); return world.createBody(bodyDef)..createFixture(fixtureDef); } } diff --git a/examples/lib/stories/bridge_libraries/forge2d/revolute_joint_with_motor_example.dart b/examples/lib/stories/bridge_libraries/flame_forge2d/revolute_joint_with_motor_example.dart similarity index 78% rename from examples/lib/stories/bridge_libraries/forge2d/revolute_joint_with_motor_example.dart rename to examples/lib/stories/bridge_libraries/flame_forge2d/revolute_joint_with_motor_example.dart index 4b3462137..3a179107c 100644 --- a/examples/lib/stories/bridge_libraries/forge2d/revolute_joint_with_motor_example.dart +++ b/examples/lib/stories/bridge_libraries/flame_forge2d/revolute_joint_with_motor_example.dart @@ -2,12 +2,13 @@ import 'dart:math'; -import 'package:examples/stories/bridge_libraries/forge2d/utils/balls.dart'; -import 'package:examples/stories/bridge_libraries/forge2d/utils/boundaries.dart'; -import 'package:flame/input.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart'; +import 'package:flame/events.dart'; +import 'package:flame/experimental.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; -class RevoluteJointWithMotorExample extends Forge2DGame with TapDetector { +class RevoluteJointWithMotorExample extends Forge2DGame { static const String description = ''' This example showcases a revolute joint, which is the spinning balls in the center. @@ -17,21 +18,28 @@ class RevoluteJointWithMotorExample extends Forge2DGame with TapDetector { down the funnel. '''; + RevoluteJointWithMotorExample() : super(world: RevoluteJointWithMotorWorld()); +} + +class RevoluteJointWithMotorWorld extends Forge2DWorld + with TapCallbacks, HasGameReference { + final random = Random(); + @override Future onLoad() async { - final boundaries = createBoundaries(this); - boundaries.forEach(add); - final center = screenToWorld(camera.viewport.effectiveSize / 2); + super.onLoad(); + final boundaries = createBoundaries(game); + addAll(boundaries); + final center = Vector2.zero(); add(CircleShuffler(center)); add(CornerRamp(center, isMirrored: true)); add(CornerRamp(center)); } @override - void onTapDown(TapDownInfo info) { + void onTapDown(TapDownEvent info) { super.onTapDown(info); - final tapPosition = info.eventPosition.game; - final random = Random(); + final tapPosition = info.localPosition; List.generate(15, (i) { final randomVector = (Vector2.random() - Vector2.all(-0.5)).normalized(); add(Ball(tapPosition + randomVector, radius: random.nextDouble())); diff --git a/examples/lib/stories/bridge_libraries/forge2d/sprite_body_example.dart b/examples/lib/stories/bridge_libraries/flame_forge2d/sprite_body_example.dart similarity index 63% rename from examples/lib/stories/bridge_libraries/forge2d/sprite_body_example.dart rename to examples/lib/stories/bridge_libraries/flame_forge2d/sprite_body_example.dart index a1a0a7577..4bba53af8 100644 --- a/examples/lib/stories/bridge_libraries/forge2d/sprite_body_example.dart +++ b/examples/lib/stories/bridge_libraries/flame_forge2d/sprite_body_example.dart @@ -1,44 +1,53 @@ import 'dart:math'; -import 'package:examples/stories/bridge_libraries/forge2d/utils/boundaries.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart'; import 'package:flame/components.dart'; -import 'package:flame/input.dart'; +import 'package:flame/events.dart'; +import 'package:flame/experimental.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; -class SpriteBodyExample extends Forge2DGame with TapDetector { +class SpriteBodyExample extends Forge2DGame { static const String description = ''' In this example we show how to add a sprite on top of a `BodyComponent`. Tap the screen to add more pizzas. '''; - SpriteBodyExample() : super(gravity: Vector2(0, 10.0)); + SpriteBodyExample() + : super( + gravity: Vector2(0, 10.0), + world: SpriteBodyWorld(), + ); +} +class SpriteBodyWorld extends Forge2DWorld + with TapCallbacks, HasGameReference { @override Future onLoad() async { - addAll(createBoundaries(this)); + super.onLoad(); + addAll(createBoundaries(game)); } @override - void onTapDown(TapDownInfo info) { + void onTapDown(TapDownEvent info) { super.onTapDown(info); - final position = info.eventPosition.game; + final position = info.localPosition; add(Pizza(position, size: Vector2(10, 15))); } } class Pizza extends BodyComponent { - final Vector2 position; + final Vector2 initialPosition; final Vector2 size; Pizza( - this.position, { + this.initialPosition, { Vector2? size, }) : size = size ?? Vector2(2, 3); @override Future onLoad() async { await super.onLoad(); - final sprite = await gameRef.loadSprite('pizza.png'); + final sprite = await game.loadSprite('pizza.png'); renderBody = false; add( SpriteComponent( @@ -69,8 +78,8 @@ class Pizza extends BodyComponent { ); final bodyDef = BodyDef( - position: position, - angle: (position.x + position.y) / 2 * pi, + position: initialPosition, + angle: (initialPosition.x + initialPosition.y) / 2 * pi, type: BodyType.dynamic, ); return world.createBody(bodyDef)..createFixture(fixtureDef); diff --git a/examples/lib/stories/bridge_libraries/forge2d/tap_callbacks_example.dart b/examples/lib/stories/bridge_libraries/flame_forge2d/tap_callbacks_example.dart similarity index 63% rename from examples/lib/stories/bridge_libraries/forge2d/tap_callbacks_example.dart rename to examples/lib/stories/bridge_libraries/flame_forge2d/tap_callbacks_example.dart index df07bd8dd..96831ba76 100644 --- a/examples/lib/stories/bridge_libraries/forge2d/tap_callbacks_example.dart +++ b/examples/lib/stories/bridge_libraries/flame_forge2d/tap_callbacks_example.dart @@ -1,26 +1,24 @@ -// ignore_for_file: deprecated_member_use - -import 'package:examples/stories/bridge_libraries/forge2d/utils/balls.dart'; -import 'package:examples/stories/bridge_libraries/forge2d/utils/boundaries.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart'; import 'package:flame/events.dart'; import 'package:flame/palette.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; -class TappableExample extends Forge2DGame { +class TapCallbacksExample extends Forge2DGame { static const String description = ''' In this example we show how to use Flame's TapCallbacks mixin to react to taps on `BodyComponent`s. Tap the ball to give it a random impulse, or the text to add an effect to it. '''; - TappableExample() : super(zoom: 20, gravity: Vector2(0, 10.0)); + TapCallbacksExample() : super(zoom: 20, gravity: Vector2(0, 10.0)); @override Future onLoad() async { + super.onLoad(); final boundaries = createBoundaries(this); - boundaries.forEach(add); - final center = screenToWorld(camera.viewport.effectiveSize / 2); - add(TappableBall(center)); + world.addAll(boundaries); + world.add(TappableBall(Vector2.zero())); } } diff --git a/examples/lib/stories/bridge_libraries/forge2d/utils/balls.dart b/examples/lib/stories/bridge_libraries/flame_forge2d/utils/balls.dart similarity index 92% rename from examples/lib/stories/bridge_libraries/forge2d/utils/balls.dart rename to examples/lib/stories/bridge_libraries/flame_forge2d/utils/balls.dart index 172b72b83..0719aa22f 100644 --- a/examples/lib/stories/bridge_libraries/forge2d/utils/balls.dart +++ b/examples/lib/stories/bridge_libraries/flame_forge2d/utils/balls.dart @@ -1,4 +1,4 @@ -import 'package:examples/stories/bridge_libraries/forge2d/utils/boundaries.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart'; import 'package:flame/palette.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/material.dart'; @@ -59,6 +59,8 @@ class Ball extends BodyComponent with ContactCallbacks { canvas.drawLine(center, center + lineRotation, _blue); } + final _impulseForce = Vector2(0, 1000); + @override @mustCallSuper void update(double dt) { @@ -66,7 +68,7 @@ class Ball extends BodyComponent with ContactCallbacks { if (giveNudge) { giveNudge = false; if (_timeSinceNudge > _minNudgeRest) { - body.applyLinearImpulse(Vector2(0, 1000)); + body.applyLinearImpulse(_impulseForce); _timeSinceNudge = 0.0; } } diff --git a/examples/lib/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart b/examples/lib/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart new file mode 100644 index 000000000..b369d4a90 --- /dev/null +++ b/examples/lib/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart @@ -0,0 +1,39 @@ +import 'package:flame/extensions.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; + +List createBoundaries(Forge2DGame game, {double? strokeWidth}) { + final visibleRect = game.cameraComponent.visibleWorldRect; + final topLeft = visibleRect.topLeft.toVector2(); + final topRight = visibleRect.topRight.toVector2(); + final bottomRight = visibleRect.bottomRight.toVector2(); + final bottomLeft = visibleRect.bottomLeft.toVector2(); + + return [ + Wall(topLeft, topRight, strokeWidth: strokeWidth), + Wall(topRight, bottomRight, strokeWidth: strokeWidth), + Wall(bottomLeft, bottomRight, strokeWidth: strokeWidth), + Wall(topLeft, bottomLeft, strokeWidth: strokeWidth), + ]; +} + +class Wall extends BodyComponent { + final Vector2 start; + final Vector2 end; + final double strokeWidth; + + Wall(this.start, this.end, {double? strokeWidth}) + : strokeWidth = strokeWidth ?? 1; + + @override + Body createBody() { + final shape = EdgeShape()..set(start, end); + final fixtureDef = FixtureDef(shape, friction: 0.3); + final bodyDef = BodyDef( + userData: this, // To be able to determine object in collision + position: Vector2.zero(), + ); + paint.strokeWidth = strokeWidth; + + return world.createBody(bodyDef)..createFixture(fixtureDef); + } +} diff --git a/examples/lib/stories/bridge_libraries/forge2d/utils/boxes.dart b/examples/lib/stories/bridge_libraries/flame_forge2d/utils/boxes.dart similarity index 71% rename from examples/lib/stories/bridge_libraries/forge2d/utils/boxes.dart rename to examples/lib/stories/bridge_libraries/flame_forge2d/utils/boxes.dart index ab1c2f4bf..6ded867e6 100644 --- a/examples/lib/stories/bridge_libraries/forge2d/utils/boxes.dart +++ b/examples/lib/stories/bridge_libraries/flame_forge2d/utils/boxes.dart @@ -1,6 +1,5 @@ import 'dart:ui'; -import 'package:flame/components.dart'; import 'package:flame/events.dart'; import 'package:flame/extensions.dart'; import 'package:flame/palette.dart'; @@ -43,10 +42,10 @@ class Box extends BodyComponent { } } -// ignore: deprecated_member_use -class DraggableBox extends Box with Draggable { +class DraggableBox extends Box with DragCallbacks { MouseJoint? mouseJoint; late final groundBody = world.createBody(BodyDef()); + bool _destroyJoint = false; DraggableBox({ required super.startPosition, @@ -55,12 +54,25 @@ class DraggableBox extends Box with Draggable { }); @override - bool onDragUpdate(DragUpdateInfo info) { + void update(double dt) { + if (_destroyJoint && mouseJoint != null) { + world.destroyJoint(mouseJoint!); + mouseJoint = null; + _destroyJoint = false; + } + } + + @override + bool onDragUpdate(DragUpdateEvent info) { + final target = info.localPosition; + if (target.isNaN) { + return false; + } final mouseJointDef = MouseJointDef() - ..maxForce = 3000 * body.mass * 10 + ..maxForce = body.mass * 300 ..dampingRatio = 0 ..frequencyHz = 20 - ..target.setFrom(info.eventPosition.game) + ..target.setFrom(body.position) ..collideConnected = false ..bodyA = groundBody ..bodyB = body; @@ -68,19 +80,19 @@ class DraggableBox extends Box with Draggable { if (mouseJoint == null) { mouseJoint = MouseJoint(mouseJointDef); world.createJoint(mouseJoint!); + } else { + mouseJoint?.setTarget(target); } - - mouseJoint?.setTarget(info.eventPosition.game); return false; } @override - bool onDragEnd(DragEndInfo info) { + void onDragEnd(DragEndEvent info) { + super.onDragEnd(info); if (mouseJoint == null) { - return true; + return; } - world.destroyJoint(mouseJoint!); - mouseJoint = null; - return false; + _destroyJoint = true; + info.continuePropagation = false; } } diff --git a/examples/lib/stories/bridge_libraries/forge2d/widget_example.dart b/examples/lib/stories/bridge_libraries/flame_forge2d/widget_example.dart similarity index 75% rename from examples/lib/stories/bridge_libraries/forge2d/widget_example.dart rename to examples/lib/stories/bridge_libraries/flame_forge2d/widget_example.dart index 07eef24a0..13e1e166e 100644 --- a/examples/lib/stories/bridge_libraries/forge2d/widget_example.dart +++ b/examples/lib/stories/bridge_libraries/flame_forge2d/widget_example.dart @@ -1,37 +1,33 @@ // ignore_for_file: deprecated_member_use -import 'package:examples/stories/bridge_libraries/forge2d/utils/boundaries.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart'; import 'package:flame/game.dart'; -import 'package:flame/input.dart'; import 'package:flame_forge2d/flame_forge2d.dart' hide Transform; import 'package:flutter/material.dart'; -class WidgetExample extends Forge2DGame with TapDetector { +class WidgetExample extends Forge2DGame { static const String description = ''' This examples shows how to render a widget on top of a Forge2D body outside of Flame. '''; - List updateStates = []; - Map bodyIdMap = {}; - List addLaterIds = []; - - Vector2 screenPosition(Body body) => worldToScreen(body.worldCenter); + final List updateStates = []; + final Map bodyIdMap = {}; + final List addLaterIds = []; WidgetExample() : super(zoom: 20, gravity: Vector2(0, 10.0)); @override Future onLoad() async { - final boundaries = createBoundaries(this); - addAll(boundaries); + super.onLoad(); + final boundaries = createBoundaries(this, strokeWidth: 0); + world.addAll(boundaries); } Body createBody() { final bodyDef = BodyDef( angularVelocity: 3, - position: screenToWorld( - Vector2.random()..multiply(camera.viewport.effectiveSize), - ), + position: Vector2.zero(), type: BodyType.dynamic, ); final body = world.createBody(bodyDef); @@ -47,8 +43,7 @@ class WidgetExample extends Forge2DGame with TapDetector { return body; } - int createBodyId() { - final id = bodyIdMap.length + addLaterIds.length; + int createBodyId(int id) { addLaterIds.add(id); return id; } @@ -56,7 +51,11 @@ class WidgetExample extends Forge2DGame with TapDetector { @override void update(double dt) { super.update(dt); - addLaterIds.forEach((id) => bodyIdMap[id] = createBody()); + addLaterIds.forEach((id) { + if (!bodyIdMap.containsKey(id)) { + bodyIdMap[id] = createBody(); + } + }); addLaterIds.clear(); updateStates.forEach((f) => f()); } @@ -71,10 +70,10 @@ class BodyWidgetExample extends StatelessWidget { game: WidgetExample(), overlayBuilderMap: { 'button1': (ctx, game) { - return BodyButtonWidget(game, game.createBodyId()); + return BodyButtonWidget(game, game.createBodyId(1)); }, 'button2': (ctx, game) { - return BodyButtonWidget(game, game.createBodyId()); + return BodyButtonWidget(game, game.createBodyId(2)); }, }, initialActiveOverlays: const ['button1', 'button2'], @@ -117,7 +116,7 @@ class _BodyButtonState extends State { if (body == null) { return Container(); } else { - final bodyPosition = _game.screenPosition(body); + final bodyPosition = _game.worldToScreen(body.position); return Positioned( top: bodyPosition.y - 18, left: bodyPosition.x - 90, diff --git a/examples/lib/stories/bridge_libraries/forge2d/camera_example.dart b/examples/lib/stories/bridge_libraries/forge2d/camera_example.dart deleted file mode 100644 index 9a2b4509e..000000000 --- a/examples/lib/stories/bridge_libraries/forge2d/camera_example.dart +++ /dev/null @@ -1,22 +0,0 @@ -// ignore_for_file: deprecated_member_use - -import 'package:examples/stories/bridge_libraries/forge2d/domino_example.dart'; -import 'package:examples/stories/bridge_libraries/forge2d/sprite_body_example.dart'; -import 'package:flame/input.dart'; -import 'package:flame_forge2d/flame_forge2d.dart'; - -class CameraExample extends DominoExample { - static const String description = ''' - This example showcases the possibility to follow BodyComponents with the - camera. When the screen is tapped a pizza is added, which the camera will - follow. Other than that it is the same as the domino example. - '''; - - @override - void onTapDown(TapDownInfo info) { - final position = info.eventPosition.game; - final pizza = Pizza(position); - add(pizza); - pizza.mounted.whenComplete(() => camera.followBodyComponent(pizza)); - } -} diff --git a/examples/lib/stories/bridge_libraries/forge2d/contact_callbacks_example.dart b/examples/lib/stories/bridge_libraries/forge2d/contact_callbacks_example.dart deleted file mode 100644 index 422c49807..000000000 --- a/examples/lib/stories/bridge_libraries/forge2d/contact_callbacks_example.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'dart:math' as math; - -import 'package:examples/stories/bridge_libraries/forge2d/utils/balls.dart'; -import 'package:examples/stories/bridge_libraries/forge2d/utils/boundaries.dart'; -import 'package:flame/input.dart'; -import 'package:flame_forge2d/flame_forge2d.dart'; - -class ContactCallbacksExample extends Forge2DGame with TapDetector { - static const description = ''' - This example shows how `BodyComponent`s can react to collisions with other - bodies. - Tap the screen to add balls, the white balls will give an impulse to the - balls that it collides with. - '''; - - ContactCallbacksExample() : super(gravity: Vector2(0, 10.0)); - - @override - Future onLoad() async { - final boundaries = createBoundaries(this); - boundaries.forEach(add); - } - - @override - void onTapDown(TapDownInfo info) { - super.onTapDown(info); - final position = info.eventPosition.game; - if (math.Random().nextInt(10) < 2) { - add(WhiteBall(position)); - } else { - add(Ball(position)); - } - } -} diff --git a/examples/lib/stories/bridge_libraries/forge2d/draggable_example.dart b/examples/lib/stories/bridge_libraries/forge2d/draggable_example.dart deleted file mode 100644 index 859bd0ac2..000000000 --- a/examples/lib/stories/bridge_libraries/forge2d/draggable_example.dart +++ /dev/null @@ -1,52 +0,0 @@ -// ignore_for_file: deprecated_member_use - -import 'package:examples/stories/bridge_libraries/forge2d/utils/balls.dart'; -import 'package:examples/stories/bridge_libraries/forge2d/utils/boundaries.dart'; -import 'package:flame/components.dart'; -import 'package:flame/game.dart'; -import 'package:flame/input.dart'; -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flutter/material.dart' hide Draggable; - -class DraggableExample extends Forge2DGame with HasDraggables { - static const description = ''' - In this example we use Flame's normal `Draggable` mixin to give impulses to - a ball when we are dragging it around. If you are interested in dragging - bodies around, also have a look at the MouseJointExample. - '''; - - DraggableExample() : super(gravity: Vector2.all(0.0)); - - @override - Future onLoad() async { - final boundaries = createBoundaries(this); - boundaries.forEach(add); - final center = screenToWorld(camera.viewport.effectiveSize / 2); - add(DraggableBall(center)); - } -} - -class DraggableBall extends Ball with Draggable { - DraggableBall(super.position) : super(radius: 5) { - originalPaint = Paint()..color = Colors.amber; - paint = originalPaint; - } - - @override - bool onDragStart(DragStartInfo info) { - paint = randomPaint(); - return true; - } - - @override - bool onDragUpdate(DragUpdateInfo info) { - body.applyLinearImpulse(info.delta.game * 1000); - return true; - } - - @override - bool onDragEnd(DragEndInfo info) { - paint = originalPaint; - return true; - } -} diff --git a/examples/lib/stories/bridge_libraries/forge2d/joints/friction_joint.dart b/examples/lib/stories/bridge_libraries/forge2d/joints/friction_joint.dart deleted file mode 100644 index 72160cffd..000000000 --- a/examples/lib/stories/bridge_libraries/forge2d/joints/friction_joint.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:examples/stories/bridge_libraries/forge2d/utils/balls.dart'; -import 'package:examples/stories/bridge_libraries/forge2d/utils/boundaries.dart'; -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; - late Ball ball; - - @override - Future onLoad() async { - super.onLoad(); - final boundaries = createBoundaries(this); - border = boundaries.first; - addAll(boundaries); - - ball = Ball(size / 2, radius: 3); - add(ball); - - await Future.wait([ball.loaded, border.loaded]); - - createJoint(ball.body, border.body); - } - - @override - Future onTapDown(TapDownInfo info) async { - super.onTapDown(info); - ball.body.applyLinearImpulse(Vector2.random() * 5000); - } - - void createJoint(Body first, Body second) { - final frictionJointDef = FrictionJointDef() - ..initialize(first, second, first.worldCenter) - ..collideConnected = true - ..maxForce = 500 - ..maxTorque = 500; - - world.createJoint(FrictionJoint(frictionJointDef)); - } -} diff --git a/examples/lib/stories/bridge_libraries/forge2d/joints/mouse_joint.dart b/examples/lib/stories/bridge_libraries/forge2d/joints/mouse_joint.dart deleted file mode 100644 index 1191c2784..000000000 --- a/examples/lib/stories/bridge_libraries/forge2d/joints/mouse_joint.dart +++ /dev/null @@ -1,63 +0,0 @@ -// ignore_for_file: deprecated_member_use - -import 'package:examples/stories/bridge_libraries/forge2d/revolute_joint_with_motor_example.dart'; -import 'package:examples/stories/bridge_libraries/forge2d/utils/balls.dart'; -import 'package:examples/stories/bridge_libraries/forge2d/utils/boundaries.dart'; -import 'package:flame/input.dart'; -import 'package:flame_forge2d/flame_forge2d.dart'; - -class MouseJointExample extends Forge2DGame with MultiTouchDragDetector { - static const description = ''' - In this example we use a `MouseJoint` to make the ball follow the mouse - when you drag it around. - '''; - - MouseJointExample() : super(gravity: Vector2(0, 10.0)); - - late Ball ball; - late Body groundBody; - MouseJoint? mouseJoint; - - @override - Future onLoad() async { - final boundaries = createBoundaries(this); - boundaries.forEach(add); - - final center = screenToWorld(camera.viewport.effectiveSize / 2); - groundBody = world.createBody(BodyDef()); - ball = Ball(center, radius: 5); - add(ball); - add(CornerRamp(center)); - add(CornerRamp(center, isMirrored: true)); - } - - @override - bool onDragUpdate(int pointerId, DragUpdateInfo info) { - final mouseJointDef = MouseJointDef() - ..maxForce = 3000 * ball.body.mass * 10 - ..dampingRatio = 0.1 - ..frequencyHz = 5 - ..target.setFrom(ball.body.position) - ..collideConnected = false - ..bodyA = groundBody - ..bodyB = ball.body; - - if (mouseJoint == null) { - mouseJoint = MouseJoint(mouseJointDef); - world.createJoint(mouseJoint!); - } - - mouseJoint?.setTarget(info.eventPosition.game); - return false; - } - - @override - bool onDragEnd(int pointerId, DragEndInfo info) { - if (mouseJoint == null) { - return true; - } - world.destroyJoint(mouseJoint!); - mouseJoint = null; - return false; - } -} diff --git a/examples/lib/stories/bridge_libraries/forge2d/joints/prismatic_joint.dart b/examples/lib/stories/bridge_libraries/forge2d/joints/prismatic_joint.dart deleted file mode 100644 index 806a01ea2..000000000 --- a/examples/lib/stories/bridge_libraries/forge2d/joints/prismatic_joint.dart +++ /dev/null @@ -1,65 +0,0 @@ -// ignore_for_file: deprecated_member_use - -import 'dart:ui'; - -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 PrismaticJointExample extends Forge2DGame - with TapDetector, HasDraggables { - static const description = ''' - This example shows how to use a `PrismaticJoint`. - - Drag the box along the specified axis, bound between lower and upper limits. - Also, there's a motor enabled that's pulling the box to the lower limit. - '''; - - late PrismaticJoint joint; - late Vector2 anchor = size / 2; - - @override - Future onLoad() async { - super.onLoad(); - - final box = DraggableBox(startPosition: anchor, width: 3, height: 3); - add(box); - await Future.wait([box.loaded]); - - createJoint(box.body, anchor); - } - - void createJoint(Body box, Vector2 anchor) { - final groundBody = world.createBody(BodyDef()); - - final prismaticJointDef = PrismaticJointDef() - ..initialize( - box, - groundBody, - anchor, - Vector2(1, 0), - ) - ..enableLimit = true - ..lowerTranslation = -20 - ..upperTranslation = 20 - ..enableMotor = true - ..motorSpeed = 1 - ..maxMotorForce = 100; - - joint = PrismaticJoint(prismaticJointDef); - world.createJoint(joint); - } - - @override - void render(Canvas canvas) { - super.render(canvas); - - final p1 = - worldToScreen(anchor + joint.getLocalAxisA() * joint.getLowerLimit()); - final p2 = - worldToScreen(anchor + joint.getLocalAxisA() * joint.getUpperLimit()); - - canvas.drawLine(p1.toOffset(), p2.toOffset(), debugPaint); - } -} diff --git a/examples/lib/stories/bridge_libraries/forge2d/joints/pulley_joint.dart b/examples/lib/stories/bridge_libraries/forge2d/joints/pulley_joint.dart deleted file mode 100644 index 8374da29f..000000000 --- a/examples/lib/stories/bridge_libraries/forge2d/joints/pulley_joint.dart +++ /dev/null @@ -1,80 +0,0 @@ -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'; - -// ignore: deprecated_member_use -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/joints/weld_joint.dart b/examples/lib/stories/bridge_libraries/forge2d/joints/weld_joint.dart deleted file mode 100644 index e3af2f5d8..000000000 --- a/examples/lib/stories/bridge_libraries/forge2d/joints/weld_joint.dart +++ /dev/null @@ -1,75 +0,0 @@ -import 'package:examples/stories/bridge_libraries/forge2d/utils/balls.dart'; -import 'package:examples/stories/bridge_libraries/forge2d/utils/boxes.dart'; -import 'package:flame/input.dart'; -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flutter/material.dart'; - -class WeldJointExample extends Forge2DGame with TapDetector { - static const description = ''' - This example shows how to use a `WeldJoint`. Tap the screen to add a - ball to test the bridge built using a `WeldJoint` - '''; - - @override - Future onLoad() async { - super.onLoad(); - - const pillarHeight = 20.0; - final leftPillar = Box( - startPosition: Vector2(10, size.y - pillarHeight / 2), - width: 5, - height: pillarHeight, - bodyType: BodyType.static, - color: Colors.white, - ); - final rightPillar = Box( - startPosition: Vector2(size.x - 10, size.y - pillarHeight / 2), - width: 5, - height: pillarHeight, - bodyType: BodyType.static, - color: Colors.white, - ); - - addAll([leftPillar, rightPillar]); - - createBridge(size.y - pillarHeight); - } - - Future createBridge(double positionY) async { - const sectionsCount = 10; - final sectionWidth = (size.x / sectionsCount).ceilToDouble(); - Body? prevSection; - - for (var i = 0; i < sectionsCount; i++) { - final section = Box( - startPosition: Vector2(sectionWidth * i, positionY), - width: sectionWidth, - height: 1, - ); - await add(section); - - if (prevSection != null) { - createJoint( - prevSection, - section.body, - Vector2(sectionWidth * i + sectionWidth, positionY), - ); - } - - prevSection = section.body; - } - } - - void createJoint(Body first, Body second, Vector2 anchor) { - final weldJointDef = WeldJointDef()..initialize(first, second, anchor); - - world.createJoint(WeldJoint(weldJointDef)); - } - - @override - Future onTapDown(TapDownInfo info) async { - super.onTapDown(info); - final ball = Ball(info.eventPosition.game, radius: 5); - add(ball); - } -} diff --git a/examples/lib/stories/bridge_libraries/forge2d/utils/boundaries.dart b/examples/lib/stories/bridge_libraries/forge2d/utils/boundaries.dart deleted file mode 100644 index fcc13d387..000000000 --- a/examples/lib/stories/bridge_libraries/forge2d/utils/boundaries.dart +++ /dev/null @@ -1,36 +0,0 @@ -// ignore_for_file: deprecated_member_use - -import 'package:flame_forge2d/flame_forge2d.dart'; - -List createBoundaries(Forge2DGame game) { - final topLeft = Vector2.zero(); - final bottomRight = game.screenToWorld(game.camera.viewport.effectiveSize); - final topRight = Vector2(bottomRight.x, topLeft.y); - final bottomLeft = Vector2(topLeft.x, bottomRight.y); - - return [ - Wall(topLeft, topRight), - Wall(topRight, bottomRight), - Wall(bottomRight, bottomLeft), - Wall(bottomLeft, topLeft), - ]; -} - -class Wall extends BodyComponent { - final Vector2 start; - final Vector2 end; - - Wall(this.start, this.end); - - @override - Body createBody() { - final shape = EdgeShape()..set(start, end); - final fixtureDef = FixtureDef(shape, friction: 0.3); - final bodyDef = BodyDef( - userData: this, // To be able to determine object in collision - position: Vector2.zero(), - ); - - return world.createBody(bodyDef)..createFixture(fixtureDef); - } -} diff --git a/examples/lib/stories/input/gesture_hitboxes_example.dart b/examples/lib/stories/input/gesture_hitboxes_example.dart index e17ca9a66..7f27cceb7 100644 --- a/examples/lib/stories/input/gesture_hitboxes_example.dart +++ b/examples/lib/stories/input/gesture_hitboxes_example.dart @@ -11,8 +11,7 @@ import 'package:flame/input.dart'; enum Shapes { circle, rectangle, polygon } -class GestureHitboxesExample extends FlameGame - with TapCallbacks, HasHoverables { +class GestureHitboxesExample extends FlameGame with TapCallbacks { static const description = ''' Tap to create a PositionComponent with a randomly shaped hitbox. You can then hover over to shapes to see that they receive the hover events diff --git a/packages/flame/lib/components.dart b/packages/flame/lib/components.dart index 9188b09d5..ef4984d5e 100644 --- a/packages/flame/lib/components.dart +++ b/packages/flame/lib/components.dart @@ -17,6 +17,7 @@ export 'src/components/input/joystick_component.dart'; export 'src/components/input/keyboard_listener_component.dart'; export 'src/components/isometric_tile_map_component.dart'; export 'src/components/mixins/component_viewport_margin.dart'; +export 'src/components/mixins/coordinate_transform.dart'; export 'src/components/mixins/draggable.dart'; export 'src/components/mixins/gesture_hitboxes.dart'; export 'src/components/mixins/has_ancestor.dart'; diff --git a/packages/flame/lib/effects.dart b/packages/flame/lib/effects.dart index d9fe93a81..01a6a6445 100644 --- a/packages/flame/lib/effects.dart +++ b/packages/flame/lib/effects.dart @@ -34,6 +34,7 @@ export 'src/effects/provider_interfaces.dart' PositionProvider, ScaleProvider, SizeProvider, + ReadOnlyPositionProvider, ReadOnlySizeProvider, OpacityProvider; export 'src/effects/remove_effect.dart'; diff --git a/packages/flame/lib/src/camera/behaviors/follow_behavior.dart b/packages/flame/lib/src/camera/behaviors/follow_behavior.dart index d7274463b..8d9ec3177 100644 --- a/packages/flame/lib/src/camera/behaviors/follow_behavior.dart +++ b/packages/flame/lib/src/camera/behaviors/follow_behavior.dart @@ -18,7 +18,7 @@ import 'package:flame/src/effects/provider_interfaces.dart'; /// movement to the horizontal/vertical directions respectively. class FollowBehavior extends Component { FollowBehavior({ - required PositionProvider target, + required ReadOnlyPositionProvider target, PositionProvider? owner, double maxSpeed = double.infinity, this.horizontalOnly = false, @@ -33,8 +33,8 @@ class FollowBehavior extends Component { 'The behavior cannot be both horizontalOnly and verticalOnly', ); - PositionProvider get target => _target; - final PositionProvider _target; + ReadOnlyPositionProvider get target => _target; + final ReadOnlyPositionProvider _target; PositionProvider get owner => _owner!; PositionProvider? _owner; diff --git a/packages/flame/lib/src/camera/camera_component.dart b/packages/flame/lib/src/camera/camera_component.dart index 0f5ee4fd9..783a0b1f3 100644 --- a/packages/flame/lib/src/camera/camera_component.dart +++ b/packages/flame/lib/src/camera/camera_component.dart @@ -121,8 +121,11 @@ class CameraComponent extends Component { /// after the camera was fully mounted. Rect get visibleWorldRect { assert( - viewport.isMounted && viewfinder.isMounted, - 'This property cannot be accessed before the camera is mounted', + viewport.isLoaded && viewfinder.isLoaded, + 'This property cannot be accessed before the camera is loaded. ' + 'If you are using visibleWorldRect from another component (for example ' + 'the World), make sure that the CameraComponent is added before that ' + 'Component.', ); return viewfinder.visibleWorldRect; } @@ -194,9 +197,9 @@ class CameraComponent extends Component { yield* viewport.componentsAtPoint(viewportPoint, nestedPoints); if ((world?.isMounted ?? false) && currentCameras.length < maxCamerasDepth) { - if (viewport.containsLocalPoint(viewportPoint)) { + if (viewport.containsLocalPoint(_viewportPoint)) { currentCameras.add(this); - final worldPoint = viewfinder.transform.globalToLocal(viewportPoint); + final worldPoint = viewfinder.transform.globalToLocal(_viewportPoint); yield* viewfinder.componentsAtPoint(worldPoint, nestedPoints); yield* world!.componentsAtPoint(worldPoint, nestedPoints); currentCameras.removeLast(); @@ -240,7 +243,7 @@ class CameraComponent extends Component { /// will move from its current position to the target's position at the given /// speed. void follow( - PositionProvider target, { + ReadOnlyPositionProvider target, { double maxSpeed = double.infinity, bool horizontalOnly = false, bool verticalOnly = false, diff --git a/packages/flame/lib/src/camera/viewfinder.dart b/packages/flame/lib/src/camera/viewfinder.dart index 374c90370..9e3e44380 100644 --- a/packages/flame/lib/src/camera/viewfinder.dart +++ b/packages/flame/lib/src/camera/viewfinder.dart @@ -184,9 +184,21 @@ class Viewfinder extends Component } } + @mustCallSuper + @override + void onLoad() { + // This has to be done here and on onMount so that it is available for + // the CameraComponent.visibleWorldRect calculation in onLoad of the game. + _initializeTransform(); + } + @mustCallSuper @override void onMount() { + _initializeTransform(); + } + + void _initializeTransform() { assert( parent! is CameraComponent, 'Viewfinder can only be mounted to a CameraComponent', diff --git a/packages/flame/lib/src/components/core/component.dart b/packages/flame/lib/src/components/core/component.dart index 97655d3fe..0274108ec 100644 --- a/packages/flame/lib/src/components/core/component.dart +++ b/packages/flame/lib/src/components/core/component.dart @@ -4,7 +4,6 @@ import 'package:collection/collection.dart'; import 'package:flame/components.dart'; import 'package:flame/src/cache/value_cache.dart'; import 'package:flame/src/components/core/component_tree_root.dart'; -import 'package:flame/src/components/mixins/coordinate_transform.dart'; import 'package:flame/src/effects/provider_interfaces.dart'; import 'package:flame/src/game/flame_game.dart'; import 'package:flame/src/game/game.dart'; diff --git a/packages/flame/lib/src/effects/provider_interfaces.dart b/packages/flame/lib/src/effects/provider_interfaces.dart index 4cc59eb71..fd6754751 100644 --- a/packages/flame/lib/src/effects/provider_interfaces.dart +++ b/packages/flame/lib/src/effects/provider_interfaces.dart @@ -3,11 +3,16 @@ import 'dart:ui'; import 'package:flame/components.dart'; /// Interface for a component that can be affected by move effects. -abstract class PositionProvider { - Vector2 get position; +abstract class PositionProvider implements ReadOnlyPositionProvider { set position(Vector2 value); } +/// Interface for a class that has [position] property which can be read but not +/// modified. +abstract class ReadOnlyPositionProvider { + Vector2 get position; +} + /// This class allows constructing [PositionProvider]s on the fly, using the /// callbacks for the position getter and setter. This class doesn't require /// either the getter or the setter, if you do not intend to use those. diff --git a/packages/flame/lib/src/game/projector.dart b/packages/flame/lib/src/game/projector.dart index 1e3127378..ce5ab9493 100644 --- a/packages/flame/lib/src/game/projector.dart +++ b/packages/flame/lib/src/game/projector.dart @@ -1,7 +1,7 @@ import 'package:flame/extensions.dart'; /// A simple interface to mark a class that can perform projection operations -/// from one 2D Euclidian coordinate space to another. +/// from one 2D Euclidean coordinate space to another. /// /// This can be a Viewport, a Camera or anything else that exposes such /// operations to the user. diff --git a/packages/flame/test/camera/camera_component_test.dart b/packages/flame/test/camera/camera_component_test.dart index 8679b6835..7818e4dfb 100644 --- a/packages/flame/test/camera/camera_component_test.dart +++ b/packages/flame/test/camera/camera_component_test.dart @@ -269,13 +269,10 @@ void main() { world: world, viewport: FixedSizeViewport(60, 40), ); - game.addAll([world, camera]); expect( () => camera.visibleWorldRect, - failsAssert( - 'This property cannot be accessed before the camera is mounted', - ), + failsAssert(), ); }); diff --git a/packages/flame_forge2d/example/lib/main.dart b/packages/flame_forge2d/example/lib/main.dart index 71f398668..5448f4de5 100644 --- a/packages/flame_forge2d/example/lib/main.dart +++ b/packages/flame_forge2d/example/lib/main.dart @@ -1,33 +1,30 @@ -import 'package:flame/camera.dart' as camera; import 'package:flame/components.dart'; import 'package:flame/events.dart'; +import 'package:flame/extensions.dart'; import 'package:flame/game.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/widgets.dart'; void main() { - runApp(GameWidget(game: Forge2DExample())); + runApp(const GameWidget.controlled(gameFactory: Forge2DExample.new)); } class Forge2DExample extends Forge2DGame { - final cameraWorld = camera.World(); - late final CameraComponent cameraComponent; - @override Future onLoad() async { - cameraComponent = CameraComponent(world: cameraWorld); - cameraComponent.viewfinder.anchor = Anchor.topLeft; - addAll([cameraComponent, cameraWorld]); + await super.onLoad(); - cameraWorld.add(Ball(size / 2)); - cameraWorld.addAll(createBoundaries()); + cameraComponent.viewport.add(FpsTextComponent()); + world.add(Ball()); + world.addAll(createBoundaries()); } List createBoundaries() { - final topLeft = Vector2.zero(); - final bottomRight = screenToWorld(cameraComponent.viewport.size); - final topRight = Vector2(bottomRight.x, topLeft.y); - final bottomLeft = Vector2(topLeft.x, bottomRight.y); + final visibleRect = cameraComponent.visibleWorldRect; + final topLeft = visibleRect.topLeft.toVector2(); + final topRight = visibleRect.topRight.toVector2(); + final bottomRight = visibleRect.bottomRight.toVector2(); + final bottomLeft = visibleRect.bottomLeft.toVector2(); return [ Wall(topLeft, topRight), @@ -39,9 +36,10 @@ class Forge2DExample extends Forge2DGame { } class Ball extends BodyComponent with TapCallbacks { - final Vector2 _position; + final Vector2 initialPosition; - Ball(this._position); + Ball({Vector2? initialPosition}) + : initialPosition = initialPosition ?? Vector2.zero(); @override Body createBody() { @@ -58,7 +56,7 @@ class Ball extends BodyComponent with TapCallbacks { final bodyDef = BodyDef( userData: this, angularDamping: 0.8, - position: _position, + position: initialPosition, type: BodyType.dynamic, ); diff --git a/packages/flame_forge2d/lib/body_component.dart b/packages/flame_forge2d/lib/body_component.dart index f8716ecf0..0f496183d 100644 --- a/packages/flame_forge2d/lib/body_component.dart +++ b/packages/flame_forge2d/lib/body_component.dart @@ -1,19 +1,22 @@ import 'dart:ui'; import 'package:flame/components.dart' hide World; -import 'package:flame/effects.dart' show ReadOnlyAngleProvider; +import 'package:flame/effects.dart'; +import 'package:flame/experimental.dart'; import 'package:flame/extensions.dart'; import 'package:flame/game.dart'; -import 'package:flame_forge2d/forge2d_game.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/foundation.dart'; -import 'package:forge2d/forge2d.dart' hide Timer, Vector2; /// Since a pure BodyComponent doesn't have anything drawn on top of it, /// it is a good idea to turn on [debugMode] for it so that the bodies can be /// seen abstract class BodyComponent extends Component - with HasGameRef, HasPaint - implements ReadOnlyAngleProvider { + with HasGameReference, HasPaint + implements + CoordinateTransform, + ReadOnlyPositionProvider, + ReadOnlyAngleProvider { BodyComponent({ Paint? paint, super.children, @@ -27,6 +30,9 @@ abstract class BodyComponent extends Component static const defaultColor = Color.fromARGB(255, 255, 255, 255); late Body body; + @override + Vector2 get position => body.position; + /// Specifies if the body's fixtures should be rendered. /// /// [renderBody] is true by default for [BodyComponent], if set to false @@ -37,7 +43,7 @@ abstract class BodyComponent extends Component bool renderBody; /// You should create the Forge2D [Body] in this method when you extend - /// the BodyComponent + /// the BodyComponent. Body createBody(); @mustCallSuper @@ -47,34 +53,32 @@ abstract class BodyComponent extends Component body = createBody(); } - World get world => gameRef.world; - - // TODO(Lukas): Use CameraComponent here instead. - // ignore: deprecated_member_use - Camera get camera => gameRef.camera; - + Forge2DWorld get world => game.world; + CameraComponent get camera => game.cameraComponent; Vector2 get center => body.worldCenter; @override double get angle => body.angle; /// The matrix used for preparing the canvas - final Matrix4 _transform = Matrix4.identity(); + final Transform2D _transform = Transform2D(); + Matrix4 get _transformMatrix => _transform.transformMatrix; double? _lastAngle; @mustCallSuper @override void renderTree(Canvas canvas) { - if (_transform.m14 != body.position.x || - _transform.m24 != body.position.y || + final matrix = _transformMatrix; + if (matrix.m14 != body.position.x || + matrix.m24 != body.position.y || _lastAngle != angle) { - _transform.setIdentity(); - _transform.translate(body.position.x, body.position.y); - _transform.rotateZ(angle); + matrix.setIdentity(); + matrix.translate(body.position.x, body.position.y); + matrix.rotateZ(angle); _lastAngle = angle; } canvas.save(); - canvas.transform(_transform.storage); + canvas.transform(matrix.storage); super.renderTree(canvas); canvas.restore(); } @@ -82,9 +86,9 @@ abstract class BodyComponent extends Component @override void render(Canvas canvas) { if (renderBody) { - body.fixtures.forEach( - (fixture) => renderFixture(canvas, fixture), - ); + for (final fixture in body.fixtures) { + renderFixture(canvas, fixture); + } } } @@ -103,10 +107,7 @@ abstract class BodyComponent extends Component /// /// **NOTE**: If [renderBody] is false, no fixtures will be rendered. Hence, /// [renderFixture] is not called when [render]ing. - void renderFixture( - Canvas canvas, - Fixture fixture, - ) { + void renderFixture(Canvas canvas, Fixture fixture) { canvas.save(); switch (fixture.type) { case ShapeType.chain: @@ -154,8 +155,13 @@ abstract class BodyComponent extends Component ); } + late final Path _path = Path(); + void renderPolygon(Canvas canvas, List points) { - final path = Path()..addPolygon(points, true); + final path = _path + ..reset() + ..addPolygon(points, true); + // TODO(Spydon): Use drawVertices instead. canvas.drawPath(path, paint); } @@ -168,6 +174,22 @@ abstract class BodyComponent extends Component canvas.drawLine(p1, p2, paint); } + @override + Vector2 parentToLocal(Vector2 point) => _transform.globalToLocal(point); + + @override + Vector2 localToParent(Vector2 point) => _transform.localToGlobal(point); + + late final Vector2 _hitTestPoint = Vector2.zero(); + + @override + bool containsLocalPoint(Vector2 point) { + _hitTestPoint + ..setFrom(body.position) + ..add(point); + return body.fixtures.any((fixture) => fixture.testPoint(_hitTestPoint)); + } + @override bool containsPoint(Vector2 point) { return body.fixtures.any((fixture) => fixture.testPoint(point)); diff --git a/packages/flame_forge2d/lib/flame_forge2d.dart b/packages/flame_forge2d/lib/flame_forge2d.dart index 2a28f229c..56cc80b14 100644 --- a/packages/flame_forge2d/lib/flame_forge2d.dart +++ b/packages/flame_forge2d/lib/flame_forge2d.dart @@ -4,5 +4,6 @@ export 'package:forge2d/forge2d.dart'; export 'body_component.dart'; export 'contact_callbacks.dart'; -export 'forge2d_camera.dart'; export 'forge2d_game.dart'; +export 'forge2d_world.dart'; +export 'world_contact_listener.dart'; diff --git a/packages/flame_forge2d/lib/forge2d_camera.dart b/packages/flame_forge2d/lib/forge2d_camera.dart deleted file mode 100644 index fd783ad8a..000000000 --- a/packages/flame_forge2d/lib/forge2d_camera.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:flame/components.dart'; -import 'package:flame/extensions.dart'; -import 'package:flame/game.dart'; -import 'package:flame_forge2d/body_component.dart'; - -extension Forge2DCameraExtension on Camera { - /// Immediately snaps the camera to start following the [BodyComponent]. - /// - /// This means that the camera will move so that the position vector of the - /// component is in a fixed position on the screen. - /// That position is determined by a fraction of screen size defined by - /// [relativeOffset] (default to the center). - /// [worldBounds] can be optionally set to add boundaries to how far the - /// camera is allowed to move. - /// [useCenterOfMass] set true to follow the body's center of mass rather than - /// position (default to false). - void followBodyComponent( - BodyComponent bodyComponent, { - Anchor relativeOffset = Anchor.center, - Rect? worldBounds, - bool useCenterOfMass = false, - }) { - followVector2( - useCenterOfMass - ? bodyComponent.body.worldCenter - : bodyComponent.body.position, - relativeOffset: relativeOffset, - worldBounds: worldBounds, - ); - } -} diff --git a/packages/flame_forge2d/lib/forge2d_game.dart b/packages/flame_forge2d/lib/forge2d_game.dart index 40eb86bbc..3046ed4e7 100644 --- a/packages/flame_forge2d/lib/forge2d_game.dart +++ b/packages/flame_forge2d/lib/forge2d_game.dart @@ -1,41 +1,61 @@ +import 'dart:async'; + +import 'package:flame/camera.dart'; import 'package:flame/game.dart'; -import 'package:flame_forge2d/world_contact_listener.dart'; +import 'package:flame_forge2d/forge2d_world.dart'; +import 'package:flutter/foundation.dart'; import 'package:forge2d/forge2d.dart'; +/// The base game class for creating games that uses the Forge2D physics engine. class Forge2DGame extends FlameGame { Forge2DGame({ Vector2? gravity, - double zoom = defaultZoom, - Camera? camera, ContactListener? contactListener, - }) : world = World(gravity ?? defaultGravity), - super(camera: camera ?? Camera()) { - // ignore: deprecated_member_use - this.camera.zoom = zoom; - world.setContactListener(contactListener ?? WorldContactListener()); + double zoom = 10, + Forge2DWorld? world, + }) : _world = (world?..setGravity(gravity)) ?? + Forge2DWorld( + gravity: gravity, + contactListener: contactListener, + ), + _initialZoom = zoom; + + /// The [Forge2DWorld] that the [cameraComponent] is rendering. + /// Inside of this world is where all your components should be added. + Forge2DWorld get world => _world; + set world(Forge2DWorld newWorld) { + cameraComponent.world = newWorld; + _world = newWorld; } - static final Vector2 defaultGravity = Vector2(0, 10.0); + Forge2DWorld _world; - static const double defaultZoom = 10.0; + CameraComponent cameraComponent = CameraComponent(); - final World world; + // TODO(spydon): Use a meterToPixels constant instead for rendering. + // (see #2613) + final double _initialZoom; @override - void update(double dt) { - super.update(dt); - world.stepDt(dt); + @mustCallSuper + FutureOr onLoad() async { + cameraComponent + ..world = world + ..viewfinder.zoom = _initialZoom; + add(cameraComponent); + add(world); } + /// Takes a point in world coordinates and returns it in screen coordinates. Vector2 worldToScreen(Vector2 position) { - return projector.projectVector(position); + return cameraComponent.localToGlobal(position); } + /// Takes a point in screen coordinates and returns it in world coordinates. + /// + /// Remember that if you are using this for your events you can most of the + /// time just use `event.localPosition` directly instead. Vector2 screenToWorld(Vector2 position) { - return projector.unprojectVector(position); - } - - Vector2 screenToFlameWorld(Vector2 position) { - return screenToWorld(position)..y *= -1; + return cameraComponent.globalToLocal(position); } } diff --git a/packages/flame_forge2d/lib/forge2d_world.dart b/packages/flame_forge2d/lib/forge2d_world.dart new file mode 100644 index 000000000..62fc709aa --- /dev/null +++ b/packages/flame_forge2d/lib/forge2d_world.dart @@ -0,0 +1,65 @@ +import 'package:flame/components.dart'; +import 'package:flame_forge2d/flame_forge2d.dart' hide World; +import 'package:forge2d/forge2d.dart' as forge2d; + +/// The root component when using [Forge2DGame], can handle both +/// [BodyComponent]s and normal Flame components. +/// +/// Wraps the world class that comes from Forge2D ([forge2d.World]). +class Forge2DWorld extends World { + Forge2DWorld({ + Vector2? gravity, + forge2d.ContactListener? contactListener, + super.children, + }) : physicsWorld = forge2d.World(gravity ?? defaultGravity) + ..setContactListener(contactListener ?? WorldContactListener()); + + static final Vector2 defaultGravity = Vector2(0, 10.0); + + final forge2d.World physicsWorld; + + @override + void update(double dt) { + physicsWorld.stepDt(dt); + } + + Body createBody(BodyDef def) { + return physicsWorld.createBody(def); + } + + void destroyBody(Body body) { + physicsWorld.destroyBody(body); + } + + void createJoint(forge2d.Joint joint) { + physicsWorld.createJoint(joint); + } + + void destroyJoint(forge2d.Joint joint) { + physicsWorld.destroyJoint(joint); + } + + void raycast(RayCastCallback callback, Vector2 point1, Vector2 point2) { + physicsWorld.raycast(callback, point1, point2); + } + + void clearForces() { + physicsWorld.clearForces(); + } + + void queryAABB(forge2d.QueryCallback callback, AABB aabb) { + physicsWorld.queryAABB(callback, aabb); + } + + void raycastParticle( + forge2d.ParticleRaycastCallback callback, + Vector2 point1, + Vector2 point2, + ) { + physicsWorld.particleSystem.raycast(callback, point1, point2); + } + + void setGravity(Vector2? gravity) { + physicsWorld.setGravity(gravity ?? defaultGravity); + } +} diff --git a/packages/flame_forge2d/test/body_component_test.dart b/packages/flame_forge2d/test/body_component_test.dart index a4734e18a..e404b58b5 100644 --- a/packages/flame_forge2d/test/body_component_test.dart +++ b/packages/flame_forge2d/test/body_component_test.dart @@ -53,18 +53,9 @@ void main() { final component = _TestBodyComponent() ..body = body ..paint = testPaint; - await game.add(component); + await game.world.add(component); - game.camera.followVector2(Vector2.zero()); - - // a CircleShape contains point - expect(component.containsPoint(Vector2.all(1.5)), isTrue); - }, - verify: (game, tester) async { - await expectLater( - find.byGame(), - matchesGoldenFile(goldenPath('circle_shape')), - ); + game.cameraComponent.follow(component); }, ); @@ -82,9 +73,9 @@ void main() { final component = _TestBodyComponent() ..body = body ..paint = testPaint; - await game.add(component); + await game.world.add(component); - game.camera.followVector2(Vector2.zero()); + game.cameraComponent.follow(component); }, verify: (game, tester) async { await expectLater( @@ -111,9 +102,9 @@ void main() { final component = _TestBodyComponent() ..body = body ..paint = testPaint; - await game.add(component); + await game.world.add(component); - game.camera.followVector2(Vector2.zero()); + game.cameraComponent.follow(component); // a PolygonShape contains point expect(component.containsPoint(Vector2.all(10)), isTrue); @@ -143,9 +134,9 @@ void main() { final component = _TestBodyComponent() ..body = body ..paint = testPaint; - await game.add(component); + await game.world.add(component); - game.camera.followVector2(Vector2.zero()); + game.cameraComponent.follow(component); }, verify: (game, tester) async { await expectLater( @@ -172,9 +163,9 @@ void main() { final component = _TestBodyComponent() ..body = body ..paint = testPaint; - await game.add(component); + await game.world.add(component); - game.camera.followVector2(Vector2.zero()); + game.cameraComponent.follow(component); }, verify: (game, tester) async { await expectLater( @@ -276,10 +267,7 @@ void main() { flameTester.testGameWidget( 'add and remove child to BodyComponent', setUp: (game, tester) async { - final worldCenter = - game.screenToWorld(game.size * game.camera.zoom / 2); - - final bodyDef = BodyDef(position: worldCenter.clone()); + final bodyDef = BodyDef(); final body = game.world.createBody(bodyDef); final shape = PolygonShape() ..set( @@ -295,18 +283,18 @@ void main() { ..body = body ..paint = testPaint; - component.addToParent(game); + game.world.add(component); await game.ready(); - expect(game.contains(component), true); + expect(game.world.contains(component), true); expect(component.isMounted, true); - expect(game.children.length, 1); + expect(game.world.children.length, 1); component.removeFromParent(); await game.ready(); expect(component.isMounted, false); expect(component.isLoaded, true); - expect(game.children.length, 0); + expect(game.world.children.length, 0); }, ); }); @@ -347,15 +335,15 @@ void main() { final positionComponent = PositionComponent(angle: 1.0); // Creates a hierarchy: game > bodyComponent > positionComponent - bodyComponent.addToParent(game); - positionComponent.addToParent(bodyComponent); + game.world.add(bodyComponent); + bodyComponent.add(positionComponent); await game.ready(); // Checks the hierarchy - expect(game.contains(bodyComponent), true); + expect(game.world.contains(bodyComponent), true); expect(bodyComponent.contains(positionComponent), true); - expect(game.children.length, 1); + expect(game.world.children.length, 1); expect(bodyComponent.children.length, 1); expect(positionComponent.children.length, 0); diff --git a/packages/flame_forge2d/test/forge2d_game_test.dart b/packages/flame_forge2d/test/forge2d_game_test.dart index 727986a90..a3f2cb62f 100644 --- a/packages/flame_forge2d/test/forge2d_game_test.dart +++ b/packages/flame_forge2d/test/forge2d_game_test.dart @@ -1,4 +1,5 @@ import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame_test/flame_test.dart'; import 'package:test/test.dart'; class _TestForge2dGame extends Forge2DGame { @@ -9,44 +10,72 @@ void main() { group( 'Test corresponding position on screen and in the Forge2D world', () { - test('Zero positioned camera should be zero in world', () { + testWithGame('Center positioned camera should be zero in world', + _TestForge2dGame.new, (game) async { + final size = Vector2.all(100); + game.onGameResize(size); expect( - _TestForge2dGame().screenToWorld(Vector2.zero()), + game.screenToWorld(size / 2), Vector2.zero(), ); }); - test('Zero positioned camera should be zero in FlameWorld', () { + testWithGame('Top left position should be converted correctly to world', + _TestForge2dGame.new, (game) async { + final size = Vector2.all(100); + game.onGameResize(size); expect( - _TestForge2dGame().screenToFlameWorld(Vector2.zero()), - Vector2.zero(), + game.screenToWorld(Vector2.zero()), + -(size / 2) / game.cameraComponent.viewfinder.zoom, ); }); - test('Converts a vector in the world space to the screen space', () { + testWithGame('Non-zero position should be converted correctly to world', + _TestForge2dGame.new, (game) async { + final size = Vector2.all(100); + final screenPosition = Vector2(10, 20); + game.onGameResize(size); expect( - _TestForge2dGame().worldToScreen(Vector2(5, 6)), - Vector2(20.0, 24.0), + game.screenToWorld(screenPosition), + (-size / 2 + screenPosition) / game.cameraComponent.viewfinder.zoom, ); }); - test('Converts a vector in the screen space to the world space', () { + testWithGame('Converts a vector in the world space to the screen space', + _TestForge2dGame.new, (game) async { + final size = Vector2.all(100); + game.onGameResize(size); expect( - _TestForge2dGame().screenToFlameWorld(Vector2(5, 6)), - Vector2(1.25, -1.5), + game.worldToScreen(Vector2.zero()), + size / 2, ); }); - }, - ); - group( - 'Test input vector does not get modified while function call', - () { - test('Camera should not modify the input vector while projecting it', () { - final vec = Vector2(5, 6); - // ignore: deprecated_member_use - _TestForge2dGame().camera.projectVector(vec); - expect(vec, Vector2(5, 6)); + testWithGame( + 'Converts a non-zero vector in the world space to the screen space', + _TestForge2dGame.new, (game) async { + final size = Vector2.all(100); + final worldPosition = Vector2.all(10); + game.onGameResize(size); + expect( + game.worldToScreen(worldPosition), + (size / 2) + worldPosition * game.cameraComponent.viewfinder.zoom, + ); + }); + + testWithGame('Converts worldToScreen correctly with moved viewfinder', + _TestForge2dGame.new, (game) async { + final size = Vector2.all(100); + final worldPosition = Vector2(10, 30); + final viewfinderPosition = Vector2(20, 10); + game.onGameResize(size); + game.cameraComponent.viewfinder.position = viewfinderPosition; + expect( + game.worldToScreen(worldPosition), + (size / 2) + + (worldPosition - viewfinderPosition) * + game.cameraComponent.viewfinder.zoom, + ); }); }, ); diff --git a/packages/flame_forge2d/test/position_test.dart b/packages/flame_forge2d/test/position_test.dart deleted file mode 100644 index e1f41f8a1..000000000 --- a/packages/flame_forge2d/test/position_test.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:flame/extensions.dart'; -import 'package:flame_forge2d/forge2d_game.dart'; -import 'package:test/test.dart'; - -class TestGame extends Forge2DGame { - TestGame() : super(zoom: 4.0, gravity: Vector2(0, -10.0)); -} - -void main() { - group( - 'Test corresponding position on screen and in the Forge2D world', - () { - test('Zero positioned camera should be zero in world', () { - expect(TestGame().screenToWorld(Vector2.zero()), Vector2.zero()); - }); - }, - ); - group( - 'Test input vector does not get modified while function call', - () { - test('Camera should not modify the input vector while projecting it', () { - final vec = Vector2(5, 6); - // ignore: deprecated_member_use - TestGame().camera.projectVector(vec); - expect(vec, Vector2(5, 6)); - }); - }, - ); -} diff --git a/packages/flame_forge2d/test/world_contact_listener_test.dart b/packages/flame_forge2d/test/world_contact_listener_test.dart index 549fe0550..570675693 100644 --- a/packages/flame_forge2d/test/world_contact_listener_test.dart +++ b/packages/flame_forge2d/test/world_contact_listener_test.dart @@ -1,5 +1,4 @@ import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flame_forge2d/world_contact_listener.dart'; import 'package:mocktail/mocktail.dart'; import 'package:test/scaffolding.dart';