Make Your Own Arduboy Game: Part 8 - Starting DinoSmasher


(Holmes) #1

Before You Start

Hey! Before you start! Do you mind following me on Twitter? I really spent a lot of time working on this tutorial series, but a follow on Twitter and/or a shout-out really motivates me to continue making more.
http://www.twitter.com/crait

And when you get done, please leave a comment letting me know if you had fun!

If you haven’t done any of the previous tutorials, please be sure to do that before this one! Part 1, Part 2, Part 3, Part 4, Part 5, Part 6, and Part 7

This Tutorial

This tutorial will be the first part of making a more complicated game than Pong. You’re going to make a game to challenge a player to find and destroy as many buildings as they can as a dinosaur in a given time limit, getting points as they do so. First, we need to learn about arrays and a few other things.

This is a HUGE tutorial, so I’m going to be breaking this down into smaller steps. Here’s what we’re going to cover in Part 8.

  1. Upgrading our library to Arduboy2
  2. Creating our sketch
  3. Learning about #define
  4. Structuring our code
  5. A moment about button presses
  6. Creating a 1D array
  7. Creating a 2D array

0. Upgrading our library to Arduboy2

So, I wrote the previous tutorials quite some time ago. Because of this, I used the original Arduboy library. I’m going to show you how to install the Arduboy2 library. If you are sure you have done this, you can skip this step.

Remember what I said back in tutorial 2… A library is a collection of instructions that someone else wrote that we can use that makes writing games easier.

To use the original library, we were using the following in our code:

#include <Arduboy.h>
Arduboy arduboy;

However, we want to use the updated Arduboy2 library, which was created by @MLXXXp . His Arduboy2 library includes more functionality that we’ll be using that the original does not include. Instead of the above, we’ll need to use the following:

#include <Arduboy2.h>
Arduboy2 arduboy;

We need to also download the library to your computer through the Arduino IDE.

In the Arduino IDE, go to Sketch > Include Library > Manage Libraries .

Simply search for Arduboy2 and select the latest version, then click Install .

1. Creating our sketch

Okay, let’s start working on the game! In the Arduino IDE, go to File > New, then save the file to your computer. In the IDE, let’s use all this.

//DinoSmasher

#include <Arduboy2.h>
Arduboy2 arduboy;

void setup() {
	arduboy.begin();
	arduboy.setFrameRate(45);
	arduboy.display();

	arduboy.initRandomSeed();
	
	arduboy.clear();
}

void loop() {
	if(!(arduboy.nextFrame())) {
		return;
	}

	arduboy.pollButtons();

	arduboy.clear();

	arduboy.setCursor(0, 0);
	arduboy.print(F("HELLO!"));

	arduboy.display();
}

You should be able to understand most of this, but if there are some new things that you don’t recognize, don’t worry! Just save the file and transfer it to your Arduboy. It should simply say “HELLO”.

2. Learning about #define

There are a few reasons to use functions in our code. One reason is organize your code, but another is to prevent you from having to repeat a lot of the same code over and over again. There’s another cool feature that helps with both of those things that uses the #define directive.

Pretend like you have the following code in your game to move your character around:

void move_left() {
	player_x -= 5;
}
void move_right() {
	player_x += 5;
}
void move_up() {
	player_y -= 5;
}
void move_down() {
	player_y += 5;
}

If you used this code and found that your character moves too fast and wanted to change its speed, you would have to change it in 4 places. Instead, we could use the #define directive to create a keyword called SPEED, set it to 5 and then use that.

#define SPEED	5

void move_left() {
	player_x -= SPEED;
}
void move_right() {
	player_x += SPEED;
}
void move_up() {
	player_y -= SPEED;
}
void move_down() {
	player_y += SPEED;
}

The way this works is that whenever you compile your code, the compiler will replace SPEED with 5 before it compiles.

Let’s use this knowledge to change our code!

3. Structuring our code

Our Pong code started to get a little crazy, so let’s make sure this game’s code is a bit easier to read. We have already have the standard loop() function that’s called many times. In our Pong game, that’s where we had a switch statement that handled which part of the game was happening.

We’re going to have to use a switch statement, as well, but let’s put it into its own function. Let’s make a gameplay() function a reference it near the end of our loop() function instead of printing “HELLO”. Remember, we want use arduboy.display() last, so let’s make sure it comes right before that.

