Why is onTap method not working in flame? - flutter

I am trying to recreate the game flappy birds using flame and flutter but I encountered a problem and I was wondering if anyone would be able to help me.
The problem is that the onTap method is not working. This is my code:
const COLOR = const Color(0xFF75DA8B);
const SIZE = 52.0;
const GRAVITY = 400.0;
const BOOST = -380.0;
void main() async{
WidgetsFlutterBinding.ensureInitialized ();
final size = await Flame.util.initialDimensions();
final game = MyGame(size);
runApp(game.widget);
}
class Bg extends Component with Resizable{
static final Paint _paint = Paint()..color = COLOR;
#override
void render(Canvas c) {
c.drawRect(Rect.fromLTWH(0.0, 0.0, size.width, size.height), _paint);
}
#override
void update(double t) {
}
}
class Bird extends AnimationComponent with Resizable{
double speedY = 0.0;
bool frozen;
Bird () : super.sequenced(SIZE, SIZE, 'bird.png', 4, textureWidth: 16.0, textureHeight: 16.0) {
this.anchor = Anchor.center;
}
Position get velocity => Position (300.0, speedY);
reset () {
this.x = size.width/ 2;
this.y = size.height/2;
speedY = 0;
frozen = true;
angle = 0.0;
}
#override
void resize(Size size) {
super.resize(size);
reset();
}
#override
void update(double t) {
super.update(t);
if (frozen) return;
this.y += speedY * t - GRAVITY * t * t / 2;
this.speedY += GRAVITY * t;
this.angle = velocity.angle();
if (y > size.height) {
reset();
}
}
onTap () {
if (frozen) {
frozen = false;
return;
}
speedY = (speedY + BOOST).clamp(BOOST, speedY);
}
}
class MyGame extends BaseGame {
Bird bird;
MyGame (Size size){
add(Bg());
add(bird = Bird());
}
#override
void onTap() {
bird.onTap();
}
}
The bird stays static, and if I comment the line of code:
if (frozen) return;
on the update method, then it falls but ontap is not working.
Would you know why?
Thank you so much in advance.

I don't know which version of Flame you were using since this is quite an old question. If you are using a release candidate of 1.0.0 today you should at least follow the following structure:
class MyGame extends BaseGame with HasTapableComponents {
Future<void> onLoad() async {
// Load images and sprites etc
add(MyComponent());
}
... your game code
}
class MyComponent extends SpriteAnimationComponent with Tapable {
...
#override
bool onTapUp(TapUpInfo event) {}
#override
bool onTapDown(TapDownInfo event) {}
#override
bool onTapCancel() {}
}

Related

Flutter flame drawRect tappable

I am drawing a lot of rectangles on the screen for a demo and would love to show which you clicked/tapped on. I have the option to move over the screen with and zoom in and out, but I can't get it working that you can show which rectangle is tapped (the tapped function in the positionComponent are not firing). Does someone know if I am forgetting something or are using the wrong technique to achieve this?
class GameEngine extends FlameGame
with ScaleDetector, ScrollDetector, HasTappables {
final List<Paint> colors;
late double startZoom;
GameEngine(this.colors);
#override
Future<void>? onLoad() {
camera.speed = 250;
for (int i = 0; i < 250; i++) {
for (int j = 0; j < 300; j++) {
Paint color = colors[(i * (j + 1)) + j];
add(Rectangle(color, j * 50, i * 30));
}
}
return super.onLoad();
}
#override
void onScaleStart(info) {
startZoom = camera.zoom;
}
#override
void onScaleUpdate(ScaleUpdateInfo info) {
final currentScale = info.scale.global;
if (!currentScale.isIdentity()) {
camera.zoom = startZoom * currentScale.y;
} else {
camera.translateBy(-info.delta.game);
camera.snap();
}
}
#override
void onScroll(PointerScrollInfo info) {
double zoomValue = info.raw.scrollDelta.dy;
if (zoomValue > 0) {
camera.zoom = camera.zoom * 0.75;
} else {
camera.zoom = camera.zoom * 1.25;
}
return super.onScroll(info);
}
}
class Rectangle extends PositionComponent with Tappable {
final Paint color;
final double left;
final double top;
Rectangle(this.color, this.left, this.top);
#override
void render(Canvas canvas) {
canvas.drawRect(Rect.fromLTWH(left, top, 50, 30), color);
}
#override
bool onTapUp(TapUpInfo info) {
print("tap up");
return true;
}
#override
bool onTapDown(TapDownInfo info) {
print("tap down");
return true;
}
#override
bool onTapCancel() {
print("tap cancel");
return true;
}
}
Your rectangle doesn't set the size and position, so Flame can not determine whether a tap is within the component or not.
I recommend that you extend the RectangleComponent instead of extending PositionComponent, since it's harder to miss something when using the RectangleComponent.
You can see the docs for RectangleComponent here.

