I Am Rick (Episode 3): Walker Takedown

Building a Flutter game with Rick Grimes.

Alexandros Baramilis
18 min readDec 22, 2019

Intro

Flutter developer Rick Grimes is back with a new kick-ass app!

If you haven’t been following the series so far, check out the previous apps I’ve made:

and the Github repo for the series.

If you’re having trouble installing Flutter on macOS Catalina, check out Setting up Flutter on macOS Catalina.

ALERT: Next episodes are out!

And a few words on why I’m doing this, copied and pasted from Episode 2:

One of the best ways for me to learn something is to write about it, so I’m making this series with the notes that I take as I go through App Brewery’s Flutter Bootcamp. Publishing my notes and code also forces me to maintain a high standard and allows me to easily go back to review or update them.

I’m keeping the Rick Grimes theme alive for as long as I can, because I’m really enjoying having Rick on my phone’s home screen and looking at his pretty face every day. 😄 Don’t worry, App Brewery’s bootcamp doesn’t have Rick in it, but I still highly recommend it if you want to learn Flutter.

Also, if you missed it, check out the Flutter 1.12 release! They added many new cool things, such as support for iOS dark mode, a beta release of web support and an alpha release of macOS support, among a thousand other things!

Walker Takedown

Instead of App Brewery’s (boring) dice rolling app, I will be making a Humans vs. Walkers game, which — despite it also being based on randomness — will have a bit more complex game mechanics!

It goes like this:

You pick Humans or Walkers and your friend picks the other side. Each side has 10 characters and each character has a Strength value. The character with the highest Strength wins the round. After each round, each player taps on his character’s icon to reveal the next random character from his arsenal. The game ends after 10 rounds.

After some image hunting and editing, here are our beloved game characters:

Humans vs. Walkers (and some Whisperers)

You can find the individual assets here.

Copy them into your images folder and update the pubspec.yaml file:

  assets:
- images/rick_gun.jpg
- images/rick_profile.jpg
- images/walker_takedown/

By writing ‘images/walker_takedown/’ we include all the images in the walker_takedown folder.

You might have noticed that Rick is missing from the human characters. I just couldn’t include him in the game fighting Walkers and Whisperers when he’s gone AWOL developing Flutter apps. Sorry.

All the other human characters are alive and well (at the time of writing) fighting Alpha and her pack.

So let’s get down to it.

Expanded widget

By using the Expanded class, we can insert the images in a row and make them take all the available horizontal space equally divided among them. If we don’t want the space to be equally divided we can use the flex factor.

So we can pitch Negan vs. Alpha like this:

Using the Expanded class inside a Row

and when we rotate the phone, it also looks good as the images maintain their original size instead of being stretched out.

Expanded class in a Row in landscape mode

We can further improve the look by using a Center widget to center the Row in the middle of the phone screen. A quick way of doing this is opening Flutter Outline from the right sidebar, clicking on Row and then on the first button in the toolbar that says Wrap with Center.

There are many actions, called Intention Actions that we can do this way from the Flutter Outline, and there are even more actions if we click on a widget in code and then click on the lightbulb icon that shows up on the side.

For example we can click on Image, then the lightbulb, then select Wrap with Padding to add some padding to the images. This is useful if we have a smaller screen and we want to add some space between the images.

We can use for example: EdgeInsets.fromLTRB(16, 0, 8, 0) for the left image and EdgeInsets.fromLTRB(8, 0, 16, 0) for the right image to achieve a symmetric padding effect.

Expanded widgets with Padding inside a Row inside Center

Buttons!

Finally, after two episodes, we can interact with our apps!

There are multiple buttons that we can choose from the Widget Catalog, but in this case we’re going to choose a FlatButton.

It’s easy to wrap our Image inside the FlatButton. We just click on Image, then on the lightbulb icon, select Wrap with widget…, type FlatButton and hit enter.

You will immediately notice that the images became smaller. This is because FlatButton comes with some default padding. So we can remove the padding that we added earlier. We select the Padding widget and using our lightbulb friend again (or by typing option + enter on Mac), we select Remove this widget and the Padding widget is magically removed!

To achieve the symmetric padding we had before, we simply need to override the default padding of FlatButton by specifying the padding property and adding the EdgeInsets like we did above.