When someone turns on the Arduboy, we want it to start at the title screen, then go to the gameplay screen, and then if they lose, we want them to go to the game over screen. However, if they beat the saved high score, we want to take them to the high score screen. From either of those, we want to go back to the title screen.

sequence

We need to create an int variable called gamestate to keep track of which state we’re in. The switch case should look something like this:

	switch(gamestate) {
		case 0:
			//Title screen stuff
			break;

		case 1:
			//Gameplay stuff
			break;

		case 2:
			//Game over screen
			break;

		case 3:
			//High score screen
			break;
	}

Putting that into our gameloop() function, our code now looks like this:

//DinoSmasher

#include <Arduboy2.h>
Arduboy2 arduboy;

int gamestate = 0;

void gameloop() {
	switch(gamestate) {
		case 0:
			//Title screen stuff
			break;

		case 1:
			//Gameplay stuff
			break;

		case 2:
			//Game over screen
			break;

		case 3:
			//High score screen
			break;
	}
}

void setup() {
	arduboy.begin();
	arduboy.setFrameRate(45);
	arduboy.display();

	arduboy.initRandomSeed();
	
	arduboy.clear();
}

void loop() {
	if(!(arduboy.nextFrame())) {
		return;
	}

	arduboy.pollButtons();

	arduboy.clear();

	gameloop();

	arduboy.display();
}

COOL!

Instead of having 0, 1, 2, and 3 represent the different states in our game, let’s use #define to make the code easier to digest.

#define GAME_TITLE	0
#define GAME_PLAY	1
#define GAME_OVER	2
#define GAME_HIGH	3

Now, any time we want to check the value of gamestate, we can simply compare gamestate to one of these defintions! We can even use it when defining/initializing varaibles! Take a look at how I’ve done it below:

//DinoSmasher

#include <Arduboy2.h>
Arduboy2 arduboy;

#define GAME_TITLE	0
#define GAME_PLAY	1
#define GAME_OVER	2
#define GAME_HIGH	3
int gamestate = GAME_TITLE;

void gameloop() {
	switch(gamestate) {
		case GAME_TITLE:
			//Title screen stuff
			break;

		case GAME_PLAY:
			//Gameplay stuff
			break;

		case GAME_OVER:
			//Game over screen
			break;

		case GAME_HIGH:
			//High score screen
			break;
	}
}

void setup() {
	arduboy.begin();
	arduboy.setFrameRate(45);
	arduboy.display();

	arduboy.initRandomSeed();
	
	arduboy.clear();
}

void loop() {
	if(!(arduboy.nextFrame())) {
		return;
	}

	arduboy.pollButtons();

	arduboy.clear();

	gameloop();

	arduboy.display();
}

If we compile this, the code would run, but nothing would be displayed on the Arduboy screen. We should add some text to the screen to make sure this compiles and can be put onto the screen! However, I want to emphasize that we are going to make sure this program is easy to read, so let’s make sure we add it in sensibly!

Instead of putting a bunch of code into our switch statements, let’s simply add a single function to each case. These functions will be titlescreen(), gameplay(), gameoverscreen(), and highscorescreen() . Any time we want to add code into one of those states, we’ll simply put the code into those functions! This means we’ll add the text to those functions. Here’s the code for that:

//DinoSmasher

#include <Arduboy2.h>
Arduboy2 arduboy;

#define GAME_TITLE	0
#define GAME_PLAY	1
#define GAME_OVER	2
#define GAME_HIGH	3
int gamestate = GAME_TITLE;

void titlescreen() {
	arduboy.setCursor(0, 0);
	arduboy.print("Title Screen\n");
}

void gameplay() {
	arduboy.setCursor(0, 0);
	arduboy.print("Gameplay\n");
}

void gameoverscreen() {
	arduboy.setCursor(0, 0);
	arduboy.print("Game Over Screen\n");
}

void highscorescreen() {
	arduboy.setCursor(0, 0);
	arduboy.print("High Score Screen\n");
}

void gameloop() {
	switch(gamestate) {
		case GAME_TITLE:
			titlescreen();
			break;

		case GAME_PLAY:
			gameplay();
			break;

		case GAME_OVER:
			gameoverscreen();
			break;

		case GAME_HIGH:
			highscorescreen();
			break;
	}
}

void setup() {
	arduboy.begin();
	arduboy.setFrameRate(45);
	arduboy.display();

	arduboy.initRandomSeed();
	
	arduboy.clear();
}

