Make Your Own Arduboy Game: Part 8 - Starting DinoSmasher

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

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

With the Arduboy2 library, 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()

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:

7 Likes

Don’t you mean ‘Let’s make a gameplay() function and reference it near the end of our loop() function instead of printing “HELLO” .’?

Also, love the tutorials, keep it up!

P.S:

The link at the end of the tutorial just links back to this page again.

1 Like

for anyone still searching here is the link to part nine Make Your Own Arduboy Game: Part 9 - Mapping DinoSmasher

2 Likes

Working through this one now and I have a couple more questions so far.

In the last tutorial we inserted the code to prevent the game from running too fast. THe code for that looked like this:

if (!arduboy.nextFrame())

In this tutorial it looks slightly different:

if (!(arduboy.nextFrame()))

Does it work the same either way? Is one prefered over the other?

In the printing text tutorial we printed hello like this:

arduboy.print("Hello");

At the start of this tutorial we insert this code:

arduboy.print(F("HELLO!"));

Later when we print it just uses the original format. What’s the purpose of the ‘F’ in this one?

You can put extra brackets around clauses to help clarify the intent of the code if it’s not clear. You can also ensure that the compiler dies the calculations I. The order you intended. For example ‘a + b / c’ will give different results to ‘(a + b) / c’ just like it does in regular maths.

The F macro is an Arduino / Arduboy/ ARM trick that stores the string in program memory rather than RAM. For programs that are not using much of the RAM it doesn’t really matter but as your programs get bigger and closer to the RAM limit you can free up memory with this little trick.

2 Likes

Small correction: AVR, not ARM.

Also, despite being needed because of the limitations of AVR, the ‘trick’ itself is part of the Arduino library rather than the underlying avr-libc (i.e. AVR implementation of the C library).


Personally I prefer the former: if (!arduboy.nextFrame()).

To me, using if (!(arduboy.nextFrame())) implies that the author doesn’t know that unary operators like ! have lower precedence than function calls. Function calls always evaluate before unary operators in nearly every language.

But on the other hand, using extra brackets with binary operators can be useful because languages do sometimes have different rules in regards to which operators are evaluated first.

If nothing else, it makes the intent clearer. Too many binary operators in one line can start to get confusing.

See also: C++ Operator Precedence - cppreference.com

@filmote has already explained the bulk of it, but a few caveats…

Firstly, using F only works with print and println. If you try to do it in other situations you’ll probably end up with some compiler errors.

It’s possible to create other functions that will work with the F macro, but that’s some fairly advanced voodoo so not many people know how to do it.

Secondly, if you wrap the same string in the F macro twice, you’ll end up with two copies of the same string. E.g.

arduboy.print(F("Hello"));
arduboy.print(F("Hello"));

Would cause two copies of "Hello" to be stored in progmem.

Most of the time that isn’t an issue, but if you ever get to the point where you’re really pushing the memory limit, it’s worth being aware of. There are tricks to get around this so you only end up with one copy of each string, but again that’s a bit more advanced.


The RAM/progmem dichotomy really deserves an entirely separate post, but I think there should be several explanations floating around the forum by now. I had a quick look and managed to dig up two: 1, 2.

2 Likes

Could you not just make a variable instead of using #define? That way you can use an enum class to organize all of your case variables?

1 Like

Yes, you could. In fact using a constexpr variable is better than using a macro (a “#define”). Macros are generally best avoided.

See:

An enum class (proper name: scoped enumeration) works slightly differently, but that’s also a much better option than using macros (#defines), and it’s actually a better option than using constexpr int variables too.

With a scoped enumeration, that part of the Dino Smasher code would look more like this:

#include <Arduboy2.h>

Arduboy2 arduboy;

// Creates a new type called 'GameState'
enum class GameState : uint8_t
{
  // Enumerator values
  // to represent the different game states
  Titlescreen,
  Gameplay,
  GameOver,
  Highscore,
};

// A variable of type 'GameState' used to
// track the current game state
GameState gameState = GameState::Titlescreen;

void updateGame()
{
  switch(gameState)
  {  
    case GameState::Titlescreen:
      // Title screen
      break;

    case GameState::Gameplay:
      // Gameplay
      break;

    case GameState::GameOver:
      // Game over screen
      break;

    case GameState::Highscore:
      // High score screen
      break;
  }  
}

void setup()
{
  arduboy.begin();
}

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

  arduboy.pollButtons();
  arduboy.clear();

  updateGame();

  arduboy.display();
}

Scoped enumerations are better because:

  • They’re type safe. That means you can’t accidentally do gameState = 10;, because 10 is an integer, not a GameState. If you were using ints then gameState = 11; would compile and suddenly you’d have a horrible bug in your code that might be difficult to track down. (Especially if you only meant to type 1 but hit the key twice by accident.)
  • You can specify how large they should be. Granted you can do this with integers too (by using uint8_t instead of int/int16_t), but it’s good that you can control this.
  • They automatically produce unique values for each ‘enumerator’ (that’s the fancy name for the values like GameState::Titlescreen and GameState::Gameplay), so you don’t have to worry about manually choosing an integer value for each unique ‘thing’ you want to create a distinct value for.

Secretly, enumerations are actually integers ‘under the hood’ (in the above code, GameState::Titlescreen has a value of 0, GameState::Gameplay has a value of 1, GameState::GameOver has a value of 2, et cetera), so they’re just as efficient and produce more or less the same code as using raw integer. They’re effectively just a clever way to make the compiler do a lot of the heavy lifting (i.e. assigning disticnt values) and prevent you from making mistakes.

(Note: Compiler errors are actually a good thing. They warn you about problems before you actually run your code rather than forcing you to hunt down the bugs manually while the code is running.)

(Note 2: You can actually convert enumeration types to and from integers if you want to override the safety, and there are rare occasions where this is useful. Particularly in defining functions that deal with enumeration types. E.g. you could make a Direction enumeration and a turnLeft function that would change Direction::North into Direction::West and so forth, which could be done more efficiently by manipulating the underlying integer values.)

1 Like