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

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

Related

Flutter Flame priority value is not working

I have SpriteComponent named Obstacle. I set the priority value to 0 in the super method. But the Obstacle component is regenerated with the update method. It disappears when it exits the screen. When it reoccurs, the priorty value becomes invalid. I am sharing sample screenshots.
first img
second img
The player stays behind the obstacle. GameOverPanel always at the back.
#override
Future<void> onLoad() async {
add(ScreenHitbox());
add(gameRef.homeBackgroundParallaxComponent);
add(gameRef.homeBaseParallax);
add(gameRef.obstacleManager..priority = 0);
add(gameRef.playerManager..priority = 1);
add(gameRef.gameOverPanel..priority = 2);
}
#override
void update(double dt) {
super.update(dt);
gameRef.obstacleManager.updateObstacle(GameConst.gameSpeed * dt);
gameRef.playerManager.changePositionDown(value: GameConst.gravity * dt);
}
Game Over Panel
class GameOverPanel extends Component with HasGameRef<HopyBirdGame> {
GameOverPanel() : super(priority: 4);
#override
Future<void> onLoad() async {
add(GameOverText());
}
#override
void renderTree(Canvas canvas) {
if (gameRef.gameOver) {
super.renderTree(canvas);
}
}
}
class GameOverText extends SpriteComponent with HasGameRef<HopyBirdGame> {
GameOverText() : super(size: Vector2(300, 80), anchor: Anchor.center);
#override
Future<void>? onLoad() async {
sprite = await gameRef.loadSprite(Assets.gameOver.path);
position = Vector2(gameRef.screenWidth / 2, gameRef.screenHeight / 2);
return super.onLoad();
}
}
-Obstacle
class Obstacle extends SpriteComponent
with HasGameRef<HopyBirdGame>, CollisionCallbacks {
final ObstacleType obstacleType;
Obstacle({
required this.obstacleType,
super.position,
super.priority,
}) : super(
size: Vector2(GameConst.obstacleWidth, GameConst.obstacleHeight),
anchor: Anchor.center,
);
Obstacle.empty({this.obstacleType = ObstacleType.empty});
#override
Future<void> onLoad() async {
switch (obstacleType) {
case ObstacleType.up:
sprite = await gameRef.loadSprite(
Assets.redPipeUp.path,
);
break;
case ObstacleType.down:
sprite = await gameRef.loadSprite(
Assets.redPipeDown.path,
);
break;
case ObstacleType.empty:
return;
}
add(RectangleHitbox());
}
}
class Obstacles extends Component with HasGameRef<HopyBirdGame> {
final Obstacle upObstacle;
final Obstacle downObstacle;
bool isCreateNextObstacles = true;
Obstacles({
required this.upObstacle,
required this.downObstacle,
super.priority,
});
#override
Future<void>? onLoad() {
upObstacle.priority = 1;
downObstacle.priority = 1;
createChildren();
return super.onLoad();
}
void createChildren() {
final children = [upObstacle, downObstacle];
addAll(children);
}
}
Obstacle Manager
class ObstacleManager extends Component with HasGameRef<HopyBirdGame> {
ObstacleManager() : super(priority: 1);
ListQueue<Obstacles> history = ListQueue();
#override
Future<void> onLoad() async {
createObstacles();
}
void updateObstacle(double value) {
if (!gameRef.gameOver) {
if (history.isNotEmpty) {
history.last.upObstacle.position.x -= value;
history.last.downObstacle.position.x -= value;
if (history.last.upObstacle.position.x <= gameRef.screenWidth / 2) {
createObstacles();
final isThereAnyObstaclesOutsideScreen = history.any(
(element) =>
element.downObstacle.position.x <=
-(GameConst.obstacleWidth / 2),
);
if (isThereAnyObstaclesOutsideScreen) {
final obstaclesOutsideScreen = history
.lastWhere((element) => element.downObstacle.position.x <= 0);
obstaclesOutsideScreen.removeFromParent();
gameRef.remove(obstaclesOutsideScreen);
}
}
if (history.length >= 2) {
history.elementAt(history.length - 2).upObstacle.position.x -= value;
history.elementAt(history.length - 2).downObstacle.position.x -=
value;
}
}
}
}
void createObstacles() {
final obstaclesGap =
gameRef.screenHeight - gameRef.difficulty.spaceForPlayers;
final gapAboveTheObstacleOnScreen =
((2 * GameConst.obstacleHeight) - obstaclesGap) / 2;
final randomNumber = _randomGenerator(
min: -gameRef.difficulty.distanceAxisYToGoUpAndDown,
max: gameRef.difficulty.distanceAxisYToGoUpAndDown,
);
final axisYOfUpObstacle = (GameConst.obstacleHeight / 2) -
gapAboveTheObstacleOnScreen +
randomNumber;
final axisYOfDownObstacle = axisYOfUpObstacle +
GameConst.obstacleHeight +
gameRef.difficulty.spaceForPlayers;
final obstacles = Obstacles(
upObstacle: Obstacle(
obstacleType: ObstacleType.up,
position: Vector2(
gameRef.screenWidth + (GameConst.obstacleWidth / 2),
axisYOfUpObstacle,
),
),
downObstacle: Obstacle(
obstacleType: ObstacleType.down,
position: Vector2(
gameRef.screenWidth + (GameConst.obstacleWidth / 2),
axisYOfDownObstacle,
),
),
);
history.add(obstacles);
gameRef.add(obstacles);
}
double _randomGenerator({required double min, required double max}) {
final gap = (max - min).toInt();
return min + Random().nextInt(gap);
}
}
EDIT: The same problem applies in game over panel.
Game Over Panel
class GameOverPanel extends Component with HasGameRef<HopyBirdGame> {
GameOverPanel() : super();
#override
Future<void> onLoad() async {
add(GameOverText());
}
#override
void renderTree(Canvas canvas) {
if (gameRef.gameOver) {
super.renderTree(canvas);
}
}
}
class GameOverText extends SpriteComponent with HasGameRef<HopyBirdGame> {
GameOverText()
: super(
size: Vector2(300, 80),
anchor: Anchor.center,
);
#override
Future<void>? onLoad() async {
sprite = await gameRef.loadSprite(Assets.gameOver.path);
position = Vector2(gameRef.screenWidth / 2, gameRef.screenHeight / 2);
return super.onLoad();
}
}
Similarly, the first assignment is made in the Flame Game class.
I'm guessing (since the code for the ObstacleManager isn't present) that you are adding the obstacles directly to the game from the ObstacleManager to the component tree and that you aren't adding the priority to the actual Obstacle components.
If this is not the case, please update the question with the code for the ObstacleManager.
EDIT: After code upload.
It doesn't help that you are giving priorities to the manager classes when the components are not added as children to them, but to the gameRef. Since the player manager class isn't included in the question you either have to set the Obstacles to priority -1, or you have to give the player priority 1 (and obstacles will default to 0).
gameRef.add(obstacles..priority = -1);
This will make the obstacles be behind the background, so you have to also change the background parallaxes:
add(gameRef.homeBackgroundParallaxComponent..priority = -3);
add(gameRef.homeBaseParallax..priority = -2);
So it will be better to just set the player priority to 1 where you are adding that one to the game, because then you don't have to change all the other classes.

