Flutter desktop custom game loop appears to drop frames - flutter

I'm testing a basic game loop for Flutter Desktop using the code below but it is not entirely smooth. It sort of seems to very briefly pause every half second or so and jump forward. I suspect that some frames are being dropped but there is nothing intensive going on so I can't see why.
The first thing I though of was that perhaps I need to adjust my update() function to allow for the exact microsecond delta since the last frame - but that didn't fix it regardless of whether I used the timestamp provided by the gameLoop() callback or measured it directly from DateTime.now().
I'm running on a fast gaming laptop so hardware isn't the problem. I am starting to think it could be something in the Flutter framework itself. Perhaps it drops a frame because it is garbage collecting? Or perhaps it skips a frame if it doesn't detect it has changed?
By comparison I created a similar loop in javascript which ran perfectly smoothly. The image used in both the Flutter and Javascript tests was a test 60wx72h png in the local directory.
I'd appreciate any pointers as to whether this is something in my code or something in the Flutter framework.
By the way, there is a discussion of this issue on the Flutter group which explores some other approaches to this problem. See flutter-dev.
Flutter/Dart:
import 'dart:typed_data';
import 'dart:ui';
import 'package:flutter/material.dart' hide Image;
import 'package:flutter/services.dart';
Canvas canvas;
Sprite sprite;
PictureRecorder pictureRecorder;
SceneBuilder builder;
main() async {
WidgetsFlutterBinding.ensureInitialized();
WidgetsBinding.instance.window.onBeginFrame = gameLoop;
sprite = Sprite();
WidgetsBinding.instance.window.scheduleFrame();
}
void gameLoop(Duration now) {
pictureRecorder = PictureRecorder();
canvas = Canvas(pictureRecorder, Rect.fromLTWH(
0.0, 0.0, WidgetsBinding.instance.window.physicalSize.width, WidgetsBinding.instance.window.physicalSize.height));
sprite.update();
sprite.render();
builder = SceneBuilder();
builder.addPicture(Offset.zero, pictureRecorder.endRecording());
WidgetsBinding.instance.window.render(builder.build());
WidgetsBinding.instance.window.scheduleFrame();
}
class Sprite {
double x=0, y=400, dx=8, dy=0;
String imageName = 'assets/bobR.png';
Image image;
void update(){
x+= dx;
y+= dy;
if((x<0 && dx<0)||(x>1000 && dx>0)) dx=-dx;
if((y<0 && dy<0)||(y>1000 && dy>0)) dy=-dy;
}
void render(){
if (image!=null) canvas.drawImage(image, Offset(x,y), Paint());
}
Sprite(){
loadAssets();
}
void loadAssets(){
rootBundle.load(imageName).then((byteData){
Uint8List lst = Uint8List.view(byteData.buffer);
instantiateImageCodec(lst).then((codec){
codec.getNextFrame().then((frameInfo){
image = frameInfo.image;
});
});
});
}
}
Javascript equivalent:
<!DOCTYPE html>
<html>
<head>
<script type="application/javascript">
"use strict";
let canvas;
let context;
let image = new Image();
let x=0,y=0;
let dx=4,dy=0;
let turnX, turnY;
image.src = "bobR.png";
window.onload = init;
function resize(){
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
turnX = canvas.width - image.width;
turnY = canvas.height - image.height;
}
function init(){
canvas = document.getElementById('canvas');
context = canvas.getContext('2d');
window.onresize = resize;
resize();
window.requestAnimationFrame(gameLoop);
}
function gameLoop(timeStamp){
context.clearRect(0, 0, canvas.width, canvas.height);
update();
render();
window.requestAnimationFrame(gameLoop);
}
function update(){
x += dx;
y += dy;
if ((x<0 && dx<0) || (x>turnX && dx>0)) dx = -dx;
if ((y<0 && dy<0) || (y>turnY && dy>0)) dy = -dy;
}
function render(){
context.drawImage(image, x, y);
}
</script>
<body>
<canvas id="canvas">To play this game, use a more up to date browser</canvas>
</body>
</html>
EDIT
I tried using the Flame game engine but still get the same slightly unsmooth motion compared to Javascript. Code is below:
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flame/game.dart';
import 'package:flame/flame.dart';
import 'package:flame/position.dart';
import 'package:flame/sprite.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
TestGame game = TestGame();
runApp(game.widget);
}
class TestGame extends Game {
Size screenSize;
Player player;
TestGame() {
initialize();
}
void initialize() async {
Flame.images.load('bobR.png');
player = Player();
resize(await Flame.util.initialDimensions());
}
void render(Canvas canvas) {
Rect bgRect = Rect.fromLTWH(0, 0, screenSize.width, screenSize.height);
Paint bgPaint = Paint();
bgPaint.color = Colors.white;
canvas.drawRect(bgRect, bgPaint);
player.render(canvas);
}
void update(double t) {
player.update(t);
}
void resize(Size size) {
screenSize = size;
}
}
class Player {
Position p = Position(0,400); double dx = 8, dy = 0;
Sprite sprite;
Player() {
sprite = Sprite('bobR.png');
}
void render(Canvas c) {
sprite.renderPosition(c, p);
}
void update(double t) {
p.x += dx;
p.y += dy;
if ((p.x < 0 && dx < 0) || (p.x > 1000 && dx > 0))
dx = -dx;
if ((p.y < 0 && dy < 0) || (p.y > 1000 && dy > 0))
dy = -dy;
}
}

