从 flame 0.29.0 到 flame 1.0.0 和从 box2d_flame: ^0.4.6 到 flame_forge2d 0.11.0

From flame 0.29.0 to flame 1.0.0 and from box2d_flame: ^0.4.6 to flame_forge2d 0.11.0

我正在从 Flame v0.29.4 迁移到 Flame 1.1.1,但找不到合适的路线图。

如何有效地替换 Box2DComponent ?例如组件和视口等属性,我无法理解在哪里以及如何替换它们。

用 Flame 游戏替换 BaseGame 是否正确?用 Forge2DGame 替换 Box2DComponent 是否正确?

下面是我的 3 类。我知道这是一个难题,但我真的需要一些帮助。 谢谢

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: 

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

这个问题对于 Whosebug 来说有点太宽泛了,但我会尽力回答它。

要在 Flame 中使用 Forge2D(以前是 box2d.dart),您必须添加 flame_forge2d 作为依赖项。从 flame_forge2d 你会得到一个 Forge2DGame,你应该使用它来代替 FlameGame(而不是你正在使用的古老的 BaseGame class)。

之后,为要添加到 Forge2DGame 的每个主体扩展 BodyComponents。

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

createBody() 方法中,您必须创建 Forge2D 实体,在本例中创建了一个圆。如果你不想让它直接渲染圆你可以设置renderBody = false。要在 BodyComponent 之上渲染其他内容,您可以覆盖 render 方法,或者将普通的 Flame 组件作为 child 添加到它,例如 SpriteComponentSpriteAnimationComponent.

要添加 child,只需在 onLoad 方法中调用 add(或在其他合适的地方):

class Ball extends BodyComponent {
  ...

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

  ...
}

由于您正在使用 Tappable mixin,因此您还应该将 HasTappables mixin 添加到您的 Forge2D 游戏中 class。

您可以在此处找到一些示例: https://examples.flame-engine.org/#/flame_forge2d_Blob%20example (按右上角的< >获取代码)