void loop() {
	if(!(arduboy.nextFrame())) {
		return;
	}

	arduboy.pollButtons();

	arduboy.clear();

	gameloop();

	arduboy.display();
}

ALRIGHTY! Compile this and put it onto your Arduboy to see what happens!

4. A moment about button presses

Up until now, you’ve been learning about button presses with the first Arduboy library. The Arduboy2 library offers us a more simple way to handle button presses. We are able to check buttons 4 different ways:

  1. The button is currently being held down using arduboy.pressed() like before
  2. The button was just pressed in using arduboy.justPressed()
  3. The button was just released using arduboy.justReleased()
  4. A button is not being pressed using aduboy.notPressed()

The library automatically handles what we had been programming in all along! In order to check if a button was pressed this frame, but don’t repeat, we can use arduboy.justPressed(). Changing the gamestate with the A button with something like the following would be easy.

if(arduboy.justPressed(A_BUTTON)) {
	gamestate = GAME_PLAY;
}

Try adding code like this to titlescreen(), gameplay(), gameoverscreen(), and highscorescreen() to cycle through all of the states. If you want to double-check your work, see my code below:

//DinoSmasher

#include <Arduboy2.h>
Arduboy2 arduboy;

#define GAME_TITLE	0
#define GAME_PLAY	1
#define GAME_OVER	2
#define GAME_HIGH	3
int gamestate = GAME_TITLE;

void titlescreen() {
	arduboy.setCursor(0, 0);
	arduboy.print("Title Screen\n");
	if(arduboy.justPressed(A_BUTTON)) {
		gamestate = GAME_PLAY;
	}
}

void gameplay() {
	arduboy.setCursor(0, 0);
	arduboy.print("Gameplay\n");
	if(arduboy.justPressed(A_BUTTON)) {
		gamestate = GAME_OVER;
	}
}

void gameoverscreen() {
	arduboy.setCursor(0, 0);
	arduboy.print("Game Over Screen\n");
	if(arduboy.justPressed(A_BUTTON)) {
		gamestate = GAME_HIGH;
	}
}

void highscorescreen() {
	arduboy.setCursor(0, 0);
	arduboy.print("High Score Screen\n");
	if(arduboy.justPressed(A_BUTTON)) {
		gamestate = GAME_TITLE;
	}
}

void gameloop() {
	switch(gamestate) {
		case GAME_TITLE:
			titlescreen();
			break;

		case GAME_PLAY:
			gameplay();
			break;

		case GAME_OVER:
			gameoverscreen();
			break;

		case GAME_HIGH:
			highscorescreen();
			break;
	}
}



void setup() {
	arduboy.begin();
	arduboy.setFrameRate(45);
	arduboy.display();

	arduboy.initRandomSeed();
	
	arduboy.clear();
}

void loop() {
	if(!(arduboy.nextFrame())) {
		return;
	}

	arduboy.pollButtons();

	arduboy.clear();

	gameloop();

	arduboy.display();
}

I suggest running this on your Arduboy and seeing how the states do not flicker too fast.

(OH, YEAH! Did you notice that A_BUTTON is a defined keyword?! It’s just a number!)

5. Creating a 1D array

Remember back in Part 6 when I told you about arrays? I told you that they were just groups of variables. Let’s talk a little more about them. They can be super helpful!

Let’s pretend that you have 5 cars in your game and you wanted to store how fast each one can drive. You could do something like this.

int car_one = 4;
int car_two = 5;
int car_three = 7;
int car_four = 8;
int car_five = 3;

But what if you wanted to add 5 more cars?? That’s a lot of typing!! Well, there’s a quicker way to write this with arrays!

int cars[5] = { 4, 5, 7, 8, 3 };

Using the above code, you would be creating an int array with all the car data in it. Let’s break this down.

int: This is the type of data you want a list of.
cars: This is the name of the array. Since you want to store many cars into it, you call it ‘cars.’
[5]: This is how many int's you want in your array.
{ 4, 5, 7, 8, 3 }: This is the data that you’re storing. Notice that there’s 1 number for each car.

To increase the number of cars, you increase the size of the array and simply add another value to the end of the list, which is a lot easier!

int cars[10] = { 4, 5, 7, 8, 3, 6, 3, 1, 7, 9 };

Now, what if we wanted to figure out the speed of one of the cars? Earlier, we would use car_one to access the number 4.

track_distance = car_four;

When using an array, we access using brackets like this:

track_distance = cars[4];