Allocations in tight loops (such as gameLoop) can create pressure on the garbage collector.
Try moving your allocations out of your game loop by setting up an initialization routine that only runs once and reuse the objects created.
The JavaScript implementation does just that, which is why it runs smoothly.
I recommend you check out Flame https://flame-engine.org/docs/#/ which is a minimal game engine on top of Flutter.

Try drawing using the Canvas.drawRawAtlas method
An extended example can be found here
I also recorded a video, as I am interested in checking what Flutter is capable of and for which games it is suitable.
At 500 sprites, some GPU processes after Flutter Engine start to go beyond 16 ms in one frame.

Related

GPU Jank when generate endless 2D terrain

I'm making a runner game which has the infinity mode.
So, I got to generate a terrain every 1s approximately.
It has some lag with my LG Q9 One, I checked the performance and it appeared that there's some problem with Raster thread.
I tried the game on Samsung Galaxy Note 10 which has better gpu and it shows less janks(but it has janks anyway).
I adds my code here. Any better way to enhance the performance?
class MyGame extends FlameGame {
final Grid grid = Grid();
final EndlessMap endlessMap = EndlessMap();
final Runner runner = Runner();
double speed = 100.0;
#override
Future<void>? onLoad() {
add(grid);
add(endlessMap);
add(runner);
return super.onLoad();
}
}
/// Camera follows the runner.
class Runner extends PositionComponent with HasGameRef<MyGame> {
#override
Future<void>? onLoad() {
gameRef.camera.followComponent(this);
return super.onLoad();
}
#override
void update(double dt) {
x += gameRef.speed * dt;
super.update(dt);
}
}
/// Set the size of tile and grid
class Grid extends Component with HasGameRef<MyGame> {
late int rows = 10;
late int columns;
late int mapColumns;
late double tileSize;
#override
void onGameResize(Vector2 size) {
tileSize = (size.y / rows).floorToDouble();
columns = (size.x / tileSize).ceil();
mapColumns = columns + 2;
super.onGameResize(size);
}
}
/// Generate terrains endlessly
class EndlessMap extends PositionComponent with HasGameRef<MyGame> {
late final Sprite terrainSprite;
late final List<SpriteComponent> terrainSpritePool;
int firstTerrainIndex = 0;
// Init terrain sprite
#override
Future<void> onLoad() async {
terrainSprite = Sprite(
await Flame.images.load('terrains.png'),
srcPosition: Vector2(4.0, 0.0),
srcSize: Vector2(32.0, 32.0 * 10),
);
terrainSpritePool = List.generate(
gameRef.grid.mapColumns,
(index) => SpriteComponent(sprite: terrainSprite),
);
for (var i = 0; i < terrainSpritePool.length; i++) {
terrainSpritePool[i].size.x = gameRef.grid.tileSize;
terrainSpritePool[i].position = Vector2(
i * gameRef.grid.tileSize,
gameRef.grid.tileSize,
);
add(terrainSpritePool[i]);
}
return super.onLoad();
}
#override
void update(double dt) {
final dx = gameRef.speed * dt;
final lastTerrainIndex = firstTerrainIndex == 0
? terrainSpritePool.length - 1
: firstTerrainIndex - 1;
// When the first terrain is behind the camera,
if (terrainSpritePool[firstTerrainIndex].position.x +
gameRef.grid.tileSize <=
gameRef.camera.position.x + dx) {
// Move the first terrain to the end
terrainSpritePool[firstTerrainIndex].size.x = gameRef.grid.tileSize;
terrainSpritePool[firstTerrainIndex].position = Vector2(
terrainSpritePool[lastTerrainIndex].position.x + gameRef.grid.tileSize,
gameRef.grid.tileSize,
);
firstTerrainIndex = (firstTerrainIndex + 1) % terrainSpritePool.length;
}
super.update(dx);
}
}
You can find a whole code in github.
2022.09.12. ----------------
It seems like the issue with that my phone(LG Q9 One) has a bad gpu to run it normally. I removed all the codes except moving the camera, it still shows janks.
In order to reduce the shader compilation and run your animations smoothly, there is an article in the official flutter website explaining about a solution
Hope this helps
Try to not create new objects in the update method.
So when you are doing:
terrainSpritePool[firstTerrainIndex].position = Vector2(
terrainSpritePool[lastTerrainIndex].position.x + gameRef.grid.tileSize,
gameRef.grid.tileSize,
);
Since the SpriteComponent already has a Vector2 you can re-use it like this:
terrainSpritePool[firstTerrainIndex].position.setValues(
terrainSpritePool[lastTerrainIndex].position.x + gameRef.grid.tileSize,
gameRef.grid.tileSize,
);