Now you should get two yellow warnings on the right side of the code and if you hover over them or go to Dart Analysis on the bottom bar you should see: ‘The parameter ‘onPressed’ is required’. This is because onPressed is a required property of FlatButton.

The onPressed property is the callback that is called when the button is tapped. It is of type VoidCallback which is a signature of callbacks that have no arguments and return no data.

We define the onPressed property like:

onPressed: () {
print('Human got pressed.');
},

for the left button and with ‘Walker got pressed.’ for the right button. And don’t forget the semicolon after each statement!

Now we should see the equivalent message printed in the Console whenever we tap either of the buttons.

Dart Functions

The function fundamentals in Dart are the same as with any programming language.

Creating a function:

void doSomething() { //instructions }

Calling a function:

doSomething();

This is a named function.

We can also have anonymous functions, like the onPressed callback we had above.

() { //instructions for an anonymous function }

The parentheses are for inputs to the function and we can have a return statement for outputs, but more on that later.

Adding some Variables

In order to be able to change the images, we need some variables that will hold the state of the current images.

Lets add:

var humanImageIndex = 2;
var walkerImageIndex = 2;

right under the build method (so they get included in the hot reload).

And add the variables into the asset name string using the $ sign:

Image.asset('images/walker_takedown/human_$humanImageIndex.png')Image.asset('images/walker_takedown/walker_$walkerImageIndex.png')

This is called String Interpolation.

Now, depending on the number that the variables hold, we will get a different image, since we named our images: human_1.png, human_2.png and walker_1.png, walker_2.png and so on…)

Reload and you get Michonne vs. Beta!

That would be an epic fight!

Dart Variables & Data Types

Like we’ve seen above, we can create a variable like:

var humanImageIndex = 2;

When we do that, the compiler infers that the data type is int (for integer).

Some common primitive data types in Dart are:

  • String — ex. ‘hello’
  • int — ex. 123
  • double — ex. 10.2
  • bool — ex. true

However, if we try to assign a different data type to that variable later on, we will get an error!

This is because Dart is a statically typed language.

This is different from dynamically typed languages where we can assign a different data type to the same variable.

Dynamically typed languages are more flexible, while statically typed languages are safer, saving you from accidental errors like assigning the wrong type without realising it.

But Dart, being a cool modern language, chooses the best of both worlds and allows you to use dynamic variables as well!

You just need to declare a variable without assigning anything to it, like:

var myDynamicVariable;

Now this variable will be of type dynamic, and you can assign any data type to it, any time.

You can also explicitly use the keyword dynamic:

dynamic myDynamicVariable;

On the other hand, if you need to declare a variable without assigning something to it (ex. because you don’t know its value yet), but you also need to maintain type safety, you can declare it using its type, like:

int myInt;
bool myBool;
String myString;

These variable won’t be dynamic and you can only assign one data type to them.

To be on the safe side, it’s better to avoid using var and dynamic and just explicitly declare each variable’s type.

Having said that, let’s change the variables we had before to:

int humanImageIndex = 1;
int walkerImageIndex = 1;

Stateful vs. Stateless Widgets

Since we included the above statements inside the build method, this means that the variables will be created every time we hot reload. Instead, we just want to update them, so let’s move these declarations above the @override statement of the build method and inside build just include the update statements:

humanImageIndex = 2;
walkerImageIndex = 2;

But now we will get a warning saying: ‘info: This class (or a class which this class inherits from) is marked as ‘@immutable’, but one or more of its instance fields are not final’

This is because we are inside a StatelessWidget, which is not supposed to hold any state i.e. it is always the same.

Instead we need to switch to a StatefulWidget that will hold the state of our game i.e. which image is displayed, what the score is etc.

We can go above the StatelessWidget, type stful and hit enter to get the boilerplate code for the StatefulWidget.

Now, we can type the name of the widget (we’ll name it WalkerTakedown — same as before since we’re replacing the StatelessWidget) and it autofills all the required places.

So, we get:

class WalkerTakedown extends StatefulWidget

and

class _WalkerTakedownState extends State<WalkerTakedown>

The State widget also has the build method that we had in the StatelessWidget, so we can simply copy and paste all the code that we had in our StatelessWidget to the _WalkerTakedownState widget and delete the StatelessWidget.

The warning that we had before should disappear now.

Now let’s actually move the update statements from the beginning of the build method to the each respective onPressed function, replacing the print statements and hot restart the app to reset the state.

Since we set the initial values of the variables to 1 and then we update them to 2, what we’re expecting now is for the app to start with Negan vs. Alpha and when we tap the images, they will change to Michonne vs. Beta. But when we tap the images, we see no change.

This is because we need to wrap our update statements inside the setState method. This looks like:

onPressed: () {
setState(() {
humanImageIndex = 2;
});
},

So whenever we tap one of the images we trigger the setState method that will set the new value of the variable and trigger the build method to update the interface with the new state.

If we don’t wrap them inside the setState method, the variables change, but the build method is not triggered to update the interface.

Getting the next random character

To get the next random character to appear, we need a random number generator.

The dart:math library comes to the rescue!

We first need to import it:

import 'dart:math';

and then we can the nextInt method of the Random class which generates a non-negative random integer uniformly distributed in the range from 0, inclusive, to max, exclusive.

Since we named our assets from 1 to 10, we need:

humanImageIndex = Random().nextInt(10) + 1;

and the equivalent for walkerImageIndex.

nextInt(10) will generate numbers from 0 to 9, so we just add 1 to get numbers between 1 and 10.

If we run the app now we can flip through all our humans and walkers randomly!

Going the extra mile

This is as far as the AppBrewery lesson goes, but this would make for a boring game, so I’m going to go a little bit further to make it a bit more interesting :)