Update touch position (slide) during time with Flame (Flutter)

in my game with Flame I have a space ship, and I want it to follow where I'm pressing with my finger. For the moment it goes to the first spot I touch until I leave the finger from the screen, but if I move keeping it on, it continues to follow where I initially pressed. Is there a way to update where my finger is(using HasTappableComponents)? Or should I change approach?
class SpaceFlame extends FlameGame with HasTappableComponents {
late Ship ship;
late TapHandler tapHandler;
#override
Future<void> onLoad() async {
ship = Ship()
..position = Vector2(size.x / 2, size.y / 2)
..size = Vector2(64, 64)
..setSpeed(100);
tapHandler = TapHandler();
ship.anchor = Anchor.center;
await addAll([
Background(Palette.background),
ship,
tapHandler,
]);
}
#override
void render(Canvas canvas) {
canvas.drawRect(canvasSize.toRect(), Paint()..color = Palette.buttonBlu);
}
#override
void update(double dt) {
// TODO: implement update
if (tapHandler.tapped) {
print('tapped');
Vector2 moveDirection = Vector2(
tapHandler.tapPosition.x - ship.position.x,
tapHandler.tapPosition.y - ship.position.y);
ship.setMoveDirection(moveDirection);
} else {
ship.setMoveDirection(Vector2.zero());
}
super.update(dt);
}
}
class TapHandler extends PositionComponent with TapCallbacks {
TapHandler() : super(anchor: Anchor.center);
bool tapped = false;
Vector2 _tapPosition = Vector2.zero();
final _paint = Paint()..color = const Color(0x00000000);
#override
void onGameResize(Vector2 canvasSize) {
super.onGameResize(canvasSize);
size = canvasSize;
position = canvasSize / 2;
}
Vector2 get tapPosition => _tapPosition;
#override
void render(Canvas canvas) {
canvas.drawRect(size.toRect(), _paint);
}
#override
void onTapDown(TapDownEvent event) {
print('object');
tapped = true;
_tapPosition = event.canvasPosition;
}
}
When you keep your finger on the screen and drag it the even is no longer a tap, but a drag.
To react to this event you need to use HasDraggableComponents on the game and DragCallbacks on the component. Then you need to implement onDragUpdate in your component and set the position to the position that the event reports for the drag.

How to detect device rotation (orientation change) in flutter?