Flutter/Flame Why my code is not working normally and getting buggy

I followed a youtube tutorial about flutter flame game design which was published recently (Flame v1.2.0) so all the versions should be up to date. But when i write the code its not working like it should. Here is a youtube link of video so you can see how it should work: https://www.youtube.com/watch?v=kknJMhnKYNc
Can someone please explain to me why my code is bugging my character (it spins so fast) while moving and it can go outside of map at bottom and right side of my screen. I cant go further because of this and you are my only hope. Here is my code:
void main() {
WidgetsFlutterBinding.ensureInitialized();
Flame.device.fullScreen();
Flame.device.setLandscape();
runApp(GameWidget(game: MyGame()));
}
class MyGame extends FlameGame with HasDraggables {
SpriteComponent background = SpriteComponent();
late SpriteAnimationComponent ghost;
late final JoystickComponent joystick;
bool ghostFlipped = false;
#override
Future<void> onLoad() async {
await super.onLoad();
var ghostImage = await images.load('ghost.png');
//Loading background
add(background
..sprite = await loadSprite('newyork.jpg')
..size = size);
//adding joystick for controlling to Ghost
final buttonPaint = BasicPalette.red.withAlpha(150).paint();
final backgroundPaint = BasicPalette.black.withAlpha(100).paint();
joystick = JoystickComponent(
knob: CircleComponent(radius: 30, paint: buttonPaint),
background: CircleComponent(radius: 100, paint: backgroundPaint),
margin: const EdgeInsets.only(left: 40, bottom: 40));
add(joystick);
//Loading ghost character
var ghostAnimation = SpriteAnimation.fromFrameData(
ghostImage,
SpriteAnimationData.sequenced(
amount: 4, stepTime: 0.17, textureSize: Vector2(32, 32)));
ghost = SpriteAnimationComponent()
..animation = ghostAnimation
..size = Vector2(120,120)
..position = Vector2(500, 250);
//ghost.flipHorizontallyAroundCenter();
add(ghost);
}
#override
void update(double dt) {
super.update(dt);
bool moveUp = joystick.relativeDelta[1] < 0;
bool moveDown = joystick.relativeDelta[1] > 0;
bool moveLeft = joystick.relativeDelta[0] < 0;
bool moveRight = joystick.relativeDelta[0] > 0;
double ghostVectorX = (joystick.relativeDelta * 300 * dt) [0];
double ghostVectorY = (joystick.relativeDelta * 300 * dt) [1];
//When ghost is moving on X direction
if((moveLeft && ghost.x > 0) || (moveRight && ghost.x < size[0])) {
ghost.position.add(Vector2(ghostVectorX,0));
}
//when ghost is moving on Y direction
if((moveUp && ghost.y > 0) || (moveDown && ghost.y < size[1])){
ghost.position.add(Vector2(0, ghostVectorY));
}
if(joystick.relativeDelta[0] < 0 && ghostFlipped) {
ghostFlipped = true;
ghost.flipHorizontallyAroundCenter();
}
if(joystick.relativeDelta[0] > 0 && !ghostFlipped) {
ghostFlipped = false;
ghost.flipHorizontallyAroundCenter();
}
}
}
I'm not sure what you mean with "spins so fast", but I'm guessing that the animation is playing too fast?
To resolve that, set the stepTime to something higher (where you have 0.17 here):
SpriteAnimationData.sequenced(
amount: 4, stepTime: 0.17, textureSize: Vector2(32, 32)));
To solve the problem you're having with the ghost being able to exit to the right and in the bottom you have to add a check with the ghosts width and height taken into account when you check against the screen size. The default anchor is top left, so that is where the position will be checked from.
In the following code snippet you can see how I've added ghost.width and ghost.height to the check against the game size:
if((moveLeft && ghost.x > 0) || (moveRight && ghost.x + ghost.width < size.x)) {
ghost.position.add(Vector2(ghostVectorX,0));
}
//when ghost is moving on Y direction
if((moveUp && ghost.y > 0) || (moveDown && ghost.y + ghost.height < size.y)){
ghost.position.add(Vector2(0, ghostVectorY));
}