It will get a bit more complex from here but it’s worth it!

Showing character names and strengths

I want to show each character’s name above his image, and his strength just below it.

To store all the character names and strengths I’m going to use Dart’s List, which is basically an array.

Quick List cheatsheet:

List<String> myList = [
'Angela',
'James',
'Katie',
];
myList[2]; // output: KatiemyList.indexOf('James'); // output: 1myList.add('Ben'); // inserts 'Ben' at the end of the listmyList.insert(2, 'Jack'); // inserts 'Jack' at index 2 (after James and before Katie)

and more in the documentation and this guide.

So here are our characters and their strengths.

List humanStrengths = [8, 9, 10, 6, 7, 4, 5, 5, 4, 3];
List walkerStrengths = [10, 9, 4, 6, 7, 5, 5, 3, 8, 4];
List humanNames = [
'NEGAN',
'MICHONNE',
'DARYL',
'CAROL',
'ROSITA',
'EUGENE',
'AARON',
'MAGNA',
'LUKE',
'JUDITH'
];
List walkerNames = [
'ALPHA',
'BETA',
'WALKER #3',
'WALKER #4',
'WALKER #5',
'WALKER #6',
'WALKER #7',
'WALKER #8',
'WALKER #9',
'WALKER #10'
];

The strengths are equally divided among humans and walkers to give each team an equal chance at survival.

(EDIT: When you create a List like we did above, it is of type List<dynamic>, which means that it can hold any data type. If we want to make our lists safer by restricting them to a single data type, we can explicitly declare the type like: List<String>)

Next, I’m going to wrap each FlatButton in a Column and set the mainAxisAlignment property of the Column to MainAxisAlignment.center. This will center our rows within the column, so we can also remove the main Center widget that we had before.

Then, I will add a Text child above and one below the FlatButton.

The one above will show the name so I will set it to:

humanNames[humanImageIndex - 1]

and the one below will show the strength so I will set it to:

'STRENGTH: $humanStrength'

and the equivalent for walkers.

I set the TextStyle to the ‘Writing You A Letter’ font that I used in Episode 2. For more details on the TextStyle used for each Text widget check the final code that I will provide at the end.

I also updated the top and bottom padding of the FlatButtons to give some spacing between the image and the text.

Finally, I declared two more variables:

int humanStrength = 8;
int walkerStrength = 10;

to hold the current strengths, and during setState() I also do:

humanStrength = humanStrengths[humanImageIndex - 1];

and the equivalent for the walkers, to update the strengths after getting the random number.

Oh and I added a new Column between the two Expanded widgets just to hold this little ‘VS.’ Text.

So now you should have this result:

Showing names and strengths