Does the above make sense to you? GOOD! Because there’s one important thing that I need to tell you about arrays.

The index is the number that you use to access the data inside of the array at a given location. In the above code, 4 would be the index since you’re seemingly trying to get the fourth car’s speed.

However, when counting indexes, you don’t start with 1, 2, 3, … etc. Counting indexes actually starts at 0! What does this mean?? This means that the first car’s speed is actually stored at cars[0] . The second’s car’s speed is stored at cars[1]. The third is at cars[2], etc, etc.

So, accessing the 4th car’s speed with cars[4] is actually wrong. We need to use cars[3] !!

track_distance = car_four; //This is lame
...
track_distance = cars[3]; //This is cool

This isn’t hard to remember and I’ll try to make sure I remind you! Besides, there are benefits to this!!

OKAY! Enough talking! Let’s code an array!

Let’s make an array called world. This will eventually hold our game’s map! For now, we’ll just have it hold random numbers. Let’s make it have length of 20.

int world[20] = { 9, 5, 7, 2, 8, 3, 1, 7, 9, 3, 2, 1, 6, 4, 7, 8, 4, 3, 1, 4 };

We can also display some values from it in the titlescreen() function to test it! Add this!

	arduboy.print(world[0]);
	arduboy.print(world[1]);
	arduboy.print(world[2]);
	arduboy.print(world[3]);

Here’s my full code:

//DinoSmasher

#include <Arduboy2.h>
Arduboy2 arduboy;

#define GAME_TITLE	0
#define GAME_PLAY	1
#define GAME_OVER	2
#define GAME_HIGH	3
int gamestate = GAME_TITLE;

int world[20] = { 9, 5, 7, 2, 8, 3, 1, 7, 9, 3, 2, 1, 6, 4, 7, 8, 4, 3, 1, 4 };

void titlescreen() {
	arduboy.setCursor(0, 0);
	arduboy.print("Title Screen\n");

	arduboy.print(world[0]);
	arduboy.print(world[1]);
	arduboy.print(world[2]);
	arduboy.print(world[3]);

	if(arduboy.justPressed(A_BUTTON)) {
		gamestate = GAME_PLAY;
	}
}

void gameplay() {
	arduboy.setCursor(0, 0);
	arduboy.print("Gameplay\n");
	if(arduboy.justPressed(A_BUTTON)) {
		gamestate = GAME_OVER;
	}
}

void gameoverscreen() {
	arduboy.setCursor(0, 0);
	arduboy.print("Game Over Screen\n");
	if(arduboy.justPressed(A_BUTTON)) {
		gamestate = GAME_HIGH;
	}
}

void highscorescreen() {
	arduboy.setCursor(0, 0);
	arduboy.print("High Score Screen\n");
	if(arduboy.justPressed(A_BUTTON)) {
		gamestate = GAME_TITLE;
	}
}

void gameloop() {
	switch(gamestate) {
		case GAME_TITLE:
			titlescreen();
			break;

		case GAME_PLAY:
			gameplay();
			break;

		case GAME_OVER:
			gameoverscreen();
			break;

		case GAME_HIGH:
			highscorescreen();
			break;
	}
}



void setup() {
	arduboy.begin();
	arduboy.setFrameRate(45);
	arduboy.display();

	arduboy.initRandomSeed();
	
	arduboy.clear();
}

void loop() {
	if(!(arduboy.nextFrame())) {
		return;
	}

	arduboy.pollButtons();

	arduboy.clear();

	gameloop();

	arduboy.display();
}

Isn’t it annoying that you still have to specificy that you want to print each individual value? What if you wanted to print all the values of the world array?? Would you copy/paste the code 20 times? NO! We have for loops that can help us!

Remember, if we want to use a for loop to go through something 20 times, we’d do something like this:

for(int i = 0; i < 20; i++) {
	arduboy.print(i);
}

If we use the above code, the numbers 0~19 would be printed out to the screen. Instead, we want to print the 20 values inside of world, starting at index 0.

for(int i = 0; i < 20; i++) {
	arduboy.print( world[i] );
}

Put this code into your title screen function and run it! Full code, here:

//DinoSmasher

#include <Arduboy2.h>
Arduboy2 arduboy;

#define GAME_TITLE	0
#define GAME_PLAY	1
#define GAME_OVER	2
#define GAME_HIGH	3
int gamestate = GAME_TITLE;

