0

I'm Migrating to flame 1.1.1 from flame v0.29.4 and I can't find a good roadmap.

How can I effectively replace Box2DComponent ? Attributes like components and viewport for example, i can't understand where and how to replace them.

Is correct replacing BaseGame with Flame game ? And would it be correct replacing Box2DComponent with Forge2DGame ?

under here my 3 classes. I knwo it's a difficult question but I could really use some help. Thank you

import 'dart:math' as math;
import 'dart:ui' as ui;

import 'package:artista_app/features/tastes/model/presentation/tastes_vm.dart';
import 'package:box2d_flame/box2d.dart';
import 'package:flame/box2d/box2d_component.dart';
import 'package:flame/box2d/viewport.dart' as box2d_viewport;
import 'package:flame/components/mixins/tapable.dart';
import 'package:flame/game/base_game.dart';
import 'package:flame/gestures.dart';
import 'package:flame/text_config.dart';
import 'package:flutter/material.dart';

class BubblePicker extends BaseGame with TapDetector {
  PickerWorld _pickerWorld;

  @override
  ui.Color backgroundColor() {
    return Colors.transparent;
  }

  final void Function(TastesVM) onTastesChange;

  BubblePicker(TastesVM tastes, {this.onTastesChange}) : super() {
    _pickerWorld = PickerWorld(tastes);
    _pickerWorld.initializeWorld();
    onTastesChange?.call(TastesVM(
        tastes: (tastes.tastes.where((taste) => taste.checked).toList())));
  }

  @override
  void onTapUp(TapUpDetails details) {
    _pickerWorld.handleTap(details);
    onTastesChange?.call(TastesVM(tastes: _pickerWorld.checkedTastes));
    super.onTapUp(details);
  }

  @override
  bool debugMode() => true;
  @override
  void render(Canvas canvas) {
    super.render(canvas);
    _pickerWorld.render(canvas);
  }

  @override
  void resize(Size size) {
    super.resize(size);
    _pickerWorld.resize(size);
  }

  @override
  void update(double t) {
    super.update(t);
    _pickerWorld.update(t);
  }
}

class PickerWorld extends Box2DComponent {
  final TastesVM tastes;
  PickerWorld(this.tastes) : super(gravity: 0);

  @override
  void initializeWorld() {}

  @override
  void render(Canvas canvas) {
    super.render(canvas);
  }

  Offset screenOffsetToWorldOffset(Offset position) {
    return Offset(position.dx - (viewport.size.width / 2),
        position.dy - (viewport.size.height / 2));
  }

  List<TasteVM> get checkedTastes => [
        for (final component in components)
          if (component is Ball && component.checked) component.model
      ];

  void handleTap(TapUpDetails details) {
    for (final component in components) {
      if (component is Ball) {
        final worldOffset = screenOffsetToWorldOffset(details.localPosition);
        if (component.checkTapOverlap(worldOffset)) {
          component.onTapUp(details);
        }
      }
    }
  }

  @override
  void resize(Size size) {
    dimensions = Size(size.width, size.height);
    viewport = box2d_viewport.Viewport(size, 1);
    if (components.isEmpty) {
      var tastesList = tastes.tastes;
      tastesList.forEach((element) {
        var ballPosOffset = Vector2(
            math.Random().nextDouble() - 0.5, math.Random().nextDouble() - 0.5);
        var x = ballPosOffset.x * 150;
        var y = ballPosOffset.y * 150;
        add(Ball(Vector2.array([x, y]), this, element));
      });
    }
  }
}

class Ball extends BodyComponent with Tapable {
  static const transitionSeconds = 0.5;
  var transforming = false;
  var kNormalRadius;
  static const kExpandedRadius = 50.0;
  var currentRadius;
  var lastTapStamp = DateTime.utc(0);
  final TasteVM model;
  final TextConfig smallTextConfig = TextConfig(
    fontSize: 12.0,
    fontFamily: 'Arial',
    color: Colors.white,
    textAlign: TextAlign.center,
  );
  final TextConfig bigTextConfig = TextConfig(
    fontSize: 24.0,
    fontFamily: 'Arial',
    color: Colors.white,
    textAlign: TextAlign.center,
  );
  Size screenSize;
  ui.Image ballImage;

  bool get checked => model.checked;

