Introduction to Flame with Flutter

    1. Introduction

    Flame is a Flutter-based 2D game engine. In this codelab, you will build a game inspired by one of the classics of '70s video games, Steve Wozniak'sBreakout. You will use Flame's Components, to draw the bat, ball, and bricks. You will utilize Flame's Effects to animate the bat's movement and see how to integrate Flame with Flutter's state management system.

    When complete, your game should look like this animated gif, albeit a tad slower.

    A screen recording of a game being played. The game has been sped up significantly.

    What you'll learn

    • How the basics of Flame work, starting withGameWidget.
    • How to use a game loop.
    • How Flame'sComponents work. They are akin to Flutter'sWidgets.
    • How to handle collisions.
    • How to useEffects to animateComponents.
    • How to overlay FlutterWidgets on top of a Flame game.
    • How to integrate Flame with Flutter's state management.

    What you'll build

    In this codelab, you're going to build a 2D game using Flutter and Flame. When complete, your game should meet the following requirements:

    • Function on all six platforms that Flutter supports: Android, iOS, Linux, macOS, Windows, and the web
    • Maintain at least 60 fps using Flame's game loop.
    • Use Flutter capabilities like thegoogle_fonts package andflutter_animate to recreate the feel of 80s arcade gaming.

    Note: If this is your first time working with Flutter, complete theYour first Flutter app codelab first. That codelab configures your Flutter development environment and gets you started with working with Flutter.

    2. Set up your Flutter environment

    Editor

    To simplify this codelab, it presumes thatVisual Studio Code (VS Code) is your development environment. VS Code is free and works on all major platforms. We use VS Code for this codelab because the instructions default to VS Code-specific shortcuts. The tasks become more straightforward: "click this button" or "press this key to do X" rather than "do the appropriateaction in your editor to do X".

    You can use any editor you like: Android Studio, other IntelliJ IDEs, Emacs, Vim, or Notepad++. They all work with Flutter.

    A screenshot of VS Code with some Flutter code

    Choose a development target

    Flutter produces apps for multiple platforms. Your app can run on any of the following operating systems:

    • iOS
    • Android
    • Windows
    • macOS
    • Linux
    • web

    It's common practice to choose one operating system as your development target. This is the operating system that your app runs on during development.

    A drawing depicting a laptop and a phone attached to the laptop by a cable. The laptop is labelled as the

    For example: say you're using a Windows laptop to develop your Flutter app. You then choose Android as your developmenttarget. To preview your app, you attach an Android device to your Windows laptop with a USB cable and your app-in-development runs on that attached Android device, or in an Android emulator. You could have chosen Windows as the development target, which runs your app-in-development as a Windows app alongside your editor.

    Tip: Choose your development device's Operating System as your development target. For example, if your computer runs Windows, choose Windows as the development target.

    A drawing of a laptop, with the label

    You might be tempted to choose the web as your development target. This has a downside during development: you lose Flutter'sStateful Hot Reload capability. Flutter can't currently hot-reload web applications.

    Make your choice before continuing. You can always run your app on other operating systems later. Choosing a development target makes the next step smoother.

    Install Flutter

    The most up-to-date instructions on installing the Flutter SDK can be found ondocs.flutter.dev.

    The instructions on the Flutter website cover the installation of the SDK and the development target-related tools and the editor plugins. For this codelab, install the following software:

    1. Flutter SDK
    2. Visual Studio Code with the Flutter plugin
    3. Compiler software for your chosen development target. (You needVisual Studio to target Windows orXcode to target macOS or iOS)

    In the next section, you'll create your first Flutter project.

    If you need to troubleshoot any problems, you might find some of these questions and answers (from StackOverflow) helpful for troubleshooting.

    Frequently Asked Questions

    3. Create a project

    Create your first Flutter project

    This involves opening VS Code and creating the Flutter app template in a directory you choose.

    1. Launch Visual Studio Code.
    2. Open the command palette (F1 orCtrl+Shift+P orShift+Cmd+P) then type "flutter new". When it appears, select theFlutter: New Project command.

    A screenshot of VS Code with

    1. SelectEmpty Application. Choose a directory in which to create your project. This should be any directory that doesn't require elevated privileges or have a space in its path. Examples include your home directory orC:\src\.

    A screenshot of VS Code with Empty Application shown as selected as part of the new application flow

    1. Name your projectbrick_breaker. The remainder of this codelab presumes you named your appbrick_breaker.

    A screen shot of VS Code with

    Flutter now creates your project folder and VS Code opens it. You'll now overwrite the contents of two files with a basic scaffold of the app.

    Copy & Paste the initial app

    This adds the example code provided in this codelab to your app.

    1. In the left pane of VS Code, clickExplorer and open thepubspec.yaml file.

    A partial screen shot of VS Code with arrows highlighting the location of the pubspec.yaml file

    1. Replace the contents of this file with the following:

    pubspec.yaml

    name:brick_breakerdescription:"Re-implementing Woz's Breakout"publish_to:'none'version:0.1.0environment:sdk:^3.8.0dependencies:flutter:sdk:flutterflame:^1.28.1flutter_animate:^4.5.2google_fonts:^6.2.1dev_dependencies:flutter_test:sdk:flutterflutter_lints:^5.0.0flutter:uses-material-design:true

    Thepubspec.yaml file specifies basic information about your app, such as its current version, its dependencies, and the assets with which it will ship.

    Note: If you gave your app a name other thanbrick_breaker, you need to change the first line correspondingly.

    1. Openmain.dart file inlib/ directory.

    A partial screen shot of VS Code with an arrow showing the location of the main.dart file

    1. Replace the contents of this file with the following:

    lib/main.dart

    import'package:flame/game.dart';import'package:flutter/material.dart';voidmain(){finalgame=FlameGame();runApp(GameWidget(game:game));}
    1. Run this code to verify everything is working. It should display a new window with only a blank black background. The world's worst video game is now rendering at 60fps!

    A screen shot showing a brick_breaker application window that is completely black.

    4. Create the game

    Size up the game

    A game played in two dimensions (2D) needs a play area. You will construct an area of specific dimensions, and then use these dimensions to size other aspects of the game.

    There are various ways to lay out coordinates in the playing area. By one convention you can measure direction from the center of the screen with the origin(0,0)at the center of the screen, the positive values move items to the right along the x axis and up along the y axis. This standard applies to most current games these days, especially when games that involve three dimensions.

    The convention when the original Breakout game was created was to set the origin in the top left corner. The positive x direction remained the same, however y was flipped. The x positive x direction was right and y was down. To stay true to the era, this game sets the origin to the top left corner.

    Aside: It may seem confusing that the way dimensions work in these different eras are different. There are two things to notice here.

    First, the defaults for people working in games depended on their default context. For programmers in the 70s and 80s, the default visual environment was a text buffer with(0,0) at the top left. This convention carried over to their graphical dimension defaults. In the 90s when 3D games became the default, the programmers working on these games were more focused on the mathematics of three dimensions and thus the putting(0,0) in the center of the screen made more sense.

    Second, you can express these games using any of these conventions, and it will work, as long as everything is consistent. It is possible to express Breakout with(0,0) placed anywhere on the screen and the player of the game won't notice any change. Model the world in a way that makes expressing your problem easier for you.

    Create a file calledconfig.dart in a new directory calledlib/src. This file will gain more constants in the following steps.

    lib/src/config.dart

    constgameWidth=820.0;constgameHeight=1600.0;

    This game will be 820 pixels wide and 1600 pixels high. The game area scales to fit the window in which it is displayed, but all the components added to the screen conform to this height and width.

    Create a PlayArea

    In the game of Breakout, the ball bounces off the walls of the play area. To accommodate collisions, you need aPlayArea component first.

    1. Create a file calledplay_area.dart in a new directory calledlib/src/components.
    2. Add the following to this file.

    lib/src/components/play_area.dart

    import'dart:async';import'package:flame/components.dart';import'package:flutter/material.dart';import'../brick_breaker.dart';classPlayAreaextendsRectangleComponentwithHasGameReference<BrickBreaker>{PlayArea():super(paint:Paint()..color=constColor(0xfff2e8cf));@overrideFutureOr<void>onLoad()async{super.onLoad();size=Vector2(game.width,game.height);}}

    Note: On entering the previous code, the IDE displays some angry looking red squiggly lines. You will fix this later when you create theBrickBreaker class.

    Where Flutter hasWidgets, Flame hasComponents. Where Flutter apps consist of creating trees of widgets, Flame games consist of maintaining trees of components.

    Therein lies an interesting difference between Flutter and Flame. Flutter's widget tree is an ephemeral description that is built to be used to update the persistent and mutableRenderObject layer. Flame's components are persistent and mutable, with an expectation that the developer will use these components as part of a simulation system.

    Flame's components are optimized for expressing game mechanics. This codelab will start with the game loop, featured in the next step.

    1. To control clutter, add a file containing all the components in this project. Create acomponents.dart file inlib/src/components and add the following content.

    lib/src/components/components.dart

    export'play_area.dart';

    Theexport directive plays the inverse role ofimport. It declares what functionality this file exposes when imported into another file. This file will grow more entries as you add new components in the following steps.

    Create a Flame game

    To extinguish the red squiggles from the previous step, derive a new subclass for Flame'sFlameGame.

    1. Create a file namedbrick_breaker.dart inlib/src and add the following code.

    lib/src/brick_breaker.dart

    import'dart:async';import'package:flame/components.dart';import'package:flame/game.dart';import'components/components.dart';import'config.dart';classBrickBreakerextendsFlameGame{BrickBreaker():super(camera:CameraComponent.withFixedResolution(width:gameWidth,height:gameHeight,),);doublegetwidth=>size.x;doublegetheight=>size.y;@overrideFutureOr<void>onLoad()async{super.onLoad();camera.viewfinder.anchor=Anchor.topLeft;world.add(PlayArea());}}

    This file coordinates the game's actions. During construction of the game instance, this code configures the game to use fixed resolution rendering. The game resizes to fill the screen that contains it and addsletterboxing as required.

    You expose the width and height of the game so that the children components, likePlayArea, can set themselves to the appropriate size.

    In theonLoad overridden method, your code performs two actions.

    1. Configures the top left as the anchor for the viewfinder. By default, theviewfinder uses the middle of the area as the anchor for(0,0).
    2. Adds thePlayArea to theworld. The world represents the game world. It projects all of its children through theCameraComponents view transformation.

    Warning: You can addComponents as children of theFlameGame directly. If you do, instead of adding them as children of theworld component, thenCameraComponent won't transform those Components. This may confuse you and your app's players when resizing the window doesn't behave how you expect.

    Get the game on screen

    To see all the changes you have made in this step, update yourlib/main.dart file with the following changes.

    lib/main.dart

    import'package:flame/game.dart';import'package:flutter/material.dart';import'src/brick_breaker.dart';//Addthisimportvoidmain(){finalgame=BrickBreaker();//ModifythislinerunApp(GameWidget(game:game));}

    After you make these changes, restart the game. The game should resemble the following figure.

    Heads up: Flame doesn't currently support Flutter'sHot Reload capability. You need to make full restarts to see the changes you have made.

    A screen shot showing a brick_breaker application window  with a sand colored rectangle in the middle of the app window

    In the next step, you will add a ball to the world, and get it moving!

    5. Display the ball

    Create the ball component

    Putting a moving ball on the screen involves creating another component and adding it to the game world.

    1. Edit the contents oflib/src/config.dart file as follows.

    lib/src/config.dart

    constgameWidth=820.0;constgameHeight=1600.0;constballRadius=gameWidth*0.02;//Addthisconstant

    The design pattern of defining named constants as derived values will return many times in this codelab. This lets you modify the top levelgameWidth andgameHeight to explore how the game look and feel changes as result.

    1. Create theBall component in a file calledball.dart inlib/src/components.

    lib/src/components/ball.dart

    import'package:flame/components.dart';import'package:flutter/material.dart';classBallextendsCircleComponent{Ball({requiredthis.velocity,requiredsuper.position,requireddoubleradius,}):super(radius:radius,anchor:Anchor.center,paint:Paint()..color=constColor(0xff1e6091)..style=PaintingStyle.fill,);finalVector2velocity;@overridevoidupdate(doubledt){super.update(dt);position+=velocity*dt;}}

    Earlier, you defined thePlayArea using theRectangleComponent, so it stands to reason that more shapes exist.CircleComponent, likeRectangleComponent, derives fromPositionedComponent, so you can position the ball on the screen. More importantly, its position can be updated.

    This component introduces the concept ofvelocity, or change in position over time. Velocity is aVector2 object asvelocity is both speed and direction. To update position, override theupdate method, which the game engine calls for every frame. Thedt is the duration between the previous frame and this frame. This lets you adapt to factors like different frame rates (60hz or 120hz) or long frames due to excessive computation.

    Aside: In this game, you are usingVectors as a way of modeling attributes like location and velocity that have components. As this game simulates a 2 dimensional world, location and velocity have two components,x andy.

    Wrapping these components up as a single concept enables simpler handling of concepts like scaling a vector, for example speeding up a ball by multiplying its velocity by a number greater than one, or updating a position with an appropriate portion of its current velocity.

    See theIntro to Vectors lesson on Khan Academy for more in depth coverage of using vectors for crafting simulations.

    Pay close attention to theposition += velocity * dt update. This is how you implement updating a discrete simulation of motion over time.

    1. To include theBall component in the list of components, edit thelib/src/components/components.dart file as follows.

    lib/src/components/components.dart

    export'ball.dart';//Addthisexportexport'play_area.dart';

    Adding the ball to the world

    You have a ball. Place it in the world and set it up to move around the play area.

    Edit thelib/src/brick_breaker.dart file as follows.

    lib/src/brick_breaker.dart

    import'dart:async';import'dart:math'asmath;//Addthisimportimport'package:flame/components.dart';import'package:flame/game.dart';import'components/components.dart';import'config.dart';classBrickBreakerextendsFlameGame{BrickBreaker():super(camera:CameraComponent.withFixedResolution(width:gameWidth,height:gameHeight,),);finalrand=math.Random();//Addthisvariabledoublegetwidth=>size.x;doublegetheight=>size.y;@overrideFutureOr<void>onLoad()async{super.onLoad();camera.viewfinder.anchor=Anchor.topLeft;world.add(PlayArea());world.add(Ball(//Addfromhere...radius:ballRadius,position:size/2,velocity:Vector2((rand.nextDouble()-0.5)*width,height*0.2,).normalized()..scale(height/4),),);debugMode=true;//Tohere.}}

    This change adds theBall component to theworld. To set the ball'sposition to the center of the display area, the code first halves the size of the game, asVector2 has operator overloads (* and/) to scale aVector2 by a scalar value.

    To set the ball'svelocity involves more complexity. The intent is to move the ball down the screen in a random direction at a reasonable speed. The call to thenormalized method creates aVector2 object set to the same direction as the originalVector2, but scaled down to a distance of 1. This keeps the speed of the ball consistent no matter which direction the ball goes. The ball's velocity is then scaled up to be a 1/4 of the height of the game.

    Getting these various values right involves some iteration, also known as playtesting in the industry.

    The last line turns on the debugging display, which adds additional information to the display to help with debugging.

    Aside: Vectors usually represent both direction and size. The size might be position, or in the case of velocity, speed. The direction is a more general concept, and there is frequently a need to extract the direction of a vector for the comparison of two vectors.

    Thenormal is the direction of a vector, created by converting a position or velocity vector to a new vector with the same direction, but with a size of1. In the above code, normalization is used to take a vector or random direction and give it a consistent speed.

    When you now run the game, it should resemble the following display.

    A screen shot showing a brick_breaker application window with a blue circle on top of the sand colored rectangle. The blue circle is annotated with numbers indicated it's size and location on screen

    Both thePlayArea component and theBall component both have debugging information, but the background mattes crop thePlayArea's numbers. The reason everything has debugging information displayed is because you turned ondebugMode for the entire component tree. You could also turn on debugging for only selected components, if that is more useful.

    If you restart your game a few times, you might notice that the ball doesn't interact with the walls quite as expected. To accomplish that effect, you need to add collision detection, which you will do in the next step.

    6. Bounce around

    Add collision detection

    Collision detection adds a behavior where your game recognizes when two objects came into contact with each other.

    To add collision detection to the game, add theHasCollisionDetection mixin to theBrickBreaker game as shown in the following code.

    lib/src/brick_breaker.dart

    import'dart:async';import'dart:math'asmath;import'package:flame/components.dart';import'package:flame/game.dart';import'components/components.dart';import'config.dart';classBrickBreakerextendsFlameGamewithHasCollisionDetection{//ModifythislineBrickBreaker():super(camera:CameraComponent.withFixedResolution(width:gameWidth,height:gameHeight,),);finalrand=math.Random();doublegetwidth=>size.x;doublegetheight=>size.y;@overrideFutureOr<void>onLoad()async{super.onLoad();camera.viewfinder.anchor=Anchor.topLeft;world.add(PlayArea());world.add(Ball(radius:ballRadius,position:size/2,velocity:Vector2((rand.nextDouble()-0.5)*width,height*0.2,).normalized()..scale(height/4),),);debugMode=true;}}

    This tracks the hitboxes of components and triggers collision callbacks on every game tick.

    Aside: Ahitbox is the area that the collision detection system will use to calculate if a component has collided with another component. The reason for the name is that the area used for collision detection is frequently simplified from the visual representation the player sees on screen, frequently to a rectangle or box. The size of the hitbox may differ from the on screen avatar to make the player's game play easier.

    In this game, the size of the hitbox for the ball and the bricks is the same as the component they are attached to. The bat, however, has a rectangular hitbox without the rounded ends that the bat has on screen. This effectively makes the bat slightly larger than it appears.

    To start populating the game's hitboxes, modify thePlayArea component as shown below.

    lib/src/components/play_area.dart

    import'dart:async';import'package:flame/collisions.dart';//Addthisimportimport'package:flame/components.dart';import'package:flutter/material.dart';import'../brick_breaker.dart';classPlayAreaextendsRectangleComponentwithHasGameReference<BrickBreaker>{PlayArea():super(paint:Paint()..color=constColor(0xfff2e8cf),children:[RectangleHitbox()],//Addthisparameter);@overrideFutureOr<void>onLoad()async{super.onLoad();size=Vector2(game.width,game.height);}}

    Adding aRectangleHitbox component as a child of theRectangleComponent will construct a hit box for collision detection that matches the size of the parent component. There is a factory constructor forRectangleHitbox calledrelative for times when you want a hitbox that is smaller, or larger, than the parent component.

    Bounce the ball

    So far, adding collision detection has made no difference to the gameplay. It does change once you modify theBall component. It's the ball's behavior that has to change when it collides with thePlayArea.

    Modify theBall component as follows.

    lib/src/components/ball.dart

    import'package:flame/collisions.dart';//Addthisimportimport'package:flame/components.dart';import'package:flutter/material.dart';import'../brick_breaker.dart';//Andthisimportimport'play_area.dart';//AndthisonetooclassBallextendsCircleComponentwithCollisionCallbacks,HasGameReference<BrickBreaker>{//AddthesemixinsBall({requiredthis.velocity,requiredsuper.position,requireddoubleradius,}):super(radius:radius,anchor:Anchor.center,paint:Paint()..color=constColor(0xff1e6091)..style=PaintingStyle.fill,children:[CircleHitbox()],//Addthisparameter);finalVector2velocity;@overridevoidupdate(doubledt){super.update(dt);position+=velocity*dt;}@override//Addfromhere...voidonCollisionStart(Set<Vector2>intersectionPoints,PositionComponentother,){super.onCollisionStart(intersectionPoints,other);if(otherisPlayArea){if(intersectionPoints.first.y <=0){velocity.y=-velocity.y;}elseif(intersectionPoints.first.x <=0){velocity.x=-velocity.x;}elseif(intersectionPoints.first.x >=game.width){velocity.x=-velocity.x;}elseif(intersectionPoints.first.y >=game.height){removeFromParent();}}else{debugPrint('collision with $other');}}//Tohere.}

    This example makes a major change with the addition of theonCollisionStart callback. The collision detection system added toBrickBreaker in the prior example calls this callback.

    First, the code tests if theBall collided withPlayArea. This seems redundant for now, as there are no other components in the game world. That will change in the next step, when you add a bat to the world. Then, it also adds anelse condition to handle when the ball collides with things that aren't the bat. A gentle reminder to implement remaining logic, if you will.

    When the ball collides with the bottom wall, it just disappears from the playing surface while still very much in view. You handle this artifact in a future step, using the power of Flame's Effects.

    Note: Flame's collision callbacks have a lifecycle. The callbacks areonCollisionStart,onCollision, andonCollisionEnd. The initial version of this game was written in terms ofonCollision and required additional code to handle follow-on collision callbacks on later game ticks. Using the right callback for the right purposesignificantly simplifies your code!

    Now that you have the ball colliding with the walls of the game, it sure would be useful to give the player a bat to hit the ball with...

    7. Get bat on ball

    Create the bat

    To add a bat to keep the ball in play within the game,

    1. Insert some constants inlib/src/config.dart file as follows.

    lib/src/config.dart

    constgameWidth=820.0;constgameHeight=1600.0;constballRadius=gameWidth*0.02;constbatWidth=gameWidth*0.2;//Addfromhere...constbatHeight=ballRadius*2;constbatStep=gameWidth*0.05;//Tohere.

    ThebatHeight andbatWidth constants are self explanatory. ThebatStep constant, on the other hand, needs a touch of explanation. To interact with the ball in this game, the player can drag the bat with the mouse or finger, depending on the platform, or use the keyboard. ThebatStep constant configures how far the bat steps for each left or right arrow key press.

    1. Define theBat component class as follows.

    lib/src/components/bat.dart

    import'package:flame/collisions.dart';import'package:flame/components.dart';import'package:flame/effects.dart';import'package:flame/events.dart';import'package:flutter/material.dart';import'../brick_breaker.dart';classBatextendsPositionComponentwithDragCallbacks,HasGameReference<BrickBreaker>{Bat({requiredthis.cornerRadius,requiredsuper.position,requiredsuper.size,}):super(anchor:Anchor.center,children:[RectangleHitbox()]);finalRadiuscornerRadius;final_paint=Paint()..color=constColor(0xff1e6091)..style=PaintingStyle.fill;@overridevoidrender(Canvascanvas){super.render(canvas);canvas.drawRRect(RRect.fromRectAndRadius(Offset.zero &size.toSize(),cornerRadius),_paint,);}@overridevoidonDragUpdate(DragUpdateEventevent){super.onDragUpdate(event);position.x=(position.x+event.localDelta.x).clamp(0,game.width);}voidmoveBy(doubledx){add(MoveToEffect(Vector2((position.x+dx).clamp(0,game.width),position.y),EffectController(duration:0.1),),);}}

    This component introduces a few new capabilities.

    First, the Bat component is aPositionComponent, not aRectangleComponent nor aCircleComponent. This means this code needs to render theBat on screen. To accomplish this, it overrides therender callback.

    Looking closely at thecanvas.drawRRect (draw rounded rectangle) call, and you might ask yourself, "where is the rectangle?" TheOffset.zero & size.toSize() leverages anoperator & overload on thedart:uiOffset class that createsRects. This shorthand might confuse you at first, but you will see it frequently in lower level Flutter and Flame code.

    Second, thisBat component is draggable using either finger or mouse depending on platform. To implement this functionality, you add theDragCallbacks mixin and override theonDragUpdate event.

    Last, theBat component needs to respond to keyboard control. ThemoveBy function allows other code to tell this bat to move left or right by a certain number of virtual pixels. This function introduces a new capability of the Flame game engine:Effects. By adding theMoveToEffect object as a child of this component, the player sees the bat animated to a new position. There are a collection ofEffects available in Flame to perform a variety of effects.

    The Effect's constructor arguments include a reference to thegame getter. This is why you include theHasGameReference mixin on this class. This mixin adds a type-safegame accessor to this component to access theBrickBreaker instance at the top of the component tree.

    You may be wondering why theclamp on theBat'sx position allows for the bat to go halfway off screen. In a previous version of this codelab there was code here to prevent the bat from going off screen, but this reduced the ability of the player to use the bat's effectively curved front edge to its fullest. This code has been updated to allow for the bat to be slid halfway off screen, to allow for more interesting and fun game play.

    1. To make theBat available toBrickBreaker, update thelib/src/components/components.dart file as follows.

    lib/src/components/components.dart

    export'ball.dart';export'bat.dart';//Addthisexportexport'play_area.dart';

    Add the bat to the world

    To add theBat component to the game world, updateBrickBreaker as follows.

    lib/src/brick_breaker.dart

    import'dart:async';import'dart:math'asmath;import'package:flame/components.dart';import'package:flame/events.dart';//Addthisimportimport'package:flame/game.dart';import'package:flutter/material.dart';//Andthisimportimport'package:flutter/services.dart';//Andthisimport'components/components.dart';import'config.dart';classBrickBreakerextendsFlameGamewithHasCollisionDetection,KeyboardEvents{//ModifythislineBrickBreaker():super(camera:CameraComponent.withFixedResolution(width:gameWidth,height:gameHeight,),);finalrand=math.Random();doublegetwidth=>size.x;doublegetheight=>size.y;@overrideFutureOr<void>onLoad()async{super.onLoad();camera.viewfinder.anchor=Anchor.topLeft;world.add(PlayArea());world.add(Ball(radius:ballRadius,position:size/2,velocity:Vector2((rand.nextDouble()-0.5)*width,height*0.2,).normalized()..scale(height/4),),);world.add(//Addfromhere...Bat(size:Vector2(batWidth,batHeight),cornerRadius:constRadius.circular(ballRadius/2),position:Vector2(width/2,height*0.95),),);//Tohere.debugMode=true;}@override//Addfromhere...KeyEventResultonKeyEvent(KeyEventevent,Set<LogicalKeyboardKey>keysPressed,){super.onKeyEvent(event,keysPressed);switch(event.logicalKey){caseLogicalKeyboardKey.arrowLeft:world.children.query<Bat>().first.moveBy(-batStep);caseLogicalKeyboardKey.arrowRight:world.children.query<Bat>().first.moveBy(batStep);}returnKeyEventResult.handled;}//Tohere.}

    The addition of theKeyboardEvents mixin and the overriddenonKeyEvent method handle the keyboard input. Recall the code you added earlier to move the bat by the appropriate step amount.

    The remaining chunk of added code adds the bat to the game world in the appropriate position and with the right proportions. Having all these settings exposed in this file simplifies your ability to tweak the relative size of the bat and the ball to get the right feel for the game.

    If you play the game at this point, you see that you can move the bat to intercept the ball, but get no visible response, apart from the debug logging that you left inBall's collision detection code.

    Time to fix that now. Edit theBall component as follows.

    lib/src/components/ball.dart

    import'package:flame/collisions.dart';import'package:flame/components.dart';import'package:flame/effects.dart';//Addthisimportimport'package:flutter/material.dart';import'../brick_breaker.dart';import'bat.dart';//Andthisimportimport'play_area.dart';classBallextendsCircleComponentwithCollisionCallbacks,HasGameReference<BrickBreaker>{Ball({requiredthis.velocity,requiredsuper.position,requireddoubleradius,}):super(radius:radius,anchor:Anchor.center,paint:Paint()..color=constColor(0xff1e6091)..style=PaintingStyle.fill,children:[CircleHitbox()],);finalVector2velocity;@overridevoidupdate(doubledt){super.update(dt);position+=velocity*dt;}@overridevoidonCollisionStart(Set<Vector2>intersectionPoints,PositionComponentother,){super.onCollisionStart(intersectionPoints,other);if(otherisPlayArea){if(intersectionPoints.first.y <=0){velocity.y=-velocity.y;}elseif(intersectionPoints.first.x <=0){velocity.x=-velocity.x;}elseif(intersectionPoints.first.x >=game.width){velocity.x=-velocity.x;}elseif(intersectionPoints.first.y >=game.height){add(RemoveEffect(delay:0.35));//Modifyfromhere...}}elseif(otherisBat){velocity.y=-velocity.y;velocity.x=velocity.x+(position.x-other.position.x)/other.size.x*game.width*0.3;}else{//Tohere.debugPrint('collision with $other');}}}

    These code changes fix two separate issues.

    First, it fixes the ball popping out of existence the moment it touches the bottom of the screen. To fix this issue, you replace theremoveFromParent call withRemoveEffect. TheRemoveEffect removes the ball from the game world after letting the ball exit the viewable play area.

    Second, these changes fix the handling of collision between bat and ball. This handling code works very much in the player's favor. As long as the player touches the ball with the bat, the ball returns to the top of the screen. If this feels too forgiving and you want something more realistic, then change this handling to better fit how you want your game to feel.

    It's worth pointing out the complexity of thevelocity update. It doesn't just reversethey component of the velocity, as was done for the wall collisions. It also updates thex component in a way that depends on the relative position of the bat and ballat the time of contact. This gives the player more control over what the ball does, but exactly how is not communicated to the player in any way except through play.

    Now that you have a bat with which to hit the ball, it'd be neat to have some bricks to break with the ball!

    8. Break down the wall

    Creating the bricks

    To add bricks to the game,

    1. Insert some constants inlib/src/config.dart file as follows.

    lib/src/config.dart

    import'package:flutter/material.dart';//AddthisimportconstbrickColors=[//AddthisconstColor(0xfff94144),Color(0xfff3722c),Color(0xfff8961e),Color(0xfff9844a),Color(0xfff9c74f),Color(0xff90be6d),Color(0xff43aa8b),Color(0xff4d908e),Color(0xff277da1),Color(0xff577590),];constgameWidth=820.0;constgameHeight=1600.0;constballRadius=gameWidth*0.02;constbatWidth=gameWidth*0.2;constbatHeight=ballRadius*2;constbatStep=gameWidth*0.05;constbrickGutter=gameWidth*0.015;//Addfromhere...finalbrickWidth=(gameWidth-(brickGutter*(brickColors.length+1)))/brickColors.length;constbrickHeight=gameHeight*0.03;constdifficultyModifier=1.03;//Tohere.
    1. Insert theBrick component as follows.

    lib/src/components/brick.dart

    import'package:flame/collisions.dart';import'package:flame/components.dart';import'package:flutter/material.dart';import'../brick_breaker.dart';import'../config.dart';import'ball.dart';import'bat.dart';classBrickextendsRectangleComponentwithCollisionCallbacks,HasGameReference<BrickBreaker>{Brick({requiredsuper.position,requiredColorcolor}):super(size:Vector2(brickWidth,brickHeight),anchor:Anchor.center,paint:Paint()..color=color..style=PaintingStyle.fill,children:[RectangleHitbox()],);@overridevoidonCollisionStart(Set<Vector2>intersectionPoints,PositionComponentother,){super.onCollisionStart(intersectionPoints,other);removeFromParent();if(game.world.children.query<Brick>().length==1){game.world.removeAll(game.world.children.query<Ball>());game.world.removeAll(game.world.children.query<Bat>());}}}

    By now, most of this code should be familiar. This code uses aRectangleComponent, with both collision detection and a type-safe reference to theBrickBreaker game at the top of the component tree.

    The most important new concept this code introduces is how the player achieves the win condition. The win condition check queries the world for bricks, and confirms that only one remains. This might be a bit confusing, because the preceding line removes this brick from its parent.

    The key point to understand is that component removal is a queued command. It removes the brick after this code runs, but before the next tick of the game world.

    To make theBrick component accessible toBrickBreaker, editlib/src/components/components.dart as follows.

    lib/src/components/components.dart

    export'ball.dart';export'bat.dart';export'brick.dart';//Addthisexportexport'play_area.dart';

    Add bricks to the world

    Update theBall component as follows.

    lib/src/components/ball.dart

    import'package:flame/collisions.dart';import'package:flame/components.dart';import'package:flame/effects.dart';import'package:flutter/material.dart';import'../brick_breaker.dart';import'bat.dart';import'brick.dart';//Addthisimportimport'play_area.dart';classBallextendsCircleComponentwithCollisionCallbacks,HasGameReference<BrickBreaker>{Ball({requiredthis.velocity,requiredsuper.position,requireddoubleradius,requiredthis.difficultyModifier,//Addthisparameter}):super(radius:radius,anchor:Anchor.center,paint:Paint()..color=constColor(0xff1e6091)..style=PaintingStyle.fill,children:[CircleHitbox()],);finalVector2velocity;finaldoubledifficultyModifier;//Addthismember@overridevoidupdate(doubledt){super.update(dt);position+=velocity*dt;}@overridevoidonCollisionStart(Set<Vector2>intersectionPoints,PositionComponentother,){super.onCollisionStart(intersectionPoints,other);if(otherisPlayArea){if(intersectionPoints.first.y <=0){velocity.y=-velocity.y;}elseif(intersectionPoints.first.x <=0){velocity.x=-velocity.x;}elseif(intersectionPoints.first.x >=game.width){velocity.x=-velocity.x;}elseif(intersectionPoints.first.y >=game.height){add(RemoveEffect(delay:0.35));}}elseif(otherisBat){velocity.y=-velocity.y;velocity.x=velocity.x+(position.x-other.position.x)/other.size.x*game.width*0.3;}elseif(otherisBrick){//Modifyfromhere...if(position.y <other.position.y-other.size.y/2){velocity.y=-velocity.y;}elseif(position.y >other.position.y+other.size.y/2){velocity.y=-velocity.y;}elseif(position.x <other.position.x){velocity.x=-velocity.x;}elseif(position.x >other.position.x){velocity.x=-velocity.x;}velocity.setFrom(velocity*difficultyModifier);//Tohere.}}}

    This introduces the only new aspect, a difficulty modifier that increases the ball velocity after each brick collision. This tunable parameter needs to be playtested to find the appropriate difficulty curve that is appropriate for your game.

    Edit theBrickBreaker game as follows.

    lib/src/brick_breaker.dart

    import'dart:async';import'dart:math'asmath;import'package:flame/components.dart';import'package:flame/events.dart';import'package:flame/game.dart';import'package:flutter/material.dart';import'package:flutter/services.dart';import'components/components.dart';import'config.dart';classBrickBreakerextendsFlameGamewithHasCollisionDetection,KeyboardEvents{BrickBreaker():super(camera:CameraComponent.withFixedResolution(width:gameWidth,height:gameHeight,),);finalrand=math.Random();doublegetwidth=>size.x;doublegetheight=>size.y;@overrideFutureOr<void>onLoad()async{super.onLoad();camera.viewfinder.anchor=Anchor.topLeft;world.add(PlayArea());world.add(Ball(difficultyModifier:difficultyModifier,//Addthisargumentradius:ballRadius,position:size/2,velocity:Vector2((rand.nextDouble()-0.5)*width,height*0.2,).normalized()..scale(height/4),),);world.add(Bat(size:Vector2(batWidth,batHeight),cornerRadius:constRadius.circular(ballRadius/2),position:Vector2(width/2,height*0.95),),);awaitworld.addAll([//Addfromhere...for(vari=0;i <brickColors.length;i++)for(varj=1;j <=5;j++)Brick(position:Vector2((i+0.5)*brickWidth+(i+1)*brickGutter,(j+2.0)*brickHeight+j*brickGutter,),color:brickColors[i],),]);//Tohere.debugMode=true;}@overrideKeyEventResultonKeyEvent(KeyEventevent,Set<LogicalKeyboardKey>keysPressed,){super.onKeyEvent(event,keysPressed);switch(event.logicalKey){caseLogicalKeyboardKey.arrowLeft:world.children.query<Bat>().first.moveBy(-batStep);caseLogicalKeyboardKey.arrowRight:world.children.query<Bat>().first.moveBy(batStep);}returnKeyEventResult.handled;}}

    If you run the game as it currently stands, it displays all the key game mechanics. You could turn off debugging and call it done, but something feels missing.

    A screen shot showing brick_breaker with ball, bat, and most of the bricks on the playing area. Each of the components has debugging labels

    How about a welcome screen, a game over screen, and maybe a score? Flutter can add these features to the game, and that is where you will turn your attention next.

    9. Win the game

    Add play states

    In this step, you embed the Flame game inside of a Flutter wrapper, and then add Flutter overlays for the welcome, game over, and won screens.

    First, you modify the game and component files to implement a play state that reflects whether to show an overlay, and if so, which one.

    1. Modify theBrickBreaker game as follows.

    lib/src/brick_breaker.dart

    import'dart:async';import'dart:math'asmath;import'package:flame/components.dart';import'package:flame/events.dart';import'package:flame/game.dart';import'package:flutter/material.dart';import'package:flutter/services.dart';import'components/components.dart';import'config.dart';enumPlayState{welcome,playing,gameOver,won}//AddthisenumerationclassBrickBreakerextendsFlameGamewithHasCollisionDetection,KeyboardEvents,TapDetector{//ModifythislineBrickBreaker():super(camera:CameraComponent.withFixedResolution(width:gameWidth,height:gameHeight,),);finalrand=math.Random();doublegetwidth=>size.x;doublegetheight=>size.y;latePlayState_playState;//Addfromhere...PlayStategetplayState=>_playState;setplayState(PlayStateplayState){_playState=playState;switch(playState){casePlayState.welcome:casePlayState.gameOver:casePlayState.won:overlays.add(playState.name);casePlayState.playing:overlays.remove(PlayState.welcome.name);overlays.remove(PlayState.gameOver.name);overlays.remove(PlayState.won.name);}}//Tohere.@overrideFutureOr<void>onLoad()async{super.onLoad();camera.viewfinder.anchor=Anchor.topLeft;world.add(PlayArea());playState=PlayState.welcome;//Addfromhere...}voidstartGame(){if(playState==PlayState.playing)return;world.removeAll(world.children.query<Ball>());world.removeAll(world.children.query<Bat>());world.removeAll(world.children.query<Brick>());playState=PlayState.playing;//Tohere.world.add(Ball(difficultyModifier:difficultyModifier,radius:ballRadius,position:size/2,velocity:Vector2((rand.nextDouble()-0.5)*width,height*0.2,).normalized()..scale(height/4),),);world.add(Bat(size:Vector2(batWidth,batHeight),cornerRadius:constRadius.circular(ballRadius/2),position:Vector2(width/2,height*0.95),),);world.addAll([//Droptheawaitfor(vari=0;i <brickColors.length;i++)for(varj=1;j <=5;j++)Brick(position:Vector2((i+0.5)*brickWidth+(i+1)*brickGutter,(j+2.0)*brickHeight+j*brickGutter,),color:brickColors[i],),]);}//DropthedebugMode@override//Addfromhere...voidonTap(){super.onTap();startGame();}//Tohere.@overrideKeyEventResultonKeyEvent(KeyEventevent,Set<LogicalKeyboardKey>keysPressed,){super.onKeyEvent(event,keysPressed);switch(event.logicalKey){caseLogicalKeyboardKey.arrowLeft:world.children.query<Bat>().first.moveBy(-batStep);caseLogicalKeyboardKey.arrowRight:world.children.query<Bat>().first.moveBy(batStep);caseLogicalKeyboardKey.space://Addfromhere...caseLogicalKeyboardKey.enter:startGame();//Tohere.}returnKeyEventResult.handled;}@overrideColorbackgroundColor()=>constColor(0xfff2e8cf);//Addthisoverride}

    This code changes a good deal of theBrickBreaker game. Adding theplayState enumeration takes a lot of work. This captures where the player is in entering, playing, and either losing or winning the game. At the top of the file, you define the enumeration, then instantiate it as a hidden state with matching getters and setters. These getters and setters enable modifying overlays when the various parts of the game trigger play state transitions.

    Next, you split the code inonLoad into onLoad and a newstartGame method. Before this change, you could only start a new game through restarting the game. With these new additions, the player can now start a new game without such drastic measures.

    To permit the player to start a new game, you configured two new handlers for the game. You added a tap handler and extended the keyboard handler to enable the user to start a new game in multiple modalities. With play state modeled, it would make sense to update the components to trigger play state transitions when the player either wins, or loses.

    1. Modify theBall component as follows.

    lib/src/components/ball.dart

    import'package:flame/collisions.dart';import'package:flame/components.dart';import'package:flame/effects.dart';import'package:flutter/material.dart';import'../brick_breaker.dart';import'bat.dart';import'brick.dart';import'play_area.dart';classBallextendsCircleComponentwithCollisionCallbacks,HasGameReference<BrickBreaker>{Ball({requiredthis.velocity,requiredsuper.position,requireddoubleradius,requiredthis.difficultyModifier,}):super(radius:radius,anchor:Anchor.center,paint:Paint()..color=constColor(0xff1e6091)..style=PaintingStyle.fill,children:[CircleHitbox()],);finalVector2velocity;finaldoubledifficultyModifier;@overridevoidupdate(doubledt){super.update(dt);position+=velocity*dt;}@overridevoidonCollisionStart(Set<Vector2>intersectionPoints,PositionComponentother,){super.onCollisionStart(intersectionPoints,other);if(otherisPlayArea){if(intersectionPoints.first.y <=0){velocity.y=-velocity.y;}elseif(intersectionPoints.first.x <=0){velocity.x=-velocity.x;}elseif(intersectionPoints.first.x >=game.width){velocity.x=-velocity.x;}elseif(intersectionPoints.first.y >=game.height){add(RemoveEffect(delay:0.35,onComplete:(){//Modifyfromheregame.playState=PlayState.gameOver;},),);//Tohere.}}elseif(otherisBat){velocity.y=-velocity.y;velocity.x=velocity.x+(position.x-other.position.x)/other.size.x*game.width*0.3;}elseif(otherisBrick){if(position.y <other.position.y-other.size.y/2){velocity.y=-velocity.y;}elseif(position.y >other.position.y+other.size.y/2){velocity.y=-velocity.y;}elseif(position.x <other.position.x){velocity.x=-velocity.x;}elseif(position.x >other.position.x){velocity.x=-velocity.x;}velocity.setFrom(velocity*difficultyModifier);}}}

    This small change adds anonComplete callback to theRemoveEffect which triggers thegameOver play state. This should feel about right if the player allows the ball to escape off the bottom of the screen.

    1. Edit theBrick component as follows.

    lib/src/components/brick.dart

    impimport'package:flame/collisions.dart';import'package:flame/components.dart';import'package:flutter/material.dart';import'../brick_breaker.dart';import'../config.dart';import'ball.dart';import'bat.dart';classBrickextendsRectangleComponentwithCollisionCallbacks,HasGameReference<BrickBreaker>{Brick({requiredsuper.position,requiredColorcolor}):super(size:Vector2(brickWidth,brickHeight),anchor:Anchor.center,paint:Paint()..color=color..style=PaintingStyle.fill,children:[RectangleHitbox()],);@overridevoidonCollisionStart(Set<Vector2>intersectionPoints,PositionComponentother,){super.onCollisionStart(intersectionPoints,other);removeFromParent();if(game.world.children.query<Brick>().length==1){game.playState=PlayState.won;//Addthislinegame.world.removeAll(game.world.children.query<Ball>());game.world.removeAll(game.world.children.query<Bat>());}}}

    On the other hand, if the player can break all the bricks, they have earned a "game won" screen. Well done player, well done!

    Add the Flutter wrapper

    To provide somewhere to embed the game and add play state overlays, add the Flutter shell.

    1. Create awidgets directory underlib/src.
    2. Add agame_app.dart file and insert the following content to that file.

    lib/src/widgets/game_app.dart

    import'package:flame/game.dart';import'package:flutter/material.dart';import'package:google_fonts/google_fonts.dart';import'../brick_breaker.dart';import'../config.dart';classGameAppextendsStatelessWidget{constGameApp({super.key});@overrideWidgetbuild(BuildContextcontext){returnMaterialApp(debugShowCheckedModeBanner:false,theme:ThemeData(textTheme:GoogleFonts.pressStart2pTextTheme().apply(bodyColor:constColor(0xff184e77),displayColor:constColor(0xff184e77),),),home:Scaffold(body:Container(decoration:constBoxDecoration(gradient:LinearGradient(begin:Alignment.topCenter,end:Alignment.bottomCenter,colors:[Color(0xffa9d6e5),Color(0xfff2e8cf)],),),child:SafeArea(child:Padding(padding:constEdgeInsets.all(16),child:Center(child:FittedBox(child:SizedBox(width:gameWidth,height:gameHeight,child:GameWidget.controlled(gameFactory:BrickBreaker.new,overlayBuilderMap:{PlayState.welcome.name:(context,game)=>Center(child:Text('TAP TO PLAY',style:Theme.of(context).textTheme.headlineLarge,),),PlayState.gameOver.name:(context,game)=>Center(child:Text('G A M E   O V E R',style:Theme.of(context).textTheme.headlineLarge,),),PlayState.won.name:(context,game)=>Center(child:Text('Y O U   W O N ! ! !',style:Theme.of(context).textTheme.headlineLarge,),),},),),),),),),),),);}}

    Most content in this file follows a standard Flutter widget tree build. The parts specific to Flame include usingGameWidget.controlled to construct and manage theBrickBreaker game instance and the newoverlayBuilderMap argument to theGameWidget.

    ThisoverlayBuilderMap's keys must align with the overlays that theplayState setter inBrickBreaker added or removed. Attempting to set an overlay that is not in this map leads to unhappy faces all around.

    1. To get this new functionality on screen, replacelib/main.dart file with the following content.

    lib/main.dart

    import'package:flutter/material.dart';import'src/widgets/game_app.dart';voidmain(){runApp(constGameApp());}

    If you run this code on iOS, Linux, Windows or the web, the intended output displays in the game. If you target macOS or Android, you need one last tweak to enablegoogle_fonts to display.

    Enabling font access

    Add internet permission for Android

    For Android, you must add Internet permission. Edit yourAndroidManifest.xml as follows.

    android/app/src/main/AndroidManifest.xml

    <manifestxmlns:android="http://schemas.android.com/apk/res/android"><!--Addthefollowingline--><uses-permissionandroid:name="android.permission.INTERNET"/><applicationandroid:label="brick_breaker"android:name="${applicationName}"android:icon="@mipmap/ic_launcher"><activityandroid:name=".MainActivity"android:exported="true"android:launchMode="singleTop"android:taskAffinity=""android:theme="@style/LaunchTheme"android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"android:hardwareAccelerated="true"android:windowSoftInputMode="adjustResize"><!--SpecifiesanAndroidthemetoapplytothisActivityassoonastheAndroidprocesshasstarted.ThisthemeisvisibletotheuserwhiletheFlutterUIinitializes.Afterthat,thisthemecontinuestodeterminetheWindowbackgroundbehindtheFlutterUI.--><meta-dataandroid:name="io.flutter.embedding.android.NormalTheme"android:resource="@style/NormalTheme"/><intent-filter><actionandroid:name="android.intent.action.MAIN"/><categoryandroid:name="android.intent.category.LAUNCHER"/></intent-filter></activity><!--Don't delete the meta-data below.ThisisusedbytheFluttertooltogenerateGeneratedPluginRegistrant.java--><meta-dataandroid:name="flutterEmbedding"android:value="2"/></application><!--Requiredtoqueryactivitiesthatcanprocesstext,see:https://developer.android.com/training/package-visibilityandhttps://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.Inparticular,thisisusedbytheFlutterengineinio.flutter.plugin.text.ProcessTextPlugin.--><queries><intent><actionandroid:name="android.intent.action.PROCESS_TEXT"/><dataandroid:mimeType="text/plain"/></intent></queries></manifest>

    Edit entitlement files for macOS

    For macOS, you have two files to edit.

    1. Edit theDebugProfile.entitlements file to match the following code.

    macos/Runner/DebugProfile.entitlements

    <?xmlversion="1.0"encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plistversion="1.0"><dict><key>com.apple.security.app-sandbox</key><true/><key>com.apple.security.cs.allow-jit</key><true/><key>com.apple.security.network.server</key><true/><!--Addfromhere... --><key>com.apple.security.network.client</key><true/><!--tohere.--></dict></plist>
    1. Edit theRelease.entitlements file to match the following code

    macos/Runner/Release.entitlements

    <?xmlversion="1.0"encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plistversion="1.0"><dict><key>com.apple.security.app-sandbox</key><true/><!--Addfromhere... --><key>com.apple.security.network.client</key><true/><!--tohere.--></dict></plist>

    Running this as is should display a welcome screen and a game over or won screen on all platforms. Those screens might be a little simplistic and it would be nice to have a score. So, guess what you'll be doing in the next step!

    10. Keep score

    Add score to the game

    In this step, you expose the game score to the surrounding Flutter context. In this step you expose state from the Flame game to the surrounding Flutter state management. This enables the game code to update the score every time the player breaks a brick.

    1. Modify theBrickBreaker game as follows.

    lib/src/brick_breaker.dart

    import'dart:async';import'dart:math'asmath;import'package:flame/components.dart';import'package:flame/events.dart';import'package:flame/game.dart';import'package:flutter/material.dart';import'package:flutter/services.dart';import'components/components.dart';import'config.dart';enumPlayState{welcome,playing,gameOver,won}classBrickBreakerextendsFlameGamewithHasCollisionDetection,KeyboardEvents,TapDetector{BrickBreaker():super(camera:CameraComponent.withFixedResolution(width:gameWidth,height:gameHeight,),);finalValueNotifier<int>score=ValueNotifier(0);//Addthislinefinalrand=math.Random();doublegetwidth=>size.x;doublegetheight=>size.y;latePlayState_playState;PlayStategetplayState=>_playState;setplayState(PlayStateplayState){_playState=playState;switch(playState){casePlayState.welcome:casePlayState.gameOver:casePlayState.won:overlays.add(playState.name);casePlayState.playing:overlays.remove(PlayState.welcome.name);overlays.remove(PlayState.gameOver.name);overlays.remove(PlayState.won.name);}}@overrideFutureOr<void>onLoad()async{super.onLoad();camera.viewfinder.anchor=Anchor.topLeft;world.add(PlayArea());playState=PlayState.welcome;}voidstartGame(){if(playState==PlayState.playing)return;world.removeAll(world.children.query<Ball>());world.removeAll(world.children.query<Bat>());world.removeAll(world.children.query<Brick>());playState=PlayState.playing;score.value=0;//Addthislineworld.add(Ball(difficultyModifier:difficultyModifier,radius:ballRadius,position:size/2,velocity:Vector2((rand.nextDouble()-0.5)*width,height*0.2,).normalized()..scale(height/4),),);world.add(Bat(size:Vector2(batWidth,batHeight),cornerRadius:constRadius.circular(ballRadius/2),position:Vector2(width/2,height*0.95),),);world.addAll([for(vari=0;i <brickColors.length;i++)for(varj=1;j <=5;j++)Brick(position:Vector2((i+0.5)*brickWidth+(i+1)*brickGutter,(j+2.0)*brickHeight+j*brickGutter,),color:brickColors[i],),]);}@overridevoidonTap(){super.onTap();startGame();}@overrideKeyEventResultonKeyEvent(KeyEventevent,Set<LogicalKeyboardKey>keysPressed,){super.onKeyEvent(event,keysPressed);switch(event.logicalKey){caseLogicalKeyboardKey.arrowLeft:world.children.query<Bat>().first.moveBy(-batStep);caseLogicalKeyboardKey.arrowRight:world.children.query<Bat>().first.moveBy(batStep);caseLogicalKeyboardKey.space:caseLogicalKeyboardKey.enter:startGame();}returnKeyEventResult.handled;}@overrideColorbackgroundColor()=>constColor(0xfff2e8cf);}

    By addingscore to the game, you tie the game's state to Flutter state management.

    1. Modify theBrick class to add a point to the score when the player breaks bricks.

    lib/src/components/brick.dart

    import'package:flame/collisions.dart';import'package:flame/components.dart';import'package:flutter/material.dart';import'../brick_breaker.dart';import'../config.dart';import'ball.dart';import'bat.dart';classBrickextendsRectangleComponentwithCollisionCallbacks,HasGameReference<BrickBreaker>{Brick({requiredsuper.position,requiredColorcolor}):super(size:Vector2(brickWidth,brickHeight),anchor:Anchor.center,paint:Paint()..color=color..style=PaintingStyle.fill,children:[RectangleHitbox()],);@overridevoidonCollisionStart(Set<Vector2>intersectionPoints,PositionComponentother,){super.onCollisionStart(intersectionPoints,other);removeFromParent();game.score.value++;//Addthislineif(game.world.children.query<Brick>().length==1){game.playState=PlayState.won;game.world.removeAll(game.world.children.query<Ball>());game.world.removeAll(game.world.children.query<Bat>());}}}

    Make a good-looking game

    Now that you can keep score in Flutter, it's time to put together the widgets to make it look good.

    1. Createscore_card.dart inlib/src/widgets and add the following.

    lib/src/widgets/score_card.dart

    import'package:flutter/material.dart';classScoreCardextendsStatelessWidget{constScoreCard({super.key,requiredthis.score});finalValueNotifier<int>score;@overrideWidgetbuild(BuildContextcontext){returnValueListenableBuilder<int>(valueListenable:score,builder:(context,score,child){returnPadding(padding:constEdgeInsets.fromLTRB(12,6,12,18),child:Text('Score: $score'.toUpperCase(),style:Theme.of(context).textTheme.titleLarge!,),);},);}}
    1. Createoverlay_screen.dart inlib/src/widgets and add the following code.

    This adds more polish to the overlays using the power of theflutter_animate package to add some movement and style to the overlay screens.

    lib/src/widgets/overlay_screen.dart

    import'package:flutter/material.dart';import'package:flutter_animate/flutter_animate.dart';classOverlayScreenextendsStatelessWidget{constOverlayScreen({super.key,requiredthis.title,requiredthis.subtitle});finalStringtitle;finalStringsubtitle;@overrideWidgetbuild(BuildContextcontext){returnContainer(alignment:constAlignment(0,-0.15),child:Column(mainAxisSize:MainAxisSize.min,children:[Text(title,style:Theme.of(context).textTheme.headlineLarge,).animate().slideY(duration:750.ms,begin:-3,end:0),constSizedBox(height:16),Text(subtitle,style:Theme.of(context).textTheme.headlineSmall).animate(onPlay:(controller)=>controller.repeat()).fadeIn(duration:1.seconds).then().fadeOut(duration:1.seconds),],),);}}

    To get a more in-depth look at the power offlutter_animate, check out theBuilding next generation UIs in Flutter codelab.

    This code changed a lot in theGameApp component. First, to enableScoreCard to access thescore , you convert it from aStatelessWidget toStatefulWidget. The addition of the score card requires the addition of aColumn to stack the score above the game.

    Second, to enhance the welcome, game over, and won experiences, you added the newOverlayScreen widget.

    lib/src/widgets/game_app.dart

    import'package:flame/game.dart';import'package:flutter/material.dart';import'package:google_fonts/google_fonts.dart';import'../brick_breaker.dart';import'../config.dart';import'overlay_screen.dart';//Addthisimportimport'score_card.dart';//AndthisonetooclassGameAppextendsStatefulWidget{//ModifythislineconstGameApp({super.key});@override//Addfromhere...State<GameApp>createState()=>_GameAppState();}class_GameAppStateextendsState<GameApp>{latefinalBrickBreakergame;@overridevoidinitState(){super.initState();game=BrickBreaker();}//Tohere.@overrideWidgetbuild(BuildContextcontext){returnMaterialApp(debugShowCheckedModeBanner:false,theme:ThemeData(textTheme:GoogleFonts.pressStart2pTextTheme().apply(bodyColor:constColor(0xff184e77),displayColor:constColor(0xff184e77),),),home:Scaffold(body:Container(decoration:constBoxDecoration(gradient:LinearGradient(begin:Alignment.topCenter,end:Alignment.bottomCenter,colors:[Color(0xffa9d6e5),Color(0xfff2e8cf)],),),child:SafeArea(child:Padding(padding:constEdgeInsets.all(16),child:Center(child:Column(//Modifyfromhere...children:[ScoreCard(score:game.score),Expanded(child:FittedBox(child:SizedBox(width:gameWidth,height:gameHeight,child:GameWidget(game:game,overlayBuilderMap:{PlayState.welcome.name:(context,game)=>constOverlayScreen(title:'TAP TO PLAY',subtitle:'Use arrow keys or swipe',),PlayState.gameOver.name:(context,game)=>constOverlayScreen(title:'G A M E   O V E R',subtitle:'Tap to Play Again',),PlayState.won.name:(context,game)=>constOverlayScreen(title:'Y O U   W O N ! ! !',subtitle:'Tap to Play Again',),},),),),),],),//Tohere.),),),),),);}}

    With that all in place, you should now be able to run this game on any of the six Flutter target platforms. The game should resemble the following.

    A screenshot of brick_breaker showing the pre-game screen inviting the user to tap the screen to play the game

    A screenshot of brick_breaker showing the game over screen overlaid on top of a bat and some of the bricks

    11. Congratulations

    Congratulations, you succeeded in building a game with Flutter and Flame!

    You built a game using the Flame 2D game engine and embedded it in a Flutter wrapper. You used Flame's Effects to animate and remove components. You used Google Fonts and Flutter Animate packages to make the whole game look well designed.

    What's next?

    Check out some of these codelabs...

    Further reading

    Except as otherwise noted, the content of this page is licensed under theCreative Commons Attribution 4.0 License, and code samples are licensed under theApache 2.0 License. For details, see theGoogle Developers Site Policies. Java is a registered trademark of Oracle and/or its affiliates.