how can I apply gravity to my gameobject in flutters flame library

I am having trouble applying gravity to a Rect object I want to fall freely along the Y-axis by updating Rect's position In my code with the body.position.y. Here is a snippet of my code :
import 'dart:ui';
import 'package:box2d_flame/box2d.dart';
import 'package:flame/sprite.dart';
import 'package:mimo/mimo-game.dart';
class Mimo {
final MimoGame game;
CircleShape shape;
Body body;
List<Sprite> mimoSprite;
List<Sprite> deadSprite;
double flyingSpriteIndex = 0;
Rect mimoRect;
bool isDead = false;
bool isOffScreen = false;
Mimo(this.game, double x, double y) {
shape = CircleShape(); //build in shape, just set the radius
shape.p.setFrom(Vector2.zero());
shape.radius = .1; //10cm ball
BodyDef bd = BodyDef();
bd.position = new Vector2(x,y);
bd.type = BodyType.DYNAMIC;
body = game.world.createBody(bd);
body.userData = this;
FixtureDef fd = FixtureDef();
fd.restitution = 0.5;
fd.density = 0.05;
fd.friction = 0;
fd.shape = shape;
body.createFixtureFromFixtureDef(fd);
mimoSprite = List();
mimoSprite.add(Sprite('mimo/mimo-1.png'));
mimoSprite.add(Sprite('mimo/mimo-2.png'));
mimoSprite.add(Sprite('mimo/mimo-3.png'));
mimoSprite.add(Sprite('mimo/mimo-4.png'));
mimoSprite.add(Sprite('mimo/mimo-5.png'));
deadSprite = List();
mimoRect = Rect.fromLTWH(body.position.x, body.position.y, game.mimoSize, game.mimoSize);
}
void render(Canvas c) {
mimoSprite[flyingSpriteIndex.toInt()].renderRect(c, mimoRect.inflate(2));
}
void update(double t) {
mimoRect = mimoRect.translate(body.position.x, body.position.y);
}
}
in the update method I use the line mimoRect = mimoRect.translate(body.position.x, body.position.y); so the body.position can update my Rect object in real-time. but nothing happens as my spawned object is always fixed at a position and does not move. I decided to log body.position.y to the console and noticed it does not change.
In my main class, I create a world object like this :
//Needed for Box2D
static const int WORLD_POOL_SIZE = 100;
static const int WORLD_POOL_CONTAINER_SIZE = 10;
//Main physic object -> our game world
World world;
//Zero vector -> no gravity
final Vector2 _gravity = Vector2(0, 4.0);
Body body;
CircleShape shape;
//Scale to get from rad/s to something in the game, I like the number 5
double sensorScale = 5;
//Draw class
Paint paint;
//Initial acceleration -> no movement as its (0,0)
Vector2 acceleration = Vector2.zero();
MimoGame() {
world = new World.withPool(_gravity,
DefaultWorldPool(WORLD_POOL_SIZE, WORLD_POOL_CONTAINER_SIZE));
initialize();
}
Please any idea on what I can do?
You need to use the Box2DGame (flame 0.22.0^) class in flame for your game and the object that you want to apply gravity to needs to extend BodyComponent, then you create an instance of your BodyComponent in your Box2DGame and use the add function to add it to the game loop which will make sure that update and render is called on it every iteration.
There is a good example in the flame source here.
Also remember that if your gravity is positive, your object will be flying upwards.

