Search code examples
flutterdartflame

From flame 0.29.0 to flame 1.0.0 and from box2d_flame: ^0.4.6 to flame_forge2d 0.11.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;
  }
}


Solution

  • 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).