My app has a draggable floating action button that could move everywhere on the screen and stay at that position when being dropped.
The code for the button is from a blogspot and not mine.
The problem is, since everything is set in portrait mode, and there is no detection when rotation occurs, the button could be unreachable when device rotates from portrait mode to landscape mode (if the button was previously on the bottom half of the screen in portrait mode).
Is there anyway to detect previous orientation in order to set the offset for the button again when it rotates?
Here is the code for the floating button class/widget
import 'package:flutter/material.dart';
class DraggableFloatingActionButton extends StatefulWidget {
final Widget child;
final double deviceWidth;
final double deviceHeight;
final Function onPressed;
final GlobalKey parentKey;
DraggableFloatingActionButton({
required this.child,
required this.deviceWidth,
required this.deviceHeight,
required this.onPressed,
required this.parentKey,
});
#override
State<StatefulWidget> createState() => _DraggableFloatingActionButtonState();
}
class _DraggableFloatingActionButtonState extends State<DraggableFloatingActionButton> {
final GlobalKey _key = GlobalKey();
bool _isDragging = false;
late Offset _offset;
late Offset _minOffset;
late Offset _maxOffset;
#override
void initState() {
_offset = Offset(widget.deviceWidth * 0.8, widget.deviceHeight * 0.75);
WidgetsBinding.instance.addPostFrameCallback(_setBoundary);
super.initState();
}
void _setBoundary(_) {
final RenderBox parentRenderBox =
widget.parentKey.currentContext?.findRenderObject() as RenderBox;
final RenderBox renderBox = _key.currentContext?.findRenderObject() as RenderBox;
try {
final Size parentSize = parentRenderBox.size;
final Size size = renderBox.size;
setState(() {
_minOffset = const Offset(0, 0);
_maxOffset = Offset(parentSize.width - size.width, parentSize.height - size.height);
});
} catch (e) {
print('catch: $e');
}
}
void _updatePosition(PointerMoveEvent pointerMoveEvent) {
double newOffsetX = _offset.dx + pointerMoveEvent.delta.dx;
double newOffsetY = _offset.dy + pointerMoveEvent.delta.dy;
if (newOffsetX < _minOffset.dx) {
newOffsetX = _minOffset.dx;
} else if (newOffsetX > _maxOffset.dx) {
newOffsetX = _maxOffset.dx;
}
if (newOffsetY < _minOffset.dy) {
newOffsetY = _minOffset.dy;
} else if (newOffsetY > _maxOffset.dy) {
newOffsetY = _maxOffset.dy;
}
setState(() {
_offset = Offset(newOffsetX, newOffsetY);
});
}
#override
Widget build(BuildContext context) {
return Positioned(
left: _offset.dx,
top: _offset.dy,
child: Listener(
onPointerMove: (PointerMoveEvent pointerMoveEvent) {
_updatePosition(pointerMoveEvent);
setState(() {
_isDragging = true;
});
},
onPointerUp: (PointerUpEvent pointerUpEvent) {
print('onPointerUp');
if (_isDragging) {
setState(() {
_isDragging = false;
});
} else {
widget.onPressed();
}
},
child: Container(
key: _key,
child: widget.child,
),
),
);
}
}
As you can see, it only sets the button's offset based on portrait mode once in initState function, and doesn't deal with rotation.
A walk around that I could think of at this moment is just having another floating button specifically set for landscape mode.
Thank you in advance for any answer.
Ok, so I still don't know how to detect rotation, but my walk around works.
All I do is create another class(widget) for a landscape floating button.
In main, check device orientation to use either the floating button portrait class or landscape class.
The only thing that I change in the landscape button class is
from
_offset = Offset(widget.deviceWidth * 0.8, widget.deviceHeight * 0.75);
to
_offset = Offset(widget.deviceHeight * 1.75, widget.deviceWidth * 0.3);
You shouldn't care about orientation. You should use something like LayoutBuilder to determine your space available, and make breakpoint decisions based on that. LayoutBuilder will update immediately if the device space changes, and you should react to that by doing another layout.

How to add a sprite on top of another sprite in Flame Flutter?