int world[20] = { 9, 5, 7, 2, 8, 3, 1, 7, 9, 3, 2, 1, 6, 4, 7, 8, 4, 3, 1, 4 };

void titlescreen() {
	arduboy.setCursor(0, 0);
	arduboy.print("Title Screen\n");

	for(int i = 0; i < 20; i++) {
		arduboy.print(world[i]);
	}

	if(arduboy.justPressed(A_BUTTON)) {
		gamestate = GAME_PLAY;
	}
}

void gameplay() {
	arduboy.setCursor(0, 0);
	arduboy.print("Gameplay\n");
	if(arduboy.justPressed(A_BUTTON)) {
		gamestate = GAME_OVER;
	}
}

void gameoverscreen() {
	arduboy.setCursor(0, 0);
	arduboy.print("Game Over Screen\n");
	if(arduboy.justPressed(A_BUTTON)) {
		gamestate = GAME_HIGH;
	}
}

void highscorescreen() {
	arduboy.setCursor(0, 0);
	arduboy.print("High Score Screen\n");
	if(arduboy.justPressed(A_BUTTON)) {
		gamestate = GAME_TITLE;
	}
}

void gameloop() {
	switch(gamestate) {
		case GAME_TITLE:
			titlescreen();
			break;

		case GAME_PLAY:
			gameplay();
			break;

		case GAME_OVER:
			gameoverscreen();
			break;

		case GAME_HIGH:
			highscorescreen();
			break;
	}
}



void setup() {
	arduboy.begin();
	arduboy.setFrameRate(45);
	arduboy.display();

	arduboy.initRandomSeed();
	
	arduboy.clear();
}

void loop() {
	if(!(arduboy.nextFrame())) {
		return;
	}

	arduboy.pollButtons();

	arduboy.clear();

	gameloop();

	arduboy.display();
}

This is what you should get after compiling!

Alright, now that you have a good understanding of what arrays are, let’s talk about 2D arrays!

6. Creating a 2D array

Arrays can store all sorts of data; You can have an array of int's… an array of boolean's… These are all called 1D arrays because they store data in 1 dimension. It’s basically just a single list of variables all grouped together.

But, you can take arrays a step further! You can even have an array of arrays!! WHAT?! These are called 2D arrays because they store data in 2 dimensions… This means that instead of just a list of information, you can have a whole grid of it!

Here’s a neat way to visualize the difference between a 1D array and a 2D array.

We can make a 2D array and store numbers into it and then print the numbers out like we did with a 1D array! Lemme show you how by starting by modifying our world array.

int world[4][20] = {
	{ 9, 5, 7, 2, 8, 3, 1, 7, 9, 3, 2, 1, 6, 4, 7, 8, 4, 3, 1, 4 },
	{ 8, 7, 2, 5, 6, 1, 4, 6, 6, 2, 6, 3, 5, 4, 2, 4, 3, 1, 5, 2 },
	{ 7, 2, 6, 3, 5, 4, 0, 2, 3, 5, 9, 2, 3, 5, 4, 2, 5, 4, 1, 9 },
	{ 6, 4, 5, 2, 6, 4, 5, 1, 2, 5, 4, 3, 2, 4, 6, 5, 4, 2, 4, 5 }
};

Notice that our world array now has 2 lengths given to it. [4] refers to the the amount 1D arrays that will be stored. [20] is how long the 1D arrays will be. When creating this grid, you can think of the first number as the height of the grid and the second as the width.

In order to read one of the int values, we need to give two different indexes. Something like this: world[2][6] The first number is the row, and the second is the column. This would return the value 0 .

Now, how would you loop through all of these numbers to print them out? Well, we could write 4 separate for loops, but that would cause the same problem as before. Like in Part 6, we can have a for loop for the height and a for loop for the width!!

We can print the world by adjusting our for loop to look like this:

	for(int y = 0; y < 4; y++) {
		for(int x = 0; x < 20; x++) {
			arduboy.print(world[y][x]);
		}
		arduboy.print("\n");
	}

Remember from Part 5 that arduboy.print("\n") is supposed to create a new line of text! Compile and test the completed code:

//DinoSmasher

#include <Arduboy2.h>
Arduboy2 arduboy;

#define GAME_TITLE	0
#define GAME_PLAY	1
#define GAME_OVER	2
#define GAME_HIGH	3
int gamestate = GAME_TITLE;