What is the purpose of "angle %= 2 * math.pi;" in the bellow code?

I am running this example from Flame engine source, and I wonder what is the purpose of angle %= 2 * math.pi; in the update method. Commenting it also doesn't affect the circular animation of rectangle! If there is a mathematical reason please put a link to the related article if possible.
import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame/input.dart';
import 'package:flame/palette.dart';
import 'package:flutter/material.dart';
void main() {
runApp(
GameWidget(
game: MyGame(),
),
);
}
/// This example simply adds a rotating white square on the screen, if you press
/// somewhere other than on the existing square another square will be added and
/// if you press on a square it will be removed.
class MyGame extends FlameGame with DoubleTapDetector, HasTappables {
bool running = true;
#override
Future<void> onLoad() async {
add(Square(Vector2(100, 200)));
}
#override
void onTapUp(int id, TapUpInfo info) {
super.onTapUp(id, info);
if (!info.handled) {
final touchPoint = info.eventPosition.game;
add(Square(touchPoint));
}
}
#override
void onDoubleTap() {
if (running) {
pauseEngine();
} else {
resumeEngine();
}
running = !running;
}
}
class Square extends PositionComponent with Tappable {
static const speed = 0.25;
static const squareSize = 128.0;
static Paint white = BasicPalette.white.paint();
static Paint red = BasicPalette.red.paint();
static Paint blue = BasicPalette.blue.paint();
Square(Vector2 position) : super(position: position);
#override
Future<void> onLoad() async {
super.onLoad();
size.setValues(squareSize, squareSize);
anchor = Anchor.center;
}
#override
void render(Canvas canvas) {
canvas.drawRect(size.toRect(), white);
canvas.drawRect(const Rect.fromLTWH(0, 0, 30, 30), red);
canvas.drawRect(Rect.fromLTWH(width / 2, height / 2, 3, 3), blue);
}
#override
void update(double dt) {
super.update(dt);
angle += speed * dt;
angle %= 2 * math.pi; //<---- What is the reason behind this line?
}
#override
bool onTapUp(TapUpInfo info) {
removeFromParent();
return true;
}
}
At some point, after a very long time if you don't have a high rotational speed, the angle will overflow what a double can fit.
The angle here is in radians, and one full circle is 2π (360°).
Since when we have rotated a full circle we can just as well go back to 0 again we use modulo to remove all the "unused" full circles that are in currently in angle.
What will happen if exceeds 2*pi.
Nothing will happen, it will just continue rotating, but angle will keep on growing to an unnecessarily large value.
It ensures that angle never exceeds 2×PI, the maximum value of an angle in radians. By performing a modulus by this value, any number that exceeds 2×PI will just wrap around from 0.

How to move a Vector2 using PositionComponent with Flame/Flutter?

I am trying to move a Vector2's x position, the update is being called, the x value is changing but the white square component does not move, what am I missing?
i am using flame 1.0.0-releasecandidate.11.
import 'dart:ui';
import 'package:flame/components.dart';
import 'package:flame/palette.dart';
class PlayerComponent extends PositionComponent {
static const squareSize = 128.0;
static Paint white = BasicPalette.white.paint();
#override
void render(Canvas canvas) {
canvas.drawRect(size.toRect(), white);
super.render(canvas);
}
#override
void update(double dt) {
x += 10 * dt;
print(x);
super.update(dt);
}
#override
void onMount() {
super.onMount();
size.setValues(squareSize, squareSize);
anchor = Anchor.center;
}
}
class AnimalVillageGame extends BaseGame {
#override
Future<void> onLoad() async {
add(PlayerComponent()
..x = 100
..y = 100
);
}
}
You render method renders the size as a zero-bound rectangle, size.toRect() returns a Rect at the position 0,0.
Instead you can use the toRect method from the PositionComponent class:
#override
void render(Canvas canvas) {
canvas.drawRect(toRect(), white);
super.render(canvas);
}
This method returns the rect of the component, based on the x, y position and the size.
Edit:
As mentioned by spydon, the super.render is called after the rendering to the canvas, it would be better to call the super method first as it will automatically handle things like translation, rotation, etc for you.
It prepares the canvas for your own rendering.

Flutter desktop custom game loop appears to drop frames

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.

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