Scoring and keeping the highest scores are essential for any game. Some games base the score on the number of coins collected, some base it on enemies killed, and some base it on survival time.
We can’t ignore sound effects and background music either. Combined, those two could make or break a game.
Besides a great game isn’t really a great game if it’s too quiet.

Welcome to the fourth part of the game development tutorial series with Flame and Flutter. We’re picking up where we left off and if you haven’t read the previous parts, I recommend you start with them.
Here’s the whole series:
0. [Introduction to game development](https://jap.alekhin.io/create-mobile-game-flutter-flame-beginner-tutorial)
1. [Set up a playable game](https://jap.alekhin.io/2d-casual-mobile-game-tutorial-flame-flutter-part-1)
2. [Graphics and animation](https://jap.alekhin.io/game-graphics-and-animation-tutorial-flame-flutter-part-2)
3. [Views and dialog boxes](https://jap.alekhin.io/views-dialog-boxes-tutorial-flame-flutter-part-3)
4. **Scoring, storage, and sound** (you are here)
5. _Finishing up and packaging_
# Prerequisites
1. **All the requirements** from the previous part.
2. **More graphics assets** – Graphics assets can be found all over game resource sites on the internet ([Open Game Art](https://opengameart.org/) for example). Just make sure to credit the makers.
3. **Sound and music assets** – Similar to graphics assets audio files can also be found on game resource sites like Open Game Art. There are even resource sites dedicated to audio resources like [BenSound.com](https://www.bensound.com/). Again, same with graphics, just make sure you understand the license and credit the creators if necessary.
We will be using the conventions from the previous parts regarding code and file path references.
_All the code for this tutorial is available for viewing and download on [this GitHub repository](https://github.com/japalekhin/langaw)._
# Another Resource Pack
In this part, we have another resource pack. This pack contains additional graphics, background music, and some sound effects.
[](https://jap.alekhin.io/wp-content/uploads/2019/03/resource-pack-part-4.zip)
[Click the image above or this link to download!](https://jap.alekhin.io/wp-content/uploads/2019/03/resource-pack-part-4.zip)
----------
**Important Note**: The resource pack above can be used if you’re following along with this tutorial. It is part of [the Langaw project on GitHub](https://github.com/japalekhin/langaw) which is licensed with a **`CC-BY-NC-ND`** license.
It means that you can share, copy, and redistribute the material in any medium or format with some restrictions.
- You must give appropriate credit, provide a link to the license, and indicate if changes were made.
- You may not use the material for commercial purposes.
- If you remix, transform, or build upon the material, you may not distribute the modified material.
- You may not apply legal terms or technological measures that legally restrict others from doing anything the license permits.
[Learn more about the CC-BY-NC-ND license by clicking here](https://creativecommons.org/licenses/by-nc-nd/2.0/legalcode).
----------
# Let’s continue the build
In this part, we’ll be focusing on two main things: scoring and sound effects.
We will be using another Flutter plugin for storing the high scores. We need to store the highest score that the player achieves.
As for the sound, we’ll use an audio library that is already included with Flame.
## Step 1: Scoring
The game’s goal, for now, is just to tap flies endlessly until the player misses. Aside from that, there’s no goal, no reward.
Let’s add in another goal. Let’s make it so that when a player taps and hits a fly, one point will be rewarded and added to the current score. Like any other game, the score starts at zero and resets to zero whenever a game is started.
### A variable to keep the score
Let’s jump into the code and open up `./lib/langaw-game.dart`. Add in another instance variable using the following code:
```
int score;
```
**Just a quick refresher**, _instance variables_ are grouped together just below the `class LangawGame` line.
Inside the `initialize` method, let’s initialize the value of the `score` variable as soon as possible with the following line:
```
score = 0;
```
We need to reset this value every time the start button is clicked (every time a game is started).
Jump over to `./lib/components/start-button.dart` and add the following line inside the `onTapDown` handler:
```
game.score = 0;
```
### Displaying the score
So far so good! Next, we need to display the score.
The rendering code can be inside the game class. But, since rendering text on the canvas is quite a handful, let’s encapsulate that logic in its own component.
Create a component in `./lib/components/score-display.dart`:
```
import 'dart:ui';
import 'package:flutter/painting.dart';
import 'package:langaw/langaw-game.dart';
class ScoreDisplay {
final LangawGame game;
ScoreDisplay(this.game) {}
void render(Canvas c) {}
void update(double t) {}
}
```
**Let’s break it down**: Starting with the `import` statements at the top, `dart:ui` gives us access to the `Canvas` and `Offset` classes; `package:flutter/painting.dart` gives us access to the `TextPainter` class; and `package:langaw/langaw-game.dart` gives us access to the game class.
_**Note**: We already have an instance variable named `game` and this must be provided when creating an instance of this class. The same as all other components, controllers, and views we’ve defined in the previous parts_.
Let’s add three more instance variables: `painter` is the `TextPainter` that we’ll use to render the value of the score on the screen; `textStyle` contains the styles that will control how the score is rendered; `position` will contain the `Offset` where the score will be painted on.
```
TextPainter painter;
TextStyle textStyle;
Offset position;
```
Then we have the constructor where we initialize the values of the instance variables.
```
ScoreDisplay(this.game) {
painter = TextPainter(
textAlign: TextAlign.center,
textDirection: TextDirection.ltr,
);
textStyle = TextStyle(
color: Color(0xffffffff),
fontSize: 90,
shadows: <Shadow>[
Shadow(
blurRadius: 7,
color: Color(0xff000000),
offset: Offset(3, 3),
),
],
);
position = Offset.zero;
}
```
**Breakdown**: First, we initialize the `painter` variable with a new instance of `TextPainter`. We set its `textAlign`property to center since we’ll be rendering the score horizontally centered on the screen. The `textDirection`property is set to **LTR** (left-to-right) since the game is in English (which has a written direction of left to right).
Next, the `textStyle` property is initialized with an instance of `TextStyle`. Three properties are set: `color` is set to `Color(0xffffffff)` (which is solid white); font size is set to `90 logical pixels`; and the `shadows`property is set to a `List` of `Shadow`s that contains only one item. This item defines a shadow that is offset to `3 logical pixels` to the right and to the bottom. This gives the rendered text depth and contrast against the background in case the score is rendered on top of another white object (like the clouds).
To learn more about logical pixels please check [the Flutter documentation about it](https://docs.flutter.io/flutter/dart-ui/Window/devicePixelRatio.html).
The `update` method actually happens before the render (both initially and during the game loop) so let’s write it in first.
```
void update(double t) {
if ((painter.text?.text ?? '') != game.score.toString()) {
painter.text = TextSpan(
text: game.score.toString(),
style: textStyle,
);
painter.layout();
position = Offset(
(game.screenSize.width / 2) - (painter.width / 2),
(game.screenSize.height * .25) - (painter.height / 2),
);
}
}
```
**Breakdown**: To avoid unnecessary recalculation of the text layout, nothing really happens if the text property (of the text property) of the `painter` variable is equivalent to the string form of the current score.
This is done with the following check:
```
(painter.text?.text ?? '') != game.score.toString()
```
The left part of the boolean expression might be a little confusing, so let me explain. This expression uses Dart’s null-aware operators.
The `?.` operator checks if the object just before it is `null`, if it is, immediately stop the whole expression and return `null`. We already know that `painter` is initialized and not null so we don’t check it. We’re not sure though if the `text` property of `painter` is `null` or not, so we use this operator.
Another operator used is `??`. This operator returns the left-hand side expression if it’s not `null`, if it is, the operator returns the right-hand side expression.
With the whole expression, if `painter`‘s `text` property is not set, the whole of `painter.text?.text` returns `null`. Since it’s followed by `??`, if the value returned is an empty string. This final value is what gets compared to `game.score.toString()`. On the other hand, if `painter`‘s `text` property is set, the actual current score is returned.
You can [read more about null-aware operators from this article](https://medium.com/@thinkdigitalsoftware/null-aware-operators-in-dart-53ffb8ae80bb). If you have more questions, don’t hesitate to ask me on [my Discord server](https://discord.gg/SNvNfas).
Now if the `painter`‘s text is not the same as the current score, we update its text property with a new instance of the `TextSpan` class that takes in the current value of the `score` variable in `game` and the reusable `textStyle`variable. Then the `layout` method is called so the `TextPainter` can calculate the dimensions of the new text it was just assigned.
Then we calculate a new instance of `Offset` and assign it to the `position` variable. We want the score to be horizontally centered. As for the vertical position, let’s place the score’s vertical center in about a quarter of the screen’s height from the top.
If you need an explanation about this calculation, just let me know.
Finally, inside the `render` method, add the following line of code:
```
void render(Canvas c) {
painter.paint(c, position);
}
```
**One line breakdown**: We render the score by calling `painter`‘s `paint` method and supplying the parameters it requires: a canvas to draw on and an `Offset` to tell the painter where to draw the score.
The whole `./lib/components/score-display.dart` should look like this:
```
import 'dart:ui';
import 'package:flutter/painting.dart';
import 'package:langaw/langaw-game.dart';
class ScoreDisplay {
final LangawGame game;
TextPainter painter;
TextStyle textStyle;
Offset position;
ScoreDisplay(this.game) {
painter = TextPainter(
textAlign: TextAlign.center,
textDirection: TextDirection.ltr,
);
textStyle = TextStyle(
color: Color(0xffffffff),
fontSize: 90,
shadows: <Shadow>[
Shadow(
blurRadius: 7,
color: Color(0xff000000),
offset: Offset(3, 3),
),
],
);
position = Offset.zero;
}
void render(Canvas c) {
painter.paint(c, position);
}
void update(double t) {
if ((painter.text?.text ?? '') != game.score.toString()) {
painter.text = TextSpan(
text: game.score.toString(),
style: textStyle,
);
painter.layout();
position = Offset(
(game.screenSize.width / 2) - (painter.width / 2),
(game.screenSize.height * .25) - (painter.height / 2),
);
}
}
}
```
### Rendering the score component
To actually render the score display component, we must add it to the game class and include it when executing the game loop.
Let’s open up `./lib/langaw-game.dart` and do the usual steps. Import the class file we just created:
```
import 'package:langaw/components/score-display.dart';
```
Then let’s add an instance variable that will hold an instance of `ScoreDisplay`:
```
ScoreDisplay scoreDisplay;
```
Add the following inside the `initialize` method (after `resize` has been called) to create a new instance of `ScoreDisplay` and assign it to the `scoreDisplay` variable (mind the capitalization), preferably below the button initializations:
```
scoreDisplay = ScoreDisplay(this);
```
Inside the `update` method, check if the current view is set to `View.playing`, and if it is, call `scoreDisplay`‘s `update` method. You can put this line anywhere.
Putting it as the last line (just above the method closing brace) is fine.
```
if (activeView == View.playing) scoreDisplay.update(t);
```
Then inside the game class’ `render` method, we do the same thing but call the `render` method instead. Remember that the order that you write the lines inside this method is the actual order the graphics are drawn on the screen with.
We want the score to be just above the background but behind everything else so flies can fly over it. So put the following line just after rendering the background:
```
if (activeView == View.playing) scoreDisplay.render(canvas);
```
With the above lines of code added, you should see something like this when you run the game:

### Score for every fly
For players to earn score points, flies must be tapped and dropped.
To have this functionality, open up `./lib/components/fly.dart`. Let’s import the `View` `enum` file as we will be needing access to the `View` values below.
```
import 'package:langaw/view.dart';
```
Inside the `onTapDown` handler, replace the existing single line of code with the following block:
```
if (!isDead) {
isDead = true;
if (game.activeView == View.playing) {
game.score += 1;
}
}
```
**Breakdown**: When a fly is tapped, we first check if it’s alive (`!isDead`). Nothing happens if the fly is already dead. If it’s alive, we set its `isDead` property to `true` to let the component know that this fly is now dead (just like what we previously had in here).
After that, we check if the current view is the “playing” view. We don’t need to add scores if the player is not playing. If the player is currently playing, we add `1` to the score variable. This should trigger an update in the `ScoreDisplay` instance.
Try running the game at this point to see something like the screenshot below.

### Smarter flies
Flies don’t just fly around, they eat too. Besides, it would be boring if the player just waits and carefully taps the flies to not miss.
Let’s add another losing condition.
As soon as a fly appears on screen, it shows a countdown timer. This counter corresponds to how much the flies have eaten. When the timer hits zero, it means that the fly is full and the player loses.
This way, the game becomes more challenging and a little less boring.
We have to make sure that the player notices this countdown timer and see which flies are about to get full. We’ll use the callout graphics from the resource pack to show this countdown timer.
Let’s add the callout graphics from the resource pack into our assets directory under `./assets/images/ui/callout.png`. Then we make sure to add it in `./pubspec.yaml` using the following line along with the other assets:
```
- assets/images/ui/callout.png
```
Then open up `./lib/main.dart` and **add `ui/callout.png`** to the list of preloaded images. This is inside the array that is a parameter to the `Flame.images.loadAll` call.
```
Flame.images.loadAll([
// all the other images
'ui/callout.png',
]);
```
Next, create a component for this callout in `./lib/components/callout.dart`.
```
import 'dart:ui';
import 'package:flame/sprite.dart';
import 'package:langaw/components/fly.dart';
class Callout {
final Fly fly;
Rect rect;
Sprite sprite;
double value;
TextPainter tp;
TextStyle textStyle;
Offset textOffset;
Callout(this.fly) {}
void render(Canvas c) {}
void update(double t) {}
}
```
**Breakdown**: Here we create yet another component that is pretty standard. Except that our reference to the parent is not to `game` but to a fly. This is because a `Fly` class will instantiate this instead of the game class. Like a sub-component of the fly component.
This class has additional instance variables that will be used for painting the value inside the callout.
Let’s proceed by initializing the values inside the constructor:
```
Callout(this.fly) {
sprite = Sprite('ui/callout.png');
value = 1;
tp = TextPainter(
textAlign: TextAlign.center,
textDirection: TextDirection.ltr,
);
textStyle = TextStyle(
color: Color(0xff000000),
fontSize: 15,
);
}
```
**Breakdown**: Don’t be confused, the body is actually just four lines. The last two are expanded vertically for readability. Inside the constructor, we just assign initial values to our instance variables.
Inside the `update` method, we subtract a certain amount to the `value` variable and if gets down to zero, we set the game’s active view to the “you lose” view. All this should only happen if the game is currently in to the “playing” view. Put the following block into the `update` method.
```
if (fly.game.activeView == View.playing) {
value = value - .5 * t;
if (value <= 0) {
fly.game.activeView = View.lost;
}
}
```
**Breakdown**: First we check if the current view is `View.playing`, if it is we subtract `.5 * t` from value. The `t`variable contains a fraction of the second from the last time `update` was called. This calculation makes sure that **a fly has a lifetime of two seconds**.
Then we check if the value has hit zero. If it has, we tell the game that the player loses.
After that, we make sure that the `rect` variable for this callout is updated so that the `render` method places it correctly relative to the parent fly. This block of code goes just below the `value` deduction block.
```
rect = Rect.fromLTWH(
fly.flyRect.left - (fly.game.tileSize * .25),
fly.flyRect.top - (fly.game.tileSize * .5),
fly.game.tileSize * .75,
fly.game.tileSize * .75,
);
```
**Breakdown**: Just like all other `Rect` initializations we’ve done so far, this is just one line spread vertically. The last two parameters are the **W**idth and **H**eight of the `rect` which are both set to three-quarters of the game tile size. The value for **L**eft is just the same as the **L**eft of the fly’s `rect` minus a quarter of the game tile size. The **T**op value uses the same logic but subtracts half of the game tile size.
Still inside the `update` method, the last block of code updates the text painter which draws the current value inside the callout graphics.
```
tp.text = TextSpan(
text: (value * 10).toInt().toString(),
style: textStyle,
);
tp.layout();
textOffset = Offset(
rect.center.dx - (tp.width / 2),
rect.top + (rect.height * .4) - (tp.height / 2),
);
```
**Breakdown**: Since we already initialized the `tp` variable with an instance of the `TextPainter` class, we just set its text property into an instance of the `TextSpan` class passing in the current `value` multiplied by ten converted to an integer and then to a string.
The value is multiplied by ten to make it look like it’s counting from nine down to zero.
Then we call the `layout` method so that the `tp` knows how big the text is going to be given the string of text and the style provided to it.
Next, we update the value of `textOffset` with a new `Offset` passing in a calculation that will center the text inside the white area of the callout.
Finally, write the `render` method to look like the following block of code:
```
void render(Canvas c) {
sprite.renderRect(c, rect);
tp.paint(c, textOffset);
}
```
First, we render the callout graphics, which should be a familiar line of code now. Then we draw the text using `TextPainter`‘s `paint` method passing the `textOffset` variable that we just updated in the `update` method.
The whole callout class file should look like this:
```
import 'dart:ui';
import 'package:flame/sprite.dart';
import 'package:flutter/painting.dart';
import 'package:langaw/components/fly.dart';
import 'package:langaw/view.dart';
class Callout {
final Fly fly;
Rect rect;
Sprite sprite;
double value;
TextPainter tp;
TextStyle textStyle;
Offset textOffset;
Callout(this.fly) {
sprite = Sprite('ui/callout.png');
value = 1;
tp = TextPainter(
textAlign: TextAlign.center,
textDirection: TextDirection.ltr,
);
textStyle = TextStyle(
color: Color(0xff000000),
fontSize: 15,
);
}
void render(Canvas c) {
sprite.renderRect(c, rect);
tp.paint(c, textOffset);
}
void update(double t) {
if (fly.game.activeView == View.playing) {
value = value - .5 * t;
if (value <= 0) {
fly.game.activeView = View.lost;
}
}
rect = Rect.fromLTWH(
fly.flyRect.left - (fly.game.tileSize * .25),
fly.flyRect.top - (fly.game.tileSize * .5),
fly.game.tileSize * .75,
fly.game.tileSize * .75,
);
tp.text = TextSpan(
text: (value * 10).toInt().toString(),
style: textStyle,
);
tp.layout();
textOffset = Offset(
rect.center.dx - (tp.width / 2),
rect.top + (rect.height * .4) - (tp.height / 2),
);
}
}
```
Now we just need to add an instance of the callout component into the `Fly` class. This process should be familiar as we have done it repetitively in the previous parts. Open up `./lib/components/fly.dart` and import the `Callout` class file:
```
import 'package:langaw/components/callout.dart';
```
Then add an instance variable that will hold the callout:
```
Callout callout;
```
Inside the `Fly` constructor, initialize the `callout` variable and pass `this` as the callout’s reference to “this fly”:
```
callout = Callout(this);
```
The callout itself must be updated if the fly is not dead, so inside the `Fly`‘s `update` method write the following line at the bottom of the `else` (the fly is not dead) block.
```
callout.update(t);
```
Lastly, render the callout if the fly is not dead (inside the `else` block) and the game’s current view is set to “playing”. Put this inside the `render` method:
```
if (game.activeView == View.playing) {
callout.render(c);
}
```
If you run the game now, you should see something like this:

_**[View the code at this step on GitHub](https://github.com/japalekhin/langaw/tree/c08ce511103288c25b9cab457add7a9a01be29e0)**_.
## Step 2: High score storage
Gaining those precious points to add to the score would be a waste if the highest score is not recorded by the game.
Let’s record the highest score so players have the opportunity to get better at the game by challenging the current best performance.
When recording the highest score, we only need to store one piece of information. Just the score, an integer.
This simple task can be accomplished by the `shared_preferences` plugin. This plugin contains the `SharedPreferences` class that handles storage of simple data (numbers, strings, and booleans). It also internally handles how to save the data based on what platform (iOS or Android) the game is running.
### Prepare the data storage
Just like Flame, `shared_preferences` is a plugin for Flutter. To install this plugin, open up `./pubspec.yaml` and add the following line under the `dependencies:` section just below the `flame` line.
```
shared_preferences: ^0.5.1+2
```
_**Note**: Mind the indentation, `./pubspec.yaml` is dependent on the indentation to know which lines are under what section._
Next, run `flutter packages get` in a terminal to make sure that the `shared_preferences` plugin is downloaded to your computer like so:
```
$ flutter packages get
```
Alternatively, if you’re following by using VS Code, you can press `Ctrl + Shift + P`, type “flutter” and click `Flutter: Get Packages`.
To easily read and write data, we need an easily accessible instance of `SharedPreferences` in an instance variable in the `game` context.
Open up `./lib/langaw-game.dart` and import the `shared_preferences` package file:
```
import 'package:shared_preferences/shared_preferences.dart';
```
Then add a `final` instance variable inside the `LangawGame` class. This makes having an instance of the `SharedPreferences` ready before even creating the `LangawGame` instance. Declare the instance variable like so:
```
final SharedPreferences storage;
```
Any instance variable that is marked `final` must have an initial value by the time it is declared or a value must be fed to it via the constructor. So let’s modify our constructor method so that it looks like the following block of code:
```
LangawGame(this.storage) {
initialize();
}
```
In the constructor, when you require a parameter that is prefixed by a `this.` it means that whatever value passed to it will be the value of the variable that it’s referring to by name.
After that, let’s jump over the `./lib/main.dart` file so we can initialize the game class properly.
First, we need to import the `shared_preferences` package file here too:
```
import 'package:shared_preferences/shared_preferences.dart';
```
Next, create an instance of the `SharedPreferences` inside the `main` function.
```
SharedPreferences storage = await SharedPreferences.getInstance();
```
_**Note**: The `.getInstance` factory returns a `Future` so we must use the `await` keyword to pause the execution and wait for whatever the `Future` returns (which should be an instance of `SharedPreferences`). The `main`function is already `async` so we can await for `Future`s in its body._
In the part of the code where you declare an instance of the `LangawGame` class, pass the `storage` variable we just declared as the first (and only) parameter. The code should look like the following:
```
LangawGame game = LangawGame(storage);
```
Now we should have access to the storage variable wherever we have access to the `game` (instance of the `LangawGame` class).
### Display the high score
Our game should feature an always-displayed high-score. We can do this using another component similar to the `ScoreDisplay` class.
Create a class file in `./lib/components/highscore-display.dart` and write in the following class:]
```
import 'dart:ui';
import 'package:flutter/painting.dart';
import 'package:langaw/langaw-game.dart';
class HighscoreDisplay {
final LangawGame game;
TextPainter painter;
TextStyle textStyle;
Offset position;
HighscoreDisplay(this.game) {}
void render(Canvas c) {}
}
```
As you can see, it’s a pretty standard class file without an `update` method since the score will be updated manually.
The instance variables must be initialized by the time an instance of this class is created so let’s modify the constructor with the following code:
```
HighscoreDisplay(this.game) {
painter = TextPainter(
textAlign: TextAlign.center,
textDirection: TextDirection.ltr,
);
Shadow shadow = Shadow(
blurRadius: 3,
color: Color(0xff000000),
offset: Offset.zero,
);
textStyle = TextStyle(
color: Color(0xffffffff),
fontSize: 30,
shadows: [shadow, shadow, shadow, shadow],
);
position = Offset.zero;
updateHighscore();
}
```
**Constructor breakdown**: The first part initializes the `painter` variable with an instance of the `TextPainter`class with the required values for text alignment and text direction.
Next, we build a local `Shadow` variable that will help create a stroke effect when added to the `textStyle` that is initialized next. We put in four instances of the `shadow` variable so together when overlapped, they look like a text stroke.
The `position` variable is set to an initial value of zero (`0, 0`) as this will be calculated later.
Finally, we call a function named `updateHighscore()` which we haven’t defined or written yet. This handles the manual update of the high-score value and the text painted by the `painter` object.
Let’s build the manual update method by adding in the following function:
```
void updateHighscore() {
int highscore = game.storage.getInt('highscore') ?? 0;
painter.text = TextSpan(
text: 'High-score: ' + highscore.toString(),
style: textStyle,
);
painter.layout();
position = Offset(
game.screenSize.width - (game.tileSize * .25) - painter.width,
game.tileSize * .25,
);
}
```
**Breakdown**: In this method, we get the high-score value from the `SharedPreferences` instance we have saved in the `storage` variable of the game class. Since our scores (including the high-score) are just whole numbers we’ll store them as integers.
Then we update the `painter`‘s `text` property to a new instance of `TextSpan` passing in the high-score we just retrieved as the text. This is almost similar to the updating process in `ScoreDisplay`.
After calling `layout` (which determines the size of the text when painted), we set the `position` variable to a new `Offset` whose values will make the painted text’s right side at about one-fourth from the right edge of the screen and it’s top at the same distance from the top edge of the screen.
We finish the class by writing in the contents of the `render` method:
```
void render(Canvas c) {
painter.paint(c, position);
}
```
Nothing complicated here, just a painting the high-score on the screen at the `position` pre-calculated in the `updateHighscore` method.
Here’s the whole class file:
```
import 'dart:ui';
import 'package:flutter/painting.dart';
import 'package:langaw/langaw-game.dart';
class HighscoreDisplay {
final LangawGame game;
TextPainter painter;
TextStyle textStyle;
Offset position;
HighscoreDisplay(this.game) {
painter = TextPainter(
textAlign: TextAlign.center,
textDirection: TextDirection.ltr,
);
Shadow shadow = Shadow(
blurRadius: 3,
color: Color(0xff000000),
offset: Offset.zero,
);
textStyle = TextStyle(
color: Color(0xffffffff),
fontSize: 30,
shadows: [shadow, shadow, shadow, shadow],
);
position = Offset.zero;
updateHighscore();
}
void updateHighscore() {
int highscore = game.storage.getInt('highscore') ?? 0;
painter.text = TextSpan(
text: 'High-score: ' + highscore.toString(),
style: textStyle,
);
painter.layout();
position = Offset(
game.screenSize.width - (game.tileSize * .25) - painter.width,
game.tileSize * .25,
);
}
void render(Canvas c) {
painter.paint(c, position);
}
}
```
The only thing missing now is to add it to the game class. Let’s do that by opening the file `./lib/langaw-game.dart`.
Import the `HighscoreDisplay` class with the following line:
```
import 'package:langaw/components/highscore-display.dart';
```
Then add an instance variable that will hold an instance of `HighscoreDisplay`.
```
HighscoreDisplay highscoreDisplay;
```
Inside the `initialize` method, initialize the `highscoreDisplay` variable (preferably below `scoreDisplay`):
```
highscoreDisplay = HighscoreDisplay(this);
```
Finally inside the `render` method, right after rendering the background (before rendering `scoreDisplay`), render the high score using the following code:
```
highscoreDisplay.render(canvas);
```
Try running the game and you should see some text on the upper-right corner of the screen indicating the current high-score (which is just zero for now).

### Updating the high score
That high-score display is useless right now. It needs to be updated every time a player scores as long as the following conditions are met:
1. The player is currently playing (the view is set to “playing”)
2. The current score is higher than the current high-score
To do this, let’s open the file `./lib/components/fly.dart`. Inside the `onTapDown` handler, we already have an `if` block that checks if the player is currently playing.
Inside that block, just below the line where we add a value of `1` to the score, insert the following block:
```
if (game.score > (game.storage.getInt('highscore') ?? 0)) {
game.storage.setInt('highscore', game.score);
game.highscoreDisplay.updateHighscore();
}
```
Breakdown: This block just checks if the new score is higher than what is currently saved as high-score. At this point, we already know that the player is playing because of the enclosing `if` block.
If the condition is satisfied, first we call the `setInt` method passing over the string `'highscore'` and the new value. This method is the equivalent of `getInt` but writes the passed value in the string key reference instead of reading a value.
After that, we update our `HighscoreDisplay` instance manually to show the player that his new score is now the current highest.
The changed section should look like this:

Try running the game now and you’ll see that every time you get a higher score than what is displayed in the high-score (upper-right), that value will be updated.
It should start from zero and follow along as you score. If you play another game, you should see that the previous high-score is retained. This should challenge the player to beat his own record.

_**[View the code at this step on GitHub](https://github.com/japalekhin/langaw/tree/75a16e414d17a06c540e821b680083406d4d7366)**_.
## Step 3: Sound effects
What’s a game without sound effects right? Fortunately for us, Flame made adding sound to a game really easy.
First, we need to create a directory in the assets folder that will contain our sound effects files. Create the directory `./assets/audio/sfx` and paste all the files inside the `./audio/sfx` folderin the resource pack.
You should have the following file structure:
```
./assets
./assets/audio
./assets/audio/sfx
./assets/audio/sfx/haha1.ogg
./assets/audio/sfx/haha2.ogg
./assets/audio/sfx/haha3.ogg
./assets/audio/sfx/haha4.ogg
./assets/audio/sfx/haha5.ogg
./assets/audio/sfx/ouch1.ogg
./assets/audio/sfx/ouch2.ogg
./assets/audio/sfx/ouch3.ogg
./assets/audio/sfx/ouch4.ogg
./assets/audio/sfx/ouch5.ogg
./assets/audio/sfx/ouch6.ogg
./assets/audio/sfx/ouch7.ogg
./assets/audio/sfx/ouch8.ogg
./assets/audio/sfx/ouch9.ogg
./assets/audio/sfx/ouch10.ogg
./assets/audio/sfx/ouch11.ogg
```
Next, we need to tell Flutter that we want these new files to be included in the asset bundle during compilation. So open up `./pubspec.yaml` and add the following block of code under the `asset` section (the `asset` section has been included for reference).
```
assets:
- assets/audio/sfx/haha1.ogg
- assets/audio/sfx/haha2.ogg
- assets/audio/sfx/haha3.ogg
- assets/audio/sfx/haha4.ogg
- assets/audio/sfx/haha5.ogg
- assets/audio/sfx/ouch1.ogg
- assets/audio/sfx/ouch2.ogg
- assets/audio/sfx/ouch3.ogg
- assets/audio/sfx/ouch4.ogg
- assets/audio/sfx/ouch5.ogg
- assets/audio/sfx/ouch6.ogg
- assets/audio/sfx/ouch7.ogg
- assets/audio/sfx/ouch8.ogg
- assets/audio/sfx/ouch9.ogg
- assets/audio/sfx/ouch10.ogg
- assets/audio/sfx/ouch11.ogg
```
Again, with `./pubspec.yaml` file, you must mind the indentation or leading spaces as they control which section an item is under.
Then let’s go to `./lib/main.dart` and add the following block inside the `main` function:
```
Flame.audio.disableLog();
Flame.audio.loadAll(<String>[
'sfx/haha1.ogg',
'sfx/haha2.ogg',
'sfx/haha3.ogg',
'sfx/haha4.ogg',
'sfx/haha5.ogg',
'sfx/ouch1.ogg',
'sfx/ouch2.ogg',
'sfx/ouch3.ogg',
'sfx/ouch4.ogg',
'sfx/ouch5.ogg',
'sfx/ouch6.ogg',
'sfx/ouch7.ogg',
'sfx/ouch8.ogg',
'sfx/ouch9.ogg',
'sfx/ouch10.ogg',
'sfx/ouch11.ogg',
]);
```
**Breakdown**: The first line disables extra debug logging so that it does not write too much log information into the debug console. If you have no idea what this is, you can ignore it, for now. I’ll try to discuss this in a separate article in the future.
The next lines are actually just one line, a function call that has a parameter that is an array. The values of the array have been spread vertically for readability. This preloads all the sound effects files so that they are cached and ready to be played by the game anytime.
You might have noticed that this is the same format as preloading the images.
The next step is to play the sounds at the appropriate places.
We basically have two sound effects. An “ouch” and a “haha”. The _**ouch**_ sound is played when a player taps a fly and kills it. The **_haha_** sound is played every time the player loses.
You might be wondering why we have eleven _hahas_ and five _ouches_.
**Let me share a little game development secret**
Anything repetitive is boring. I get it, most of the things in life is repetitive (breathing, days, game loop), but **we can make our game’s sounds a little less boring by using different versions of that sound effect every time it’s played**. If we play the same sound every time the player kills a fly, it could get really boring really fast. We might as well skip adding sound effects.
To do this every time we need to play a sound we get a random number (from `game`‘s `rnd` variable) and play that corresponding variation of “the same” sound.
Let’s open up `./lib/components/fly.dart` and import Flame into this file:
```
import 'package:flame/flame.dart';
```
Then add the following block of code inside the `onTapDown` handler, right after checking if the tapped fly is not yet dead.
```
Flame.audio.play('sfx/ouch' + (game.rnd.nextInt(11) + 1).toString() + '.ogg');
```
**Breakdown**: We just call the `play` method of Flame’s `audio` library passing in the filename of the audio file we want to play.
Let me get into detail with the randomizer part — `(game.rnd.nextInt(11) + 1)`. The function `nextInt` takes in an integer parameter and returns and integer with a random value from zero up to (but not including) the passed parameter. So if we pass `11`, we could get any number from `0` to `10`. We then add `1` to it so the returned set becomes `1` to `11` matching our filenames (`ouch**1**.ogg`, `ouch**2**.ogg`, …, `ouch**11**.ogg`).
If you try the game right now, you’ll notice that flies now “react verbally” when you tap them. It’s impossible to demo this using a screenshot so I’ll reserve the demo to the final video demo.
Let’s now work on the sound effects for losing a game.
Open up `./lib/langaw-game.dart` and modify code inside the `if` block that checks if the player “is playing” and just tapped on the screen but missed a fly.
```
if (activeView == View.playing && !didHitAFly) {
Flame.audio.play('sfx/haha' + (rnd.nextInt(5) + 1).toString() + '.ogg');
activeView = View.lost;
}
```
**Breakdown**: We’ve added the same call to play a sound effect but pass in the “haha” file randomized to five variations.
We also have one other losing condition so let’s take care of that by opening `./lib/components/callout.dart`and importing Flame to the file.
```
import 'package:flame/flame.dart';
```
Inside the `update` method, modify the `if` block that checks if `value` is now less than or equal to zero so it looks like the following:
```
if (value <= 0) {
Flame.audio.play('sfx/haha' + (fly.game.rnd.nextInt(5) + 1).toString() + '.ogg');
fly.game.activeView = View.lost;
}
```
**Breakdown**: It’s basically the same code as above, the difference is how we access the game’s `rnd` object. We don’t have a direct reference to the `game` instance in this class so we pass through the `fly` reference first, then `fly`‘s `game` reference, then on to the `rnd` object.
Try running the game now. Again, the demo will be in the final video demo at the end of this part.
_**[View the code at this step on GitHub](https://github.com/japalekhin/langaw/tree/22f064e91f42f88198ab3473718662a778b828b5)**_.
## Step 4: Background music
Let’s add in some background music while we’re at it.
BGM (or background music) sets the mood for the game or the current screen the player is on. For this game, we’ll have two different BGMs one for the playing screen and another for all the others.
First, let’s copy the files inside `./audio/bgm` in the resource pack into our game directory and place them in `./lib/assets/audio/bgm` to have a file structure that looks like this:
```
./assets
./assets/audio
./assets/audio/bgm
./assets/audio/bgm/home.mp3
./assets/audio/bgm/playing.mp3
```
These files need to be included in the bundle too so let’s include them in `./pubspec.yaml` assets section (the `assets` and the first `sfx` lines are included for reference):
```
assets:
- assets/audio/bgm/home.mp3
- assets/audio/bgm/playing.mp3
- assets/audio/sfx/haha1.ogg
```
BGM files will be looped so let’s just include them in the preload list. Let’s open `./lib/main.dart` and include the BGM files in the array passed to `Flame.audio.loadAll`:
```
Flame.audio.loadAll([
'bgm/home.mp3',
'bgm/playing.mp3',
'sfx/haha1.ogg',
```
With SFX (or sound effects), we can just play them and forget about them since they play only once. With BGMs though we need to have a reference to their players so we can control them with operations like `pause`, `resume`, and `seek`.
We’ll store the variables that will keep these references in the game class so let’s open up `./lib/langaw-game.dart`.
First thing’s first, we need access to the `AudioPlayer` class so let’s import that first:
```
import 'package:audioplayers/audioplayers.dart';
```
Next, we need instance variables that will hold the reference to the audio players for each of the BGM files.
```
AudioPlayer homeBGM;
AudioPlayer playingBGM;
```
Inside the `initialize` method, we initialize these variables, pause the players, and play the home BGM using the following block:
```
homeBGM = await Flame.audio.loop('bgm/home.mp3', volume: .25);
homeBGM.pause();
playingBGM = await Flame.audio.loop('bgm/playing.mp3', volume: .25);
playingBGM.pause();
playHomeBGM();
```
**Breakdown**: The first line gets an instance of an `AudioPlayer` with the passed filename loaded. We’re using the `loop` method so this will immediately start playing the BGM. We cancel this effect by immediately following it with `homeBGM.pause()` which should be self-explanatory.
You may notice that we have the volume set to `.25` which is one-fourth of its original volume. This is because of personal gaming experience where the music is just too loud and it drowns more important aspects of the game. Feel free to play with this value. Valid values are from `0` (muted) to `1` (full volume).
The next two lines do the same thing but for the playing BGM.
Lastly, we call `playHomeBGM()` (a method we’re yet to write) to start playing the home BGM. Let’s write this function and it’s playing equivalent:
```
void playHomeBGM() {
playingBGM.pause();
playingBGM.seek(Duration.zero);
homeBGM.resume();
}
void playPlayingBGM() {
homeBGM.pause();
homeBGM.seek(Duration.zero);
playingBGM.resume();
}
```
**Breakdown**: These two methods do the same thing but one is for home BGM and the other is for playing BGM. They’re basically opposites or mirrors of each other.
With `playHomeBGM`, we pause the `playingBGM` and set its position back to the beginning (`Duration.zero`). On the other hand, `playPlayingBGM` does the same thing but swap `homeBGM` and `playingBGM`.
Every time the player loses, we should revert back to the `homeBGM`, so inside the `onTapDown` handler and inside the miss condition (just below playing the “haha” SFX), add the following line to pause and reset the `playingBGM` and play the `homeBGM`.
```
playHomeBGM();
```
Then quickly jump over to `./lib/components/callout.dart` and add the following line inside the lose condition (`if` block to check if `value` is less than or equal to zero) just below the line that plays the “haha” SFX:
```
fly.game.playHomeBGM();
```
Finally, the last step is to play the playing BGM when starting a game (tapping the start button) so let’s open up `./lib/components/start-button.dart` and add the following line just before closing the `onTapDown` method:
```
game.playPlayingBGM();
```
It’s time to run the game and listen to the awesome BGMs our game now has. View the final demo video below to listen if you’re not yet following along with your own code.
_**[View the code at this step on GitHub](https://github.com/japalekhin/langaw/tree/a542217f86d35ac75a62cd67b0b388eb6fa50fba)**_.
## Step 5: BGM and SFX control
It’s generally nice to have sound and music in the game, but it’s not for everyone (or every situation). There are times where you just want to play a game in silence.
To do this we will provide our players with two buttons, one mutes the music and the other mutes the sound effects.
### Control buttons
The two buttons we’ll be making will have two states: enabled and disabled. We already have graphical representations for both buttons and both states (four in all) in the resource pack.
Let’s copy the remaining icons from the `./images/ui/` folder of the resource pack to our assets directory in `./assets/images/ui/` to have a file structure that looks like this:
```
./assets
./assets/images
./assets/images/ui
./assets/images/ui/icon-music-disabled.png
./assets/images/ui/icon-music-enabled.png
./assets/images/ui/icon-sound-disabled.png
./assets/images/ui/icon-sound-enabled.png
```
Just like all the other assets, we need to add these files to `./pubspec.yaml`‘s `assets` section to let Flutter know we want these files to be included in the build bundle.
```
- assets/images/ui/icon-music-disabled.png
- assets/images/ui/icon-music-enabled.png
- assets/images/ui/icon-sound-disabled.png
- assets/images/ui/icon-sound-enabled.png
```
Then let’s go to `./lib/main.dart` to preload these icons. Add the following lines to the array of filenames that are passed to the call to `Flame.images.loadAll`:
```
'ui/icon-music-disabled.png',
'ui/icon-music-enabled.png',
'ui/icon-sound-disabled.png',
'ui/icon-sound-enabled.png',
```
Now we’ll create the buttons and add them to the game class. These buttons are similar to the help and credits buttons with minor differences.
Create the class file `./lib/components/music-button.dart` and add write the following class:
```
import 'dart:ui';
import 'package:flame/sprite.dart';
import 'package:langaw/langaw-game.dart';
class MusicButton {
final LangawGame game;
Rect rect;
Sprite enabledSprite;
Sprite disabledSprite;
bool isEnabled = true;
MusicButton(this.game) {
rect = Rect.fromLTWH(
game.tileSize * .25,
game.tileSize * .25,
game.tileSize,
game.tileSize,
);
enabledSprite = Sprite('ui/icon-music-enabled.png');
disabledSprite = Sprite('ui/icon-music-disabled.png');
}
void render(Canvas c) {
if (isEnabled) {
enabledSprite.renderRect(c, rect);
} else {
disabledSprite.renderRect(c, rect);
}
}
void onTapDown() {
if (isEnabled) {
isEnabled = false;
game.homeBGM.setVolume(0);
game.playingBGM.setVolume(0);
} else {
isEnabled = true;
game.homeBGM.setVolume(.25);
game.playingBGM.setVolume(.25);
}
}
}
```
**Breakdown**: Since this is similar to the help and credits buttons, I’ll only break down the different parts.
Instead of keeping just one `sprite` variable to store the button’s sprite. We have two variables, one that holds the enabled sprite and one that holds the disabled sprite. The button is positioned on the upper-left corner of the screen with it’s left and top edges at one-fourth of a tile away from the left and top edges of the screen, respectively.
We also have another variable called `isEnabled` which is a `bool`ean meaning it can hold either `true` or `false`. This variable can be manipulated to toggle the state of the button and to render the appropriate sprite as can be seen inside the `render` method.
The most important difference though, is the `onTapDown` handler. There’s an `if` block that checks if `isEnabled`is set to `true`. If it is, the value is flipped to `false` and the volume of both `homeBGM` and `playingBGM` (in `game`instance) is set to zero. If `isEnabled` is already set to `false`, it toggles the value back to `true` and set’s the volume of both BGMs back to `.25` (the initial value).
Let’s create the other control button at `./lib/components/sound-button.dart` with the following code:
```
import 'dart:ui';
import 'package:flame/sprite.dart';
import 'package:langaw/langaw-game.dart';
class SoundButton {
final LangawGame game;
Rect rect;
Sprite enabledSprite;
Sprite disabledSprite;
bool isEnabled = true;
SoundButton(this.game) {
rect = Rect.fromLTWH(
game.tileSize * 1.5,
game.tileSize * .25,
game.tileSize,
game.tileSize,
);
enabledSprite = Sprite('ui/icon-sound-enabled.png');
disabledSprite = Sprite('ui/icon-sound-disabled.png');
}
void render(Canvas c) {
if (isEnabled) {
enabledSprite.renderRect(c, rect);
} else {
disabledSprite.renderRect(c, rect);
}
}
void onTapDown() {
isEnabled = !isEnabled;
}
}
```
**Breakdown**: You’ll notice that this is almost exactly like the `MusicButton` class with its position is set to the upper-left corner of the screen but a little to the right. Also, the `onTapDown` handler is just a simple flip/toggle of the boolean value. This is because we don’t need to mute the SFX players due to their “play and forget” nature.
For these buttons to show up in the game, we need to add them to the game class. Let’s open up `./lib/langaw-game.dart` and import the buttons we just created:
```
import 'package:langaw/components/music-button.dart';
import 'package:langaw/components/sound-button.dart';
```
Then the instance variables that will hold instances of our button classes:
```
MusicButton musicButton;
SoundButton soundButton;
```
We need to initialize these buttons inside the `initialize` method, below help and credits button initialization is fine:
```
musicButton = MusicButton(this);
soundButton = SoundButton(this);
```
Next, we need to call their `render` methods from the game class’ own `render` method. Please have these buttons rendered last (but before dialog boxes) so that they’re available anytime in the game except when viewing the help and credits dialog boxes:
```
musicButton.render(canvas);
soundButton.render(canvas);
```
Lastly, we need to forward the tap down events to the button’s `onTapDown` handlers. Remember that objects on top should receive tap events first. Since our buttons will be behind the dialog boxes, the hit test for these buttons should be just below the check for dialog boxes tap down handler.
```
// music button
if (!isHandled && musicButton.rect.contains(d.globalPosition)) {
musicButton.onTapDown();
isHandled = true;
}
// sound button
if (!isHandled && soundButton.rect.contains(d.globalPosition)) {
soundButton.onTapDown();
isHandled = true;
}
```
### Sound effects control
The `MusicButton` class is already handling the volume of the BGMs. The only thing missing now is having the `SoundButton`‘s state affect the actual SFX.
What we’ll want to do is check the state of the sound button before trying to play a sound effect. This can easily be done by enclosing the “play” call with an `if` block that checks if the `soundButton`‘s `isEnabled` property is set to `true`.
We have to change three places to achieve this, first in `./lib/langaw-game.dart` (inside the `onTapDown`handler):
```
if (soundButton.isEnabled) {
Flame.audio.play('sfx/haha' + (rnd.nextInt(5) + 1).toString() + '.ogg');
}
```
Second, in `./lib/components/callout.dart` (inside the `update` method):
```
if (fly.game.soundButton.isEnabled) {
Flame.audio.play('sfx/haha' + (fly.game.rnd.nextInt(5) + 1).toString() + '.ogg');
}
```
And last, in `./lib/components/fly.dart` (inside the `onTapDown` handler):
```
if (game.soundButton.isEnabled) {
Flame.audio.play('sfx/ouch' + (game.rnd.nextInt(11) + 1).toString() + '.ogg');
}
```
_**[View the code at this step on GitHub](https://github.com/japalekhin/langaw/tree/3cce4750307ce7a6d8f88b664fa72c213b8b27b2)**_.
# Gameplay test
Here’s a demo of game’s current state. If you’ve been following along, you should have something that looks the same.
https://www.youtube.com/watch?v=eAaikwyDSoU
# Conclusion
That should take care of all the goals we had for this part. We have a scoring system, high-score storage, sound effects, background music, and buttons that mute the SFX and BGM.
Before moving on, let me present a challenge. There’s one little feature that I intentionally left out. The state of the sound and music control buttons reset to “enabled” every time the player launches the game.
By this time, you should have enough knowledge and experience to modify the buttons so that their states are saved. So try it out.
If you find it difficult, don’t hesitate to contact me by [sending me an email](https://jap.alekhin.io/contact) or dropping a comment below. You could also join my [Discord server](https://discord.gg/SNvNfas) so we can communicate in real time.
# What’s next
The final part is next. We’ll be addressing some bugs and publishing our game to Google Play Store. I hope to see you on the finish line!