int world[4][20] = {
	{ 9, 5, 7, 2, 8, 3, 1, 7, 9, 3, 2, 1, 6, 4, 7, 8, 4, 3, 1, 4 },
	{ 8, 7, 2, 5, 6, 1, 4, 6, 6, 2, 6, 3, 5, 4, 2, 4, 3, 1, 5, 2 },
	{ 7, 2, 6, 3, 5, 4, 6, 2, 3, 5, 9, 2, 3, 5, 4, 2, 5, 4, 1, 9 },
	{ 6, 4, 5, 2, 6, 4, 5, 1, 2, 5, 4, 3, 2, 4, 6, 5, 4, 2, 4, 5 }
};

void titlescreen() {
	arduboy.setCursor(0, 0);
	arduboy.print("Title Screen\n");

	for(int y = 0; y < 4; y++) {
		for(int x = 0; x < 20; x++) {
			arduboy.print(world[y][x]);
		}
		arduboy.print("\n");
	}

	if(arduboy.justPressed(A_BUTTON)) {
		gamestate = GAME_PLAY;
	}
}

void gameplay() {
	arduboy.setCursor(0, 0);
	arduboy.print("Gameplay\n");
	if(arduboy.justPressed(A_BUTTON)) {
		gamestate = GAME_OVER;
	}
}

void gameoverscreen() {
	arduboy.setCursor(0, 0);
	arduboy.print("Game Over Screen\n");
	if(arduboy.justPressed(A_BUTTON)) {
		gamestate = GAME_HIGH;
	}
}

void highscorescreen() {
	arduboy.setCursor(0, 0);
	arduboy.print("High Score Screen\n");
	if(arduboy.justPressed(A_BUTTON)) {
		gamestate = GAME_TITLE;
	}
}

void gameloop() {
	switch(gamestate) {
		case GAME_TITLE:
			titlescreen();
			break;

		case GAME_PLAY:
			gameplay();
			break;

		case GAME_OVER:
			gameoverscreen();
			break;

		case GAME_HIGH:
			highscorescreen();
			break;
	}
}



void setup() {
	arduboy.begin();
	arduboy.setFrameRate(45);
	arduboy.display();

	arduboy.initRandomSeed();
	
	arduboy.clear();
}

void loop() {
	if(!(arduboy.nextFrame())) {
		return;
	}

	arduboy.pollButtons();

	arduboy.clear();

	gameloop();

	arduboy.display();
}

Cool, huh? Alright, let’s try something very dangerous. Something that you may have even wondered on your own!! Our world array is current 4 tall and 20 wide, but what happens if we try to print out the 5th line of numbers? Or 6th line? Could we do it? What if those values don’t exist? What will be printed out??? Replace the for loops with this and see for yourself!!

	for(int y = 0; y < 6; y++) {
		for(int x = 0; x < 20; x++) {
			arduboy.print(world[y][x]);
		}
		arduboy.print("\n");
	}

On my Arduboy, this is what it looks like!

Where did those exact numbers come from?? Shouldn’t those be 0’s?? Why does this happen? Well, whenever the Arduboy stores variables and things into memory, it puts them all into one big list. If you read outside of the array, you will be reading other variables. If you try to change the value that is outside of the array, you may accidentally change a random variable somewhere else in memory. This can cause all sorts of random stuff to happen that we don’t want right now. We can experiment with this later, but for now, let’s prevent this using our #define directive!

Let’s define the height and width of world and then use those numbers in our definitions and for loops! OH, let’s also change all the values to 0’s or 1’s just to check something.

#define WORLD_WIDTH		20
#define WORLD_HEIGHT	4
int world[WORLD_HEIGHT][WORLD_WIDTH] = {
	{ 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0 },
	{ 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1 },
	{ 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0 },
	{ 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0 }
};

void titlescreen() {
	arduboy.setCursor(0, 0);
	arduboy.print("Title Screen\n");

	for(int y = 0; y < WORLD_HEIGHT; y++) {
		for(int x = 0; x < WORLD_WIDTH; x++) {
			arduboy.print(world[y][x]);
		}
		arduboy.print("\n");
	}

	if(arduboy.justPressed(A_BUTTON)) {
		gamestate = GAME_PLAY;
	}
}

This Let’s move the for loops to their own function called drawworld() and reference it inside of the gameplay() function. Here’s the completed code:

//DinoSmasher

#include <Arduboy2.h>
Arduboy2 arduboy;

