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;
}
}
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 BodyComponent
s 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).