Flutter update is giving me this error: The method '*' was called on null

I have a flutter app using the flame library. I'm trying to make an object move in a flutter game. When I run the update function, I get the following error:
The method '*' was called on null.
Receiver: null
Tried calling: *(0.0)
Seems like something isn't initialized and the update function is ran before that something is initialized. When I comment out player.update(t) it works, but the update function doesn't get called. What am I doing wrong ad how can I fix it? Here's my code:
Game Controller Class
class GameController extends Game {
Size screenSize;
Player player;
GameController() {
initialize();
}
void initialize() async {
final initDimetion = await Flame.util.initialDimensions();
resize(initDimetion);
player = Player(this);
}
void render(Canvas c) {
Rect bgRect = Rect.fromLTWH(0, 0, screenSize.width, screenSize.height);
Paint bgPaint = Paint()..color = Color(0xFFFAFAFA);
c.drawRect(bgRect, bgPaint);
player.render(c);
}
void update(double t) {
if (player is Player) { // Tried adding this if statement but it didn't work
player.update(t);
}
}
void resize(Size size) {
screenSize = size;
}
}
Player Class
class Player {
final GameController gameController;
Rect playerRect;
double speed;
Player(this.gameController) {
final size = 40.0;
playerRect = Rect.fromLTWH(gameController.screenSize.width / 2 - size / 2,
gameController.screenSize.height / 2 - size / 2, size, size);
}
void render(Canvas c) {
Paint color = Paint()..color = Color(0xFF0000FF);
c.drawRect(playerRect, color);
}
void update(double t) {
double stepDistance = speed * t;
Offset stepToSide = Offset.fromDirection(90, stepDistance);
playerRect = playerRect.shift(stepToSide);
}
}
You never initialize the speed attribute of Player to a value. So speed * t in Player.update causes this error.
Simply initialize the speed attribute in the constructor
Player(this.gameController) {
final size = 40.0;
this.speed = 0;
playerRect = Rect.fromLTWH(gameController.screenSize.width / 2 - size / 2,
gameController.screenSize.height / 2 - size / 2, size, size);
}

Flutter: Concurrent modification during iteration: Instance(length:2) of '_GrowableList'