#define GAME_TITLE	0
#define GAME_PLAY	1
#define GAME_OVER	2
#define GAME_HIGH	3
int gamestate = GAME_TITLE;

#define WORLD_WIDTH		20
#define WORLD_HEIGHT	4
int world[WORLD_HEIGHT][WORLD_WIDTH] = {
	{ 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0 },
	{ 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1 },
	{ 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0 },
	{ 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0 }
};

void drawworld() {
	for(int y = 0; y < WORLD_HEIGHT; y++) {
		for(int x = 0; x < WORLD_WIDTH; x++) {
			arduboy.print(world[y][x]);
		}
		arduboy.print("\n");
	}
}

void titlescreen() {
	arduboy.setCursor(0, 0);
	arduboy.print("Title Screen\n");
	if(arduboy.justPressed(A_BUTTON)) {
		gamestate = GAME_PLAY;
	}
}

void gameplay() {
	arduboy.setCursor(0, 0);
	arduboy.print("Gameplay\n");

	drawworld();

	if(arduboy.justPressed(A_BUTTON)) {
		gamestate = GAME_OVER;
	}
}

void gameoverscreen() {
	arduboy.setCursor(0, 0);
	arduboy.print("Game Over Screen\n");
	if(arduboy.justPressed(A_BUTTON)) {
		gamestate = GAME_HIGH;
	}
}

void highscorescreen() {
	arduboy.setCursor(0, 0);
	arduboy.print("High Score Screen\n");
	if(arduboy.justPressed(A_BUTTON)) {
		gamestate = GAME_TITLE;
	}
}

void gameloop() {
	switch(gamestate) {
		case GAME_TITLE:
			titlescreen();
			break;

		case GAME_PLAY:
			gameplay();
			break;

		case GAME_OVER:
			gameoverscreen();
			break;

		case GAME_HIGH:
			highscorescreen();
			break;
	}
}



void setup() {
	arduboy.begin();
	arduboy.setFrameRate(45);
	arduboy.display();

	arduboy.initRandomSeed();
	
	arduboy.clear();
}

void loop() {
	if(!(arduboy.nextFrame())) {
		return;
	}

	arduboy.pollButtons();

	arduboy.clear();

	gameloop();

	arduboy.display();
}

Let’s test this code and see what happens! Don’t the numbers in the gameplay screen kinda resemble a map? In fact, a lot of older games do something similar to draw the maps for their games.

image

But, we don’t want that. We want to actually draw a map using the techniques we talked about in Part 6.

Next Tutorial

You’ve learned a lot this tutorial, so let’s wait until the next tutorial to add graphics to make it look cool! You’ll also learn how to move around the map like in Zelda or Pokemon.

Credits

I wrote this tutorial in order to give back to the programming community that taught me to get into it about 10 years ago. If you’d like to follow me on Twitter, please do so at http://www.twitter.com/crait . I’d greatly appreciate that. :smile:


Make Your Own Arduboy Game: Part 9 - Mapping DinoSmasher
Make Your Own Arduboy Game: Part 7 - Make Pong From Scratch!
Make Your Own Arduboy Game: Part 7 - Make Pong From Scratch!
(Pharap) #2

Firstly, some mistakes:

#define isn’t a macro, it’s a preprocessor directive.

There’s an extra space before the underscore for car_five.

When you introduced the cars as named variables you started at car_one instead of car_zero, thus car_four would in fact be cars[3].

The naming here seems inconsistent.
First you’re using all lowercase (rather than Pascal case or camel case), then you’re using lowercase and underscores (sometimes called ‘snake case’).


Secondly some opinion:

Personally I’m against teaching people to use macros for constants and integers for representing states.

constexpr const variables should be preferred over macros because they’re typesafe and provide better debug messages.

enum classes should be preferred for representing states because they are named, type safe, and you don’t have to care about which number is which state if you don’t want to.

And in loops, ++i should be preferred over i++ because the latter is inefficient for certain complex types.


(Holmes) #3

Thanks, Pharap! Went through and either fixed those issues you bring up, or elaborated to clear up any confusion. (The cars[4] thing, for instance, was already explained, but I made sure to bold it.)

As for the enum’s vs the #define directive debacle, there are some reasons why someone would use one instead of the other. Personally, I think it’s best to teach enum’s right before class's are taught, which I won’t be doing for this game. :slight_smile: I think for most cases, enums would make sense, but the next part of the series will use the macros in some interesting ways for beginners that will make sense and look good, too.