If you come across a bug with the custom font, it seems that Flutter doesn’t like it when fonts have spaces in their names. The fix for this is simple. Right-click on the font file and choose Refactor and then Rename… This also finds other occurrences of the file name and renames them automatically for you. In this case, the pubspec.yaml file.

Here, I refactored the font name from ‘Writing You A Letter.ttf’ to ‘Writing-You-A-Letter.ttf’. You don’t need to rename the font name used in the code, so I just left it as ‘’Writing You A Letter’.

Making the first character appear randomly

So far we just start with Negan vs. Alpha. This is too predictable. Let’s make the first character appear randomly.

Since I’m going to be using the random image index code a few times, let’s pack it up inside a function.

int randomImageIndex() {
return Random().nextInt(10) + 1;
}

This function is of type int, which means it needs to return an integer, in this case a random number between 1 and 10 inclusive.

We can also make two more helper functions to update the strength.

int getHumanStrength() {
return humanStrengths[humanImageIndex - 1];
}
int getWalkerStrength() {
return walkerStrengths[walkerImageIndex - 1];
}

Now, inside setState(), we have:

humanImageIndex = randomHumanImageIndex();
humanStrength = getHumanStrength();

and the equivalent for walkers.

The reason we did this is because we’re going to declare our variables without an initial value like:

int humanImageIndex;
int walkerImageIndex;
int humanStrength;
int walkerStrength;

and then we’re going to use the initState() method of the StatefulWidget lifecycle to initialise them. This method is called only once, when the widget is created and we also need to call super.initState() inside it to call its parent initialiser.

@override
void initState() {
super.initState();
newGame();
}

Inside initState() I call newGame(), a new function that I made that I will call when the app is first opened and whenever we start a new game (more on this later).

void newGame() {
humanImageIndex = randomImageIndex();
walkerImageIndex = randomImageIndex();
humanStrength = getHumanStrength();
walkerStrength = getWalkerStrength();
}

Now we can initialise our variables using the helper functions that we created before.

Now if we restart the app (using hot restart to reset the state), we should get two random characters, like Eugene and this hot walker.

Getting the first characters randomly

Keeping score

Now that we get all our characters randomly and we know their strengths, it’s time to keep some score!

Let’s add some more variables:

int round;
int humansRound;
int walkersRound;
int humansScore;
int walkersScore;

and set the to 0 in newGame().

round = 0;
humansRound = 0;
walkersRound = 0;
humansScore = 0;
walkersScore = 0;

To prevent players from flipping their character twice we’ll add and if statement inside setState():

if (humansRound <= walkersRound) {
humansRound += 1;
humanImageIndex = randomHumanImageIndex();
humanStrength = getHumanStrength();
updateScore();
}

and for walkers:

if (walkersRound <= humansRound) {
walkersRound += 1;
walkerImageIndex = randomWalkerImageIndex();
walkerStrength = getWalkerStrength();
updateScore();
}

so unless you’re a round behind or in the same round, you can’t go to the next character.

We’ll also add an updateScore() function that we will call to update the score. Also add it to the end of newGame() to get the initial score when the game starts in Round 1.

void updateScore() {
if (humansRound == walkersRound) {
round += 1;
if (humanStrength > walkerStrength) {
humansScore += 1;
} else if (walkerStrength > humanStrength) {
walkersScore += 1;
}
}
}

So whenever we tap a button during a legit move and the humansRound and walkersRound are updated, we check whether both sides are in the same round. If they are we compare the strengths and update the equivalent score. If the strengths are the same, no one gets any points.

Now that we’re keeping tabs on the score, let’s show it in the UI.

We’ll wrap our Row in a Column and add a Row above it and another Row below it.

Set both Rows’ mainAxisAlignment property to MainAxisAlignment.center.

For the first row, we’re gonna ember a Text with value:

'ROUND: $round / 10'

to show the current round.

For the third row, we’re gonna show the score using:

'SCORE: $humansScore - $walkersScore'

Finally, set the mainAxisAlignment of the Column to MainAxisAlignment.spaceEvenly so the Rows are evenly spaced out.

Now we should be able to see the rounds and the score, evenly spaced out.

Keeping score

Nice!

No repeating characters

To make the game more ‘fair’, each player can only use a character once.

To add this functionality, we need to keep tabs on which characters have been used.