This is a game with a small rectangle randomly appearing on the screen and the player has to tap it in order to get rid of it and a new one will spawn on another part of the screen.
My code works and all but every time I tap the rectangle I drew on the screen, I get the error below. How do I fix this?
Main.dart:
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flame/flame.dart';
import 'package:flame/util.dart';
import 'package:flutter/services.dart';
import 'package:langaw/langaw-game.dart';
import 'package:flutter/gestures.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
Util flameUtil = Util();
await flameUtil.fullScreen();
await flameUtil.setOrientation(DeviceOrientation.portraitUp);
LangawGame game = LangawGame();
runApp(game.widget);
TapGestureRecognizer tapper = TapGestureRecognizer();
tapper.onTapDown = game.onTapDown;
flameUtil.addGestureRecognizer(tapper);
}
fly.dart:
import 'dart:ui';
import 'package:flutter/cupertino.dart';
import 'package:langaw/langaw-game.dart';
class Fly {
Rect flyRect;
Paint flyPaint;
bool isDead = false, isOffScreen = false;
final LangawGame game;
// initialized with the game instance, along with positions x and y
Fly({this.game, double x, double y}) {
flyRect = Rect.fromLTWH(x, y, game.tileSize, game.tileSize);
flyPaint = Paint();
flyPaint.color = Color(0xff6ab04c);
}
// needs a render and update method
// this method will do the drawing
void render(Canvas canvas) {
canvas.drawRect(flyRect, flyPaint);
}
void update(double t) {
if (isDead) {
flyRect = flyRect.translate(0, game.tileSize * 12 * t);
if (flyRect.top > game.screenSize.height) {
isOffScreen = true;
}
}
}
void onTapDown() {
flyPaint.color = Color(0xffff4757);
isDead = true;
game.spawnFly();
}
}
langaw-game.dart:
import 'dart:ui';
import 'package:flame/game.dart';
import 'package:flame/flame.dart';
import 'package:langaw/components/fly.dart';
import 'dart:math';
import 'package:flutter/gestures.dart';
class LangawGame extends Game {
Size screenSize;
double tileSize;
List<Fly> flies;
Random rnd;
LangawGame() {
initialize();
}
void initialize() async {
flies = List<Fly>();
rnd = Random();
resize(await Flame.util.initialDimensions());
spawnFly();
}
// this method will do the drawing
void render(Canvas canvas) {
Rect bgRect = Rect.fromLTWH(0, 0, screenSize.width, screenSize.height);
Paint bgPaint = Paint();
bgPaint.color = Color(0xff576574);
canvas.drawRect(bgRect, bgPaint);
// now the flies are rendered on top of the background
flies.forEach((Fly fly) => fly.render(canvas));
// print(screenSize.toString());
}
void update(double t) {
flies.forEach((Fly fly) => fly.update(t));
flies.removeWhere((Fly fly) => fly.isOffScreen);
}
void spawnFly() {
double xPos = rnd.nextDouble() * (screenSize.width - tileSize);
double yPos = rnd.nextDouble() * (screenSize.height - tileSize);
flies.add(Fly(game: this, x: xPos, y: yPos));
}
void resize(Size size) {
screenSize = size;
tileSize = screenSize.width / 9;
}
void onTapDown(TapDownDetails d) {
flies.forEach((Fly fly) {
if (fly.flyRect.contains(d.globalPosition)) {
fly.onTapDown();
}
});
}
}
The following ConcurrentModificationError was thrown while handling a gesture:
Concurrent modification during iteration: Instance(length:2) of '_GrowableList'.
When the exception was thrown, this was the stack:
#0 List.forEach (dart:core-patch/growable_array.dart:286:36)
#1 LangawGame.onTapDown (package:langaw/langaw-game.dart:57:11)
#2 TapGestureRecognizer.handleTapDown.<anonymous closure> (package:flutter/src/gestures/tap.dart:463:51)
#3 GestureRecognizer.invokeCallback (package:flutter/src/gestures/recognizer.dart:182:24)
#4 TapGestureRecognizer.handleTapDown (package:flutter/src/gestures/tap.dart:463:11)
...
Handler: "onTapDown"
Recognizer: TapGestureRecognizer#a4e24
state: possible
button: 1
I ran in the same problem today when following the tutorial on flutter game (nice tutorial BTW except I think this small bug, I will post a comment soon when I have time).
The problem is that the LangawGame.onTapDown iterates on the flies list, and during the iteration calls Fly.onTapDown() who adds an element to the list in the spawnFly method.
So the list gets modified while it is being iterated... which IMHO sounds like a bug.
The simple solution is to make a copy of the list for iteration with List.from:
ie in LangawGame:
void onTapDown(TapDownDetails d) {
List<Fly>.from(flies).forEach((Fly fly) {
if (fly.flyRect.contains(d.globalPosition)) {
fly.onTapDown();
}
});
There are more performant solutions, but with more code :)
Remove following code in in class Fly:
game.spawnFly();
Add the following code in langaw-game.dart -> update()
if(flies.length<1){
spawnFly();
}
You can use ( Dart documentation: https://api.dartlang.org/stable/2.1.0/dart-core/Map/removeWhere.html )
Map.removeWhere((key, value) => toRemove.contains(key));