(Boti Kis) #4

@crait thanks for your work!

I believe there is a small misunderstanding. @Pharap mean enum classes and not classes.
They are a thing since c++11 and bring typesafety with a slightly different syntax than regular enums.


(Pharap) #6

Such as what exactly?

I can’t think of many situations where I would prefer macros over enumerations if enums could get the job done.

I think that’s over-estimating the complexity of enums and under-estimating the complexity of macros.

Enumerations shouldn’t be treated as something complex.
They are barely more complicated than integers (because secretly they are integers with a bit of syntactic sugar).

On the other hand, the mechanism of macros might seem simpler, but the implications are far more complicated.

And like @Botisaurus points out, enum classes (the technical term being scoped enumerations) are nothing to do with classes. The reason they are called enum classes is because the standards committee is somewhat allergic to adding new keywords.


(Holmes) #7

No, I understand the difference. The C++ book that I used in school taught them leading up to classes and it just makes logical sense to me. enum's are slightly more complex than using #define.

You can overwrite them, they are global, you can use mix different types instead of unsigned int, etc.

But guys, don’t worry. :laughing: This is a tutorial series. I am allowed to tell people to go back and update their code when new concepts are introduced.


(Simon) #8

Its great to see this original series extended. I use both #defines and enum classes in my programmes - and its important that people see both. I am not sure what’s in the future articles in this series, but using #ifdef to turn on and off debugging (conditional compilation) is a trick beginners should learn early.

@crait are you planning on updating the previous articles to use Arduboy2 ? I understand that this one starts with ‘Upgrading our library to Arduboy2’ but it would be a shame to continue to talk of the old library in the earlier articles.


(Holmes) #9

Yeah, I’ll be going back through the older articles when the DinoSmasher section is done to update them. I know a lot of people have already done them, so I wanted to leave a little section in this tutorial to get everyone up to pace. After updating all the older tutorials, I’ll probably wait a few months before removing this section to make everything flow better.


(Simon) #10

Good idea!

You might like to look at what I have covered in my series as I introduce structs, enums and masks. There is obviously no problem with overlap but it might make sense to bridge the gap between the two series rather than duplicating the concepts.


(Simon) #11

Happy cake day @crait


(Holmes) #12

WOOT! I just uploaded PART 9!


#13

Nice!
Although this tutorial is a bit late… (after I learned enum from @Pharap and figured out #define myself, and using a loop for the main function myself too.)
I don’t like your graph of the program state (like you should always be able to view highscore from main menu, but why, I can change that.)
Good job man. :+1:


(Ubi De Feo) #14

@crait , I’m also with @Pharap on ENUM vs single defines when it comes to things like states and error handling in com protocols, for a whole bunch of reasons, but my main one is: “autonumbering”.

If you use enum the single items will receive an inferred value based on the previous item.
For instance

enum GAME_STATE{
   IDLE = 0,
   TITLE,
   INTRO,
   PLAYING,
   OVER,
   SCORE
};

you can now easily match against something that does not have a value but just a definition.
This means that if you want to add a state in-between you don’t need to fiddle with their defined/assigned values.

One more note:
When you define an Array and initialise its element in the same place, its outmost size is redundant, as it is inferred by the compiler using the amount of items added.
For multidimensional arrays you can omit the first size.

int world[][WORLD_WIDTH] = {
	{ 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0 },
	{ 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1 },
	{ 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0 },
	{ 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0 }
};

this will save you from having to define and track another value which is not needed :slight_smile:


(Pharap) #15

enum is better than define,
but enum class is better than both because of the extra type safety and unambiguity.

// Conflicting definition of Orange
enum Colour { Red, Green, Blue, Orange };
enum Fruit { Apple, Pear, Orange };

Vs

// No problem
enum class Colour { Red, Green, Blue, Orange };
enum class Fruit { Apple, Pear, Orange };

// Scoped enumerations prevent conflict
Colour colour = Colour::Orange;
Fruit fruit = Fruit::Orange;

And of course:

enum GameState { Idle, Title, Intro };

// No error, but this state isn't valid,
// so this ends up being a runtime bug
GameState gameState = 10;

Vs

enum class GameState { Idle, Title, Intro };

// Error: 10 is not a valid GameState,
// the compiler just stopped you shooting yourself
GameState gameState = 10;

My first Game - TicTacToe
(Josh) #16

:+1: Nice! Looking forward to part 10