A good collection class for this is the Set, from the dart core library. From the documentation:

A collection of objects in which each object can occur only once. That is, for each object of the element type, the object is either considered to be in the set, or to not be in the set.

So let’s declare our Sets.

Set usedHumanIndexes;
Set usedWalkerIndexes;

and initialise them as empty Sets in newGame().

usedHumanIndexes = Set();
usedWalkerIndexes = Set();

Now we need to modify our random functions so that they only give use characters that have not been used.

For this we create one for humans:

int randomHumanImageIndex() {
int randomImageIndex = Random().nextInt(10) + 1;
while (usedHumanIndexes.contains(randomImageIndex)) {
randomImageIndex = Random().nextInt(10) + 1;
}
usedHumanIndexes.add(randomImageIndex);
return randomImageIndex;
}

and one for walkers:

int randomWalkerImageIndex() {
int randomImageIndex = Random().nextInt(10) + 1;
while (usedWalkerIndexes.contains(randomImageIndex)) {
randomImageIndex = Random().nextInt(10) + 1;
}
usedWalkerIndexes.add(randomImageIndex);
return randomImageIndex;
}

Basically we get a random number and then we do a while loop, which checks whether the random number that we got is contained in the used character Set and if it is, it keeps getting more random numbers until we get something that has not been used.

Then, we add it to the set of used numbers and return it.

Now we should only get characters appearing once.

But what happens after all characters are exhausted?

That would send us into an infinite loop where we keep generating random numbers between 1 and 10, because they are all contained inside the Set of used numbers.

To prevent this from happening, we need to stop the game after Round 10.

The end game

The functionality that I want to have here, is that when Round 10 is over, the winner is shown along with a message, and then the user can replay the game by tapping on one of the character’s images.

Each game has a winner, so let’s add one final variable to our game:

String winner;

and initialise it in newGame():

winner = '';

Update the updateScore() function:

void updateScore() {
if (humansRound == walkersRound) {
// ...more code here... if (round == 10) {
if (humansScore > walkersScore) {
winner = 'HUMANS';
} else if (walkersScore > humansScore) {
winner = 'WALKERS';
} else {
winner = 'NO ONE';
}
}
}
}

When we reach Round 10, check who has the highest score and set the winner.

Modify the setState() methods:

if (round == 10) {
newGame();
} else if (humansRound <= walkersRound) {
humansRound += 1;
humanImageIndex = randomHumanImageIndex();
humanStrength = getHumanStrength();
updateScore();
}

and equivalently for walkers.

So here, if we tap on the character icon in Round 10, the game will restart. If it’s before Round 10, it will proceed as usual.

Finally, to show the message at the end, modify the final Row.

Flexible(
child: Text(
(round < 10)
? 'SCORE: $humansScore - $walkersScore'
: 'SCORE: $humansScore - $walkersScore\n\n$winner WON! TAP ON ANY CHARACTER TO PLAY AGAIN.',
textAlign: TextAlign.center,
style: TextStyle(...),
),
),

Wrap the Text widget inside a Flexible widget. That will wrap the text if it doesn’t fit the width of the screen. Also, set the textAlign property to TextAlign.center to keep the text centered. This is in addition to mainAxisAlignment: MainAxisAlignment.center of the Row to keep the text in the center of the Row.

For the text String we have this ternary operator:

bool ? A : B

which basically means: if bool is true, do A, if it’s false, do B.

So here, if round < 10, it will show the score like before. When we reach round 10, it will show the winner along with the message to prompt the winner to tap to play again. The \n is just the new line character, which is like hitting enter in a text editor.

Oops, the walkers won!

This is how the UI appears in Round 10.

If we restart the game by tapping on one of the character’s images, the message will disappear.

Afterword

Just when I said to myself that I’ll keep it simple and short this time… I ended up breaking my previous record for longest article 😂

Hey, at least now, whenever I have a decision to make, instead of flipping a coin or playing rock/paper/scissors, I can play Walker Takedown!

And you can do the same if you decide to build this app and run it on your phone!

Final code

You are most certainly going to need the final code if you are to make any sense of what we built here. It’s a good 275 lines of codes so hang in there!

The images assets are also here.

If you made it this far, thanks for reading!!!

Next episodes are out!

--

--