The app has a background image with using SpriteComponent and I try to add another Tile image on top of background image. Why the crate.png image doesn't appear on top of background image. Why?
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
void main() async {
runApp(GameWidget(
game: CGame(),
));
}
class CGame extends FlameGame {
#override
Future<void>? onLoad() async {
SpriteComponent crate = SpriteComponent()
..sprite = await loadSprite('crate.png')
..size = Vector2(100, 100)
..position = Vector2(250, 300);
add(crate);
add(Background()); //<== comment this line to see the loaded crate.png
add(Crate());
return super.onLoad();
}
}
class Background extends SpriteComponent with HasGameRef {
#override
Future<void>? onLoad() async {
sprite = await Sprite.load('background.png');
size = gameRef.size;
return super.onLoad();
}
#override
void render(Canvas canvas) {
super.render(canvas);
}
}
class Crate extends SpriteComponent with HasGameRef {
#override
Future<void>? onLoad() async {
sprite = await Sprite.load('crate.png');
size = Vector2(100, 100);
position = Vector2(200, 200);
return super.onLoad();
}
}
Your crate will be behind the background because it loads faster and therefore is added to the component tree faster, to solve this you'll have to set the priority of the crate:
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
void main() async {
runApp(GameWidget(
game: CGame(),
));
}
class CGame extends FlameGame {
#override
Future<void>? onLoad() async {
add(Background());
add(Crate());
return super.onLoad();
}
}
class Background extends SpriteComponent with HasGameRef {
#override
Future<void>? onLoad() async {
sprite = await Sprite.load('background.png');
size = gameRef.size;
return super.onLoad();
}
}
class Crate extends SpriteComponent with HasGameRef {
Crate() : super(priority: 1); // <-- This is what changed
#override
Future<void>? onLoad() async {
sprite = await Sprite.load('crate.png');
size = Vector2(100, 100);
position = Vector2(200, 200);
return super.onLoad();
}
}
The default priority of components are 0 and the higher the priority is the "closer" to you the component will be, like a higher z-index. So here when we set the crate's priority to 1 it will always appear in front of the background which still has the default priority 0.
And a side note: you don't have to call the render method manually in Background, it is done automatically.
I could solve the issue by adding
SpriteComponent spriteCompo = SpriteComponent()
..sprite = await loadSprite('background.png')
..size = size;
add(spriteCompo);
to the CGame class, but there is a question why it is not working when calling a class Background?
Working snippet:
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
void main() async {
runApp(GameWidget(
game: CGame(),
));
}
class CGame extends FlameGame {
#override
Future<void>? onLoad() async {
SpriteComponent spriteCompo = SpriteComponent()
..sprite = await loadSprite('background.png')
..size = size;
add(spriteCompo);
// add(Background());
add(Crate());
return super.onLoad();
}
}
class Background extends SpriteComponent with HasGameRef {
#override
Future<void>? onLoad() async {
sprite = await Sprite.load('background.png');
size = gameRef.size;
return super.onLoad();
}
}
class Crate extends SpriteComponent with HasGameRef {
#override
Future<void>? onLoad() async {
sprite = await Sprite.load('crate.png');
size = Vector2(100, 100);
position = Vector2(200, 200);
return super.onLoad();
}
}

How to detect tap on Flame components?

I am using Flutter and flame (0.29.3) to build a simple mobile game. I am trying to detect taps on PositionComponent/SpriteComponent. However, I fail to do so. References/tutorials are using addGestureRecognizer but it is deprecated.
I define my PositionComponent is as follows;
class Enemy extends PositionComponent with Tapable {
Rect enemyRect;
Enemy(double x, double y) {
enemyRect = Rect.fromLTWH(x, y, 50, 50);
}
#override
void render(Canvas c) {
Color color = Color(0XFFFF0000);
Paint enemyColor = Paint()..color = color;
c.drawRect(enemyRect, enemyColor);
}
void update(double t) {}
#override
void onTapUp(TapUpDetails details) {
print("tap up");
}
#override
void onTapDown(TapDownDetails details) {
print("tap down");
}
#override
void onTapCancel() {
print("tap cancel");
}
}
And, I add PositionComponent to my game.
class GameController extends BaseGame with HasTapableComponents {
Size screenSize;
double tileSize;
Player player;
Enemy enemy;
TapableComponent a;
GameController() {
initialize();
}
void initialize() async {
resize(await Flame.util.initialDimensions());
add(enemy = Enemy(200, 200));
}
#override
void render(Canvas c) {
Rect background = Rect.fromLTWH(0, 0, screenSize.width, screenSize.height);
Paint backgroundPaint = Paint()..color = Color(0xFFFAFAFA);
enemy.render(c);
}
#override
void update(double t) {}
#override
void resize(Size size) {
screenSize = size;
tileSize = screenSize.width / 10;
}
}
However, it's not working, am I missing something?
I think your example code is a mix between v1 code and 0.29.3, if you try with the latest release candidate of Flame: 1.0.0-rc8 then the following should work:
class TapableSquare extends PositionComponent with Tapable {
static final Paint _white = Paint()..color = const Color(0xFFFFFFFF);
static final Paint _grey = Paint()..color = const Color(0xFFA5A5A5);
TapableSquare({Vector2 position})
: super(
position: position ?? Vector2.all(100),
size: Vector2.all(100),
);
#override
void render(Canvas canvas) {
super.render(canvas);
canvas.drawRect(size.toRect());
}
#override
bool onTapUp(TapUpDetails details) {...}
#override
bool onTapDown(TapDownDetails details) {...}
#override
bool onTapCancel() {...}
}
class TapablesGame extends BaseGame with HasTapableComponents {
#override
Future<void> onLoad() async {
add(TapableSquare());
}
}
Extracted from this example.
EDIT: To make it work for 0.29.3 you should be setting position and size in your Enemy class instead of building your own rect, Tapable can't know about that rect.