  Ball(
    Vector2 position,
    Box2DComponent box2dComponent,
    this.model,
  ) : super(box2dComponent) {
    ballImage = model.tasteimageResource;
    final shape = CircleShape();

    kNormalRadius = model.initialRadius;
    currentRadius = (model.checked) ? kExpandedRadius : model.initialRadius;
    shape.radius = currentRadius;
    shape.p.x = 0.0;

    // checked = model.checked;

    final fixtureDef = FixtureDef();
    fixtureDef.shape = shape;

    fixtureDef.restitution = 0.1;
    fixtureDef.density = 1;
    fixtureDef.friction = 1;
    fixtureDef.userData = model;

    final bodyDef = BodyDef();
    bodyDef.linearVelocity = Vector2(0.0, 0.0);
    bodyDef.position = position;
    bodyDef.type = BodyType.DYNAMIC;
    bodyDef.userData = model;
    body = world.createBody(bodyDef)..createFixtureFromFixtureDef(fixtureDef);
  }

  @override
  void renderCircle(Canvas canvas, Offset center, double radius) async {
    final rectFromCircle = Rect.fromCircle(center: center, radius: radius);
    final ballDiameter = radius * 2;

    if (ballImage == null) {
      return;
    }

    final image = checked ? ballImage : null;

    final paint = Paint()..color = const Color.fromARGB(255, 101, 101, 101);
    final elapsed =
        DateTime.now().difference(lastTapStamp).inMicroseconds / 1000000;

    final transforming = elapsed < transitionSeconds;

    if (transforming) {
      _resizeBall(elapsed);
    }

    canvas.drawCircle(center, radius, paint);

    if (image != null) {
      //from: https://stackoverflow.com/questions/60468768/masking-two-images-in-flutter-using-a-custom-painter/60470034#60470034

      canvas.saveLayer(rectFromCircle, Paint());

      //draw the mask
      canvas.drawCircle(
        center,
        radius,
        Paint()..color = Colors.black,
      );

      //fit the image into the ball size
      final inputSize = Size(image.width.toDouble(), image.height.toDouble());
      final fittedSizes = applyBoxFit(
        BoxFit.cover,
        inputSize,
        Size(ballDiameter, ballDiameter),
      );
      final sourceSize = fittedSizes.source;
      final sourceRect =
          Alignment.center.inscribe(sourceSize, Offset.zero & inputSize);

      canvas.drawImageRect(
        image,
        sourceRect,
        rectFromCircle,
        Paint()..blendMode = BlendMode.srcIn,
      );
      canvas.restore();
    }

    final span = TextSpan(
        style: TextStyle(color: Colors.white, fontSize: 10),
        text: model.tasteDisplayName);

    final tp = TextPainter(
      text: span,
      textAlign: TextAlign.center,
      textDirection: TextDirection.ltr,
    );
    tp.layout(minWidth: ballDiameter, maxWidth: ballDiameter);
    tp.paint(canvas, Offset(center.dx - radius, center.dy - (tp.height / 2)));
  }

  @override
  void update(double t) {
    final center = Vector2.copy(box.world.center);
    final ball = body.position;

    center.sub(ball);
    var distance = center.distanceTo(ball);
    body.applyForceToCenter(center..scale(1000000 / (distance)));
  }

  @override
  ui.Rect toRect() {
    var rect = Rect.fromCircle(
      center: Offset(body.position.x, -body.position.y),
      radius: currentRadius,
    );
    return rect;
  }

  @override
  void onTapUp(TapUpDetails details) {
    lastTapStamp = DateTime.now();
    model.checked = !checked;
    if (checked) {
      currentRadius = kExpandedRadius;
    } else {
      currentRadius = kNormalRadius;
    }
  }

  void _resizeBall(elapsed) {
    var progress = elapsed / transitionSeconds;
    final fixture = body.getFixtureList();
    var sourceRadius = (checked) ? kNormalRadius : kExpandedRadius;
    var targetRadius = (checked) ? kExpandedRadius : kNormalRadius;

    var progressRad = ui.lerpDouble(0, math.pi / 2, progress);
    var nonLinearProgress = math.sin(progressRad);

    var actualRadius =
        ui.lerpDouble(sourceRadius, targetRadius, nonLinearProgress);
    fixture.getShape().radius = actualRadius;
  }
}

rohtvccx_
  • 3
  • 3

1 Answers1

0

This issue is a bit too wide for StackOverflow, but I'll try to answer it as well as I can.

To use Forge2D (previously box2d.dart) in Flame you have to add flame_forge2d as a dependency. From flame_forge2d you will get a Forge2DGame that you should use instead of FlameGame (and instead of the ancient BaseGame class that you are using).

After that you extend BodyComponents for each body that you want to add to your Forge2DGame.

class Ball extends BodyComponent {
  final double radius;
  final Vector2 _position;

  Ball(this._position, {this.radius = 2});

  @override
  Body createBody() {
    final shape = CircleShape();
    shape.radius = radius;

    final fixtureDef = FixtureDef(
      shape,
      restitution: 0.8,
      density: 1.0,
      friction: 0.4,
    );

    final bodyDef = BodyDef(
      userData: this,
      angularDamping: 0.8,
      position: _position,
      type: BodyType.dynamic,
    );

    return world.createBody(bodyDef)..createFixture(fixtureDef);
  }
}

In the createBody() method you have to create the Forge2D body, in this case a circle is created. If you don't want it to render the circle directly you can set renderBody = false. To render something else on top of the BodyComponent you either override the render method, or you add a normal Flame component as a child to it, for example a SpriteComponent or SpriteAnimationComponent.

To add a child, simply call add in the onLoad method (or in another fitting place):

class Ball extends BodyComponent {
  ...

  @override
  Future<void> onLoad() async {
    await super.onLoad();
    add(SpriteComponent(...));
  }

  ...
}

Since you are using the Tappable mixin, you should also add the HasTappables mixin to your Forge2D game class.

You can find some examples here: https://examples.flame-engine.org/#/flame_forge2d_Blob%20example (press the < > in the upper right corner to get to the code).

spydon
  • 9,372
  • 6
  • 33
  • 63
  • Thank you for your kind and quick response. – rohtvccx_ May 17 '22 at 08:11
  • Thank you for your kind and quick response. I have already imported flame_forge2d and did some test but nothing worked. In my case the world is `PickerWorld and it's extending Box2DComponent` that means now the world and the balls in my case will be all `BodyComponent`s right? – rohtvccx_ May 17 '22 at 08:17
  • And also for example: ` List get checkedTastes => [` ` for (final component in components)` `if (component is Ball && component.checked) component.model` ]; ` the attribute components was available before now with BodyComponent I can't manage and access this attribute – rohtvccx_ May 17 '22 at 08:21
  • PickerWorld should be extending `Forge2DGame`, any references to Box2D things are wrong, maybe you still have the old library imported? To get all components in the game from another component you should use `gameRef.children`. – spydon May 17 '22 at 17:52
  • I did update old the lybraries of flame. Currently i'am using Forge2DGame but many things are too different from old Box2D. This was actually my main problem. migrating from old to new. – rohtvccx_ May 18 '22 at 10:30
  • Would you do something like that ? `class BubblePicker extends FlameGame with TapDetector` and `class PickerWorld extends Forge2DGame` ? – rohtvccx_ May 18 '22 at 10:33
  • No, I would have `Forge2DGame` as the root and then add components to that one. You should never call update and render manually like you do in the code in the question, you should use `add` to add components to the game, or to other components that are added in the game. You could probably merge `BubblePicker` and `PickerWorld` to one class that extends `Forge2DGame`. – spydon May 18 '22 at 11:17
  • Okay, you just brought good news to me because I was alreadydoing the exact thig you said above and somethig is working and rendering but the physics is not quite the same. Before all bubbles were attracted/toching themself and they had a "bond" between them. Now thy collide and doing strange things – rohtvccx_ May 18 '22 at 12:02
  • The trick was done with this line: `body.applyForceToCenter(center..scale(1000000 / (distance)));` but now there is only applyForce and I don't really knwo how to simulate applyForceToCenter – rohtvccx_ May 18 '22 at 12:13
  • Or I misunderstood `box.world.center` because in my migrated code i wrote `body.worldCenter.clone();` in the update function of Ball – rohtvccx_ May 18 '22 at 13:01
  • okay yes, it was a wrong center, now the center of mass seems to work fine – rohtvccx_ May 18 '22 at 13:17
  • I would like to ask you the last (I hope it will be the last) question. onTap I risez the Ball but sometimes it overlaps the oher balls around, then gradually it fix itself but I would like to avoid overlaping. How can I do that ? – rohtvccx_ May 18 '22 at 13:22
  • may be this will be the last hahah how can I invoke `gameRef.children` ? – rohtvccx_ May 18 '22 at 14:48
  • I'm not sure that Forge2D supports changing the radius on the fly out of the box like you are doing in `onTapUp`. For the second question, you have to add `HasGameRef` if it is not a `BodyComponent`, if it is a `BodyComponent` then it already has a `gameRef`. – spydon May 18 '22 at 21:46
  • 1
    It does not work with HasGameRef. But It's working with `children.register();` inside `onLoad`. Anyway, thank you very much for the support! More devs like you :)) – rohtvccx_ May 19 '22 at 09:53
  • Glad to be of help! If the answer helped you, you can mark the answer as correct with the check mark under the score. :) – spydon May 19 '22 at 20:01