(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.

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, Part 7 and ESPECIALLY PART 8.

# This Tutorial

In this tutorial, we’ll be adding graphics to our game, DinoSmasher!! Make sure you’ve worked on Part 8 of the tutorial series because this tutorial directly follows the previous one. We’ll end up creating and moving a player around a map.

Here’s what we’ve go to work on to finish this tutorial.

1. Converting & drawing some images
2. Formatting our world’s images
3. Moving the map
4. Cropping the map
5. Cycling the map
6. Bounding the map

# Our Current 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();
}
``````

# 1. Converting & drawing some images

In the previous tutorial, we made the `world` 2D array out of 0’s and 1’s. Instead of drawing those numbers, let’s draw pictures, instead! Where we see a 0, let’s draw some grass, and where we see a 1, let’s draw some water!

Our maps will be made up of 16x16 images. Here are two sprites that I’ve made for us to use. They are 16x16 pixels.

GRASS:

WATER:

Converting them using my ToChars website and putting them into their own `byte` arrays, we get:

``````const unsigned char grass[] PROGMEM  = {
0xff, 0x7f, 0xfb, 0xff, 0xff, 0xbf, 0xff, 0xff, 0xf7, 0xff, 0xfd, 0xff, 0xff, 0xf7, 0x7f, 0xff, 0xdf, 0xff, 0xff, 0xfb, 0x7f, 0xff, 0xff, 0xff, 0xef, 0xfe, 0xff, 0xff, 0xfb, 0xff, 0x7f, 0xff
};

const unsigned char water[] PROGMEM  = {
0x08, 0x10, 0x10, 0x08, 0x10, 0x08, 0x10, 0x10, 0x10, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x40, 0x40, 0x20, 0x00, 0x01, 0x02, 0x02, 0x01, 0x02, 0x02, 0x01, 0x02, 0x21, 0x40, 0x40
};
``````

Since we’ll be dealing with 16x16, let’s define a keyword to keep track of that called `TILE_SIZE` and set it to `16`.

`#define TILE_SIZE 16`

Let’s update our `drawworld()` function’s `for` loops. Instead of printing each number, we need to use an `if` statement to determine which picture we’re going to draw.

``````#define TILE_SIZE	16
void drawworld() {
for(int y = 0; y < WORLD_HEIGHT; y++) {
for(int x = 0; x < WORLD_WIDTH; x++) {
if(world[y][x] == 0) {
arduboy.drawBitmap(x, y, grass, TILE_SIZE, TILE_SIZE, WHITE);
}
if(world[y][x] == 1) {
arduboy.drawBitmap(x, y, water, TILE_SIZE, TILE_SIZE, WHITE);
}
}
}
}
``````

If you run this code, you’ll see this.

Our problem is that we need to tile the images 16 pixels apart and the way we are drawing the images, we are only separating them 1 pixel since the `for` loops increase the `x` and `y` variables by 1 each loop.

Instead, let’s multiply the X and Y values by 16, or the `TILE_SIZE` when drawing the images.

``````#define TILE_SIZE	16
void drawworld() {
for(int y = 0; y < WORLD_HEIGHT; y++) {
for(int x = 0; x < WORLD_WIDTH; x++) {
if(world[y][x] == 0) {
arduboy.drawBitmap(x * TILE_SIZE, y * TILE_SIZE, grass, TILE_SIZE, TILE_SIZE, WHITE);
}
if(world[y][x] == 1) {
arduboy.drawBitmap(x * TILE_SIZE, y * TILE_SIZE, water, TILE_SIZE, TILE_SIZE, WHITE);
}
}
}
}
``````

There we go!

# 2. Formatting our world’s images

In our game’s world, we want to have a lot of different kinds of images. It would be pretty annoying to copy/paste a bunch of `if` statements in our `drawworld()` function. We could save some time by using a `switch` statement, but there’s a better way to condense this code now that we know more about arrays.

Remember when I said that we can have all sorts of stuff inside of arrays, including other arrays? Of course you do! What we can do is actually make an array of `char` arrays to hold all of our images.

Let’s make a 2D char array called `tiles` to hold our `water` and `grass` data.

``````const unsigned char tiles[2][32] PROGMEM  = {
{ 0xff, 0x7f, 0xfb, 0xff, 0xff, 0xbf, 0xff, 0xff, 0xf7, 0xff, 0xfd, 0xff, 0xff, 0xf7, 0x7f, 0xff, 0xdf, 0xff, 0xff, 0xfb, 0x7f, 0xff, 0xff, 0xff, 0xef, 0xfe, 0xff, 0xff, 0xfb, 0xff, 0x7f, 0xff },
{ 0x08, 0x10, 0x10, 0x08, 0x10, 0x08, 0x10, 0x10, 0x10, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x40, 0x40, 0x20, 0x00, 0x01, 0x02, 0x02, 0x01, 0x02, 0x02, 0x01, 0x02, 0x21, 0x40, 0x40 }
};
``````

I put `grass`'s data first, which would have index 0. Then, `water`'s data is second, at index 1.

To draw the `grass` and `water` data with `tiles`, we’d do something like this:

``````arduboy.drawBitmap(a, b, tiles[0], TILE_SIZE, TILE_SIZE, WHITE);
arduboy.drawBitmap(c, e, tiles[1], TILE_SIZE, TILE_SIZE, WHITE);
``````

Notice that the only thing different is the index `0` or `1`. This means that we won’t have to use any `if` statements in our `drawworld()` function! All we need to do is get the value at `world[y][x]`, which will be a 0 or 1, and put that into `tiles[]` to get the image data. This is what the updated `drawworld()` function would look like:

``````void drawworld() {
for(int y = 0; y < WORLD_HEIGHT; y++) {
for(int x = 0; x < WORLD_WIDTH; x++) {
arduboy.drawBitmap(x * TILE_SIZE, y * TILE_SIZE, tiles[ world[y][x] ], TILE_SIZE, TILE_SIZE, WHITE);

}
}
}
``````

Compile the code and see how it the result is the same as before!

``````//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;

const unsigned char tiles[2][32] PROGMEM  = {
{ 0xff, 0x7f, 0xfb, 0xff, 0xff, 0xbf, 0xff, 0xff, 0xf7, 0xff, 0xfd, 0xff, 0xff, 0xf7, 0x7f, 0xff, 0xdf, 0xff, 0xff, 0xfb, 0x7f, 0xff, 0xff, 0xff, 0xef, 0xfe, 0xff, 0xff, 0xfb, 0xff, 0x7f, 0xff },
{ 0x08, 0x10, 0x10, 0x08, 0x10, 0x08, 0x10, 0x10, 0x10, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x40, 0x40, 0x20, 0x00, 0x01, 0x02, 0x02, 0x01, 0x02, 0x02, 0x01, 0x02, 0x21, 0x40, 0x40 }
};

#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 }
};

#define TILE_SIZE	16
void drawworld() {
for(int y = 0; y < WORLD_HEIGHT; y++) {
for(int x = 0; x < WORLD_WIDTH; x++) {
arduboy.drawBitmap(x * TILE_SIZE, y * TILE_SIZE, tiles[world[y][x]], TILE_SIZE, TILE_SIZE, WHITE);

}
}
}

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();
}
``````

Even though the results are the same, we are doing this for a very important reason! If we want to add in more images, we can do that very easily! Let’s add two more in!

TREES:
STONE:

I converted the images with ToChars and then added them to the `tiles` array. The tree image should be the third and stone image should be the fourth image. Don’t forget to update the size of `tiles`.

``````const unsigned char tiles[4][32] PROGMEM  = {
{ 0xff, 0x7f, 0xfb, 0xff, 0xff, 0xbf, 0xff, 0xff, 0xf7, 0xff, 0xfd, 0xff, 0xff, 0xf7, 0x7f, 0xff, 0xdf, 0xff, 0xff, 0xfb, 0x7f, 0xff, 0xff, 0xff, 0xef, 0xfe, 0xff, 0xff, 0xfb, 0xff, 0x7f, 0xff },
{ 0x08, 0x10, 0x10, 0x08, 0x10, 0x08, 0x10, 0x10, 0x10, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x40, 0x40, 0x20, 0x00, 0x01, 0x02, 0x02, 0x01, 0x02, 0x02, 0x01, 0x02, 0x21, 0x40, 0x40 },
{ 0xff, 0x1f, 0x5b, 0x3f, 0xeb, 0xdd, 0xff, 0xf7, 0xbb, 0xef, 0xfd, 0x7f, 0xe3, 0xcb, 0xe3, 0xff, 0xff, 0xc7, 0x96, 0xc7, 0xff, 0xff, 0xef, 0xfd, 0xff, 0xe3, 0xcb, 0xe3, 0xff, 0xff, 0x7b, 0xff },
{ 0xff, 0xdf, 0x7b, 0x3f, 0x9f, 0x6f, 0x77, 0xab, 0xdb, 0xd7, 0xcd, 0x5f, 0xbf, 0x77, 0xff, 0xff, 0xff, 0xc1, 0xdc, 0xd3, 0xaf, 0x9f, 0xae, 0xb0, 0xbb, 0xbd, 0xbd, 0xba, 0xd7, 0xcc, 0x63, 0xff }
};
``````

Now that the images are added, let’s updated the `world` array to include the numbers `2` and `3`. These will cause stones and trees to be drawn.

``````int world[WORLD_HEIGHT][WORLD_WIDTH] = {
{ 2, 0, 0, 1, 0, 0, 0, 2, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 1, 1, 1, 0, 1, 0, 0, 1, 2, 0, 0, 0, 3, 3, 0, 2, 1, 1, 1 },
{ 0, 0, 0, 0, 0, 1, 3, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 2, 0 },
{ 3, 0, 0, 3, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0 }
};
``````

This is great! Except there’s two more things that I want to do before we get on to cooler things.

First of all, let’s adjust the `WORLD_WIDTH`. As it, we can’t see the entire map on the Arduboy’s screen, so we don’t really need one that big. Let’s change it to `8` and remove some values.

``````#define WORLD_WIDTH		8
...
int world[WORLD_HEIGHT][WORLD_WIDTH] = {
{ 2, 0, 0, 1, 0, 0, 0, 2 },
{ 0, 1, 1, 1, 0, 1, 0, 0 },
{ 0, 0, 0, 0, 0, 1, 3, 0 },
{ 3, 0, 0, 3, 2, 1, 1, 1 }
};
``````

Now that the `world` array is small enough, take a look at your Arduboy screen, you could (in theory) create any kind of combination of map using the trees, grass, stone, and water tiles that we have. But, it may be hard visualizing the result because the numbers could get confusing, especially if we end up with over 100 different numbers!

Let’s use `#define` to replace these numbers with words that makes sense to us. Use the following code:

``````#define GRASS			0
#define WATER			1
#define TREES			2
#define STONE			3
``````

Since we’re defining `GRASS`, `WATER`, `TREES`, and `STONE`, we can replace the way we store information in the `world` array to match.

``````int world[WORLD_HEIGHT][WORLD_WIDTH] = {
{ TREES, GRASS, GRASS, WATER, GRASS, GRASS, GRASS, TREES },
{ GRASS, WATER, WATER, WATER, GRASS, WATER, GRASS, GRASS },
{ GRASS, GRASS, GRASS, GRASS, GRASS, WATER, STONE, GRASS },
{ STONE, GRASS, GRASS, STONE, TREES, WATER, WATER, WATER }
};
``````

When making a games with 2D arrays for maps and stuff, I like to do this. I did this in Circuit Dude and Midnight Wild. This means you could download and modify the maps for those games by changing their 2D arrays.

Another thing about doing this is that you could have a different 2D array for each level.

Anyway, there is another really great reason to do this, but we’ll have to wait for that! For now, try this code on your device!

``````//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;

const unsigned char tiles[4][32] PROGMEM  = {
{ 0xff, 0x7f, 0xfb, 0xff, 0xff, 0xbf, 0xff, 0xff, 0xf7, 0xff, 0xfd, 0xff, 0xff, 0xf7, 0x7f, 0xff, 0xdf, 0xff, 0xff, 0xfb, 0x7f, 0xff, 0xff, 0xff, 0xef, 0xfe, 0xff, 0xff, 0xfb, 0xff, 0x7f, 0xff },
{ 0x08, 0x10, 0x10, 0x08, 0x10, 0x08, 0x10, 0x10, 0x10, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x40, 0x40, 0x20, 0x00, 0x01, 0x02, 0x02, 0x01, 0x02, 0x02, 0x01, 0x02, 0x21, 0x40, 0x40 },
{ 0xff, 0x1f, 0x5b, 0x3f, 0xeb, 0xdd, 0xff, 0xf7, 0xbb, 0xef, 0xfd, 0x7f, 0xe3, 0xcb, 0xe3, 0xff, 0xff, 0xc7, 0x96, 0xc7, 0xff, 0xff, 0xef, 0xfd, 0xff, 0xe3, 0xcb, 0xe3, 0xff, 0xff, 0x7b, 0xff },
{ 0xff, 0xdf, 0x7b, 0x3f, 0x9f, 0x6f, 0x77, 0xab, 0xdb, 0xd7, 0xcd, 0x5f, 0xbf, 0x77, 0xff, 0xff, 0xff, 0xc1, 0xdc, 0xd3, 0xaf, 0x9f, 0xae, 0xb0, 0xbb, 0xbd, 0xbd, 0xba, 0xd7, 0xcc, 0x63, 0xff }
};

#define WORLD_WIDTH		8
#define WORLD_HEIGHT	4
#define GRASS			0
#define WATER			1
#define TREES			2
#define STONE			3
int world[WORLD_HEIGHT][WORLD_WIDTH] = {
{ TREES, GRASS, GRASS, WATER, GRASS, GRASS, GRASS, TREES },
{ GRASS, WATER, WATER, WATER, GRASS, WATER, GRASS, GRASS },
{ GRASS, GRASS, GRASS, GRASS, GRASS, WATER, STONE, GRASS },
{ STONE, GRASS, GRASS, STONE, TREES, WATER, WATER, WATER }
};

#define TILE_SIZE	16
void drawworld() {
for(int y = 0; y < WORLD_HEIGHT; y++) {
for(int x = 0; x < WORLD_WIDTH; x++) {
arduboy.drawBitmap(x * TILE_SIZE, y * TILE_SIZE, tiles[world[y][x]], TILE_SIZE, TILE_SIZE, WHITE);

}
}
}

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();
}
``````

# 3. Moving the map

Now, in Part 6, we were able to draw some grass and allowed the player to move a character around by changing their character’s X and Y position values. However, not all games work like that. Some games, the player is stuck in the center of the screen and the game’s map moves in the background. Games like Zelda and Pokemon do this.

To learn how to do it, there’s a few things we need to understand, but first, let’s just simply move the X and Y position of the background based on the player’s position to offset the map on the screen. Let’s make a variable for both the X and Y called `mapx` and `mapy’.

``````int mapx = 0;
int mapy = 0;
``````

In the game, we are going to allow the player to move around and change the values of `mapx` and `mapy`. After they are updated, we want to draw the world. This means that we need to get the player’s input before drawing the world.

We should make a function called `playerinput()` to handle these changes. In the `grameplay()` function, before `drawworld()`, let’s reference the `playerinput()` function.

``````void playerinput() {

}

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

playerinput();
drawworld();

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

Changing the values of `mapx` and `mapy` will be easy. Use this code, which should be familiar to you:

``````void playerinput() {
if(arduboy.pressed(UP_BUTTON)) {
mapy += 1;
}
if(arduboy.pressed(DOWN_BUTTON)) {
mapy -= 1;
}
if(arduboy.pressed(LEFT_BUTTON)) {
mapx += 1;
}
if(arduboy.pressed(RIGHT_BUTTON)) {
mapx -= 1;
}
}
``````

When we draw the game world, let’s offset it by `mapx` and `mapy`.

``````void drawworld() {
for(int y = 0; y < WORLD_HEIGHT; y++) {
for(int x = 0; x < WORLD_WIDTH; x++) {
arduboy.drawBitmap(x * TILE_SIZE + mapx, y * TILE_SIZE + mapy, tiles[world[y][x]], TILE_SIZE, TILE_SIZE, WHITE);

}
}
}
``````

After compiling your code, you should be able to use the direction buttons to move the map around the screen. Try the following code. (I’ve adjusted the world map’s size.)

``````//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 mapx = 0;
int mapy = 0;

const unsigned char tiles[4][32] PROGMEM  = {
{ 0xff, 0x7f, 0xfb, 0xff, 0xff, 0xbf, 0xff, 0xff, 0xf7, 0xff, 0xfd, 0xff, 0xff, 0xf7, 0x7f, 0xff, 0xdf, 0xff, 0xff, 0xfb, 0x7f, 0xff, 0xff, 0xff, 0xef, 0xfe, 0xff, 0xff, 0xfb, 0xff, 0x7f, 0xff },
{ 0x08, 0x10, 0x10, 0x08, 0x10, 0x08, 0x10, 0x10, 0x10, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x40, 0x40, 0x20, 0x00, 0x01, 0x02, 0x02, 0x01, 0x02, 0x02, 0x01, 0x02, 0x21, 0x40, 0x40 },
{ 0xff, 0x1f, 0x5b, 0x3f, 0xeb, 0xdd, 0xff, 0xf7, 0xbb, 0xef, 0xfd, 0x7f, 0xe3, 0xcb, 0xe3, 0xff, 0xff, 0xc7, 0x96, 0xc7, 0xff, 0xff, 0xef, 0xfd, 0xff, 0xe3, 0xcb, 0xe3, 0xff, 0xff, 0x7b, 0xff },
{ 0xff, 0xdf, 0x7b, 0x3f, 0x9f, 0x6f, 0x77, 0xab, 0xdb, 0xd7, 0xcd, 0x5f, 0xbf, 0x77, 0xff, 0xff, 0xff, 0xc1, 0xdc, 0xd3, 0xaf, 0x9f, 0xae, 0xb0, 0xbb, 0xbd, 0xbd, 0xba, 0xd7, 0xcc, 0x63, 0xff }
};

#define WORLD_WIDTH		14
#define WORLD_HEIGHT	7
#define GRASS			0
#define WATER			1
#define TREES			2
#define STONE			3
int world[WORLD_HEIGHT][WORLD_WIDTH] = {
{ TREES, GRASS, GRASS, WATER, GRASS, GRASS, GRASS, TREES, GRASS, GRASS, GRASS, GRASS, GRASS, TREES },
{ GRASS, WATER, WATER, WATER, GRASS, WATER, GRASS, GRASS, GRASS, GRASS, GRASS, STONE, GRASS, GRASS },
{ GRASS, GRASS, GRASS, GRASS, GRASS, WATER, STONE, GRASS, GRASS, GRASS, TREES, GRASS, GRASS, GRASS },
{ STONE, GRASS, GRASS, STONE, TREES, WATER, WATER, WATER, GRASS, WATER, WATER, GRASS, TREES, GRASS },
{ GRASS, GRASS, GRASS, GRASS, TREES, GRASS, GRASS, GRASS, TREES, WATER, GRASS, GRASS, STONE, TREES },
{ GRASS, GRASS, GRASS, WATER, STONE, GRASS, GRASS, TREES, TREES, TREES, GRASS, GRASS, WATER, WATER },
{ GRASS, WATER, WATER, TREES, GRASS, WATER, WATER, TREES, TREES, GRASS, GRASS, GRASS, GRASS, STONE }
};

#define TILE_SIZE	16
void drawworld() {
for(int y = 0; y < WORLD_HEIGHT; y++) {
for(int x = 0; x < WORLD_WIDTH; x++) {
arduboy.drawBitmap(x * TILE_SIZE + mapx, y * TILE_SIZE + mapy, tiles[world[y][x]], TILE_SIZE, TILE_SIZE, WHITE);

}
}
}

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

void playerinput() {
if(arduboy.pressed(UP_BUTTON)) {
mapy += 1;
}
if(arduboy.pressed(DOWN_BUTTON)) {
mapy -= 1;
}
if(arduboy.pressed(LEFT_BUTTON)) {
mapx += 1;
}
if(arduboy.pressed(RIGHT_BUTTON)) {
mapx -= 1;
}
}

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

playerinput();
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();
}
``````

Right now, the map is being drawn that is bigger than the Arduboy screen. The bigger the map is, the more images the Arduboy will try to draw. This will slow down the Arduboy at some point, so we need to crop the map and only draw a portion that is slightly bigger than the Arduboy’s screen.

We also want to stop the player from being able to move the map off of the screen. There needs to be some kinds of boundaries!

# 4. Cropping the map

Attempting to loop through and draw everything in our `world` array will slow our game down so much that it’ll be unplayable if `world` gets too big.

We really only need to draw 8 tiles wide and 4 tiles high to fill the entire screen. The way we get these numbers is by finding the how many tiles can span across the the width of the screen and height of the screen. The tiles are 16 pixels wide and 16 pixels tall. The screen is 128 pixels wide and 64 pixels tall.

Screen width of 128 pixels wide ÷ tile width of 16 pixels = 8
Screen height of 64 pixels wide ÷ tile height of 16 pixels = 4

Since these values won’t change, we could store these into constant `int`'s!

``````const int tileswide = 128 / TILE_SIZE;
const int tilestall = 64 / TILE_SIZE;
``````

The Arduboy2 library has defined `WIDTH` and `HEIGHT` for us to use in a situation like this to represent the screen’s size so we don’t have to always remember the exact values. Let’s use those:

``````const int tileswide = WIDTH / TILE_SIZE;
const int tilestall = HEIGHT / TILE_SIZE;
``````

Alright, let’s adjust our draw function’s `for` loops. Instead of looping through the entire `WORLD_HEIGHT` and `WORLD_WIDTH`, we only need to go to 8 tall and 4 wide, which are the values of `tileswide` and `tilestall`. Let’s swap those out:

``````void drawworld() {
const int tileswide = WIDTH / TILE_SIZE;
const int tilestall = HEIGHT / TILE_SIZE;

for(int y = 0; y < tilestall; y++) {
for(int x = 0; x < tileswide; x++) {
arduboy.drawBitmap(x * TILE_SIZE + mapx, y * TILE_SIZE + mapy, tiles[world[y][x]], TILE_SIZE, TILE_SIZE, WHITE);

}
}
}
``````

Compiling and running this code would cause you to realize that you can now move a smaller section of the map around the screen.
When we draw the map tiles, we are offsetting them with the `mapx` and `mapy` variables. If these get too big, the map will move off of the screen and we won’t see anything!!

Instead, we need to keep the map on the screen and prevent the `mapx` and `mapy` variables from getting too big.

What we could do is use some code like this:

``````if(mapx > 10) {
mapx = 0;
}
if(mapx < 0) {
mapx = 10;
}
``````

This would ensure that `mapx` is between `0` and `10`. This isn’t optimal, though. Remember, the modulo operator could be used to do this!

``````mapx = mapx % 10;
``````

In this case, we’d be letting the map move up to 10 pixels before the screen moved back. We would also be overwriting our `mapx` variable. This isn’t something we want to save- We only want to use this value when drawing our map tiles. Let’s replace `mapx` and `mapy` with `mapx % 10` and `mapy % 10` in our `arduboy.drawBitmap()` function. This way, we can continue to keep track of the map’s X/Y position, no matter how big and still confine the map to be drawn on the screen.

``````void drawworld() {
const int tileswide = WIDTH / TILE_SIZE;
const int tilestall = HEIGHT / TILE_SIZE;

for(int y = 0; y < tilestall; y++) {
for(int x = 0; x < tileswide; x++) {
arduboy.drawBitmap(x * TILE_SIZE + mapx % 10, y * TILE_SIZE + mapy % 10, tiles[world[y][x]], TILE_SIZE, TILE_SIZE, WHITE);

}
}
}
``````

Compiling and running your code will result in the ability to move the map but not too far from the screen! It even works going backwards! But do you notice that there is a lot of black space when moving the map around? To fix this, we should increase how many images we draw across and tall by 1 to fill in that space.

``````	const int tileswide = WIDTH / TILE_SIZE + 1;
const int tilestall = HEIGHT / TILE_SIZE + 1;
``````

Compile your code and test it out!

Alright, there’s another problem… When moving around, you can’t even see the entirety of the bottom and right-most tiles. This is because we are only allowing the player to see `10` pixels of those tiles at a time. To stop it from cutting, let’s change the 10 to 16. Actually, no. Let’s change the `10` to `TILE_SIZE` so that we can see the entire tile.

``````void drawworld() {
const int tileswide = WIDTH / TILE_SIZE + 1;
const int tilestall = HEIGHT / TILE_SIZE + 1;

for(int y = 0; y < tilestall; y++) {
for(int x = 0; x < tileswide; x++) {
arduboy.drawBitmap(x * TILE_SIZE + mapx % TILE_SIZE, y * TILE_SIZE + mapy % TILE_SIZE, tiles[world[y][x]], TILE_SIZE, TILE_SIZE, WHITE);

}
}
}
``````

Now that we confine the map to the screen the BIGGEST problem that I’ve ignored is that the map’s tiles don’t actually change, so you can’t actually pan around the world. You’ll only be looking at the same section of the map!

Let’s do that, now!

Make Your Own Arduboy Game: Part 8 - Starting DinoSmasher
(Holmes) #2

# 5. Cycling the map

To understand how cycling through the map works, let’s verify a few things, including our X/Y coordinates. Let’s print those out.

In the `gameplay()` function, remove the `arduboy.setCursor()` and `arduboy.print()` functions since we don’t need that, anymore.

In the `drawworld()` functions, let’s draw a black rectangle and then print our `mapx` and `mapy` coordinates after drawing the tiles in the `for` loops.

OH, we should also add a black 16x16 square to the middle of the screen to represent the player.

``````	arduboy.fillRect(WIDTH / 2 - 8, HEIGHT / 2 - 8, 16, 16, BLACK);

arduboy.fillRect(0, 0, 48, 8, BLACK);
arduboy.setCursor(0, 0);
arduboy.print(mapx);
arduboy.print(",");
arduboy.print(mapx);
``````

Let’s run these quick changes and observe what happens when we move the screen around. Here’s the full code so far:

``````//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 mapx = 0;
int mapy = 0;

const unsigned char tiles[4][32] PROGMEM  = {
{ 0xff, 0x7f, 0xfb, 0xff, 0xff, 0xbf, 0xff, 0xff, 0xf7, 0xff, 0xfd, 0xff, 0xff, 0xf7, 0x7f, 0xff, 0xdf, 0xff, 0xff, 0xfb, 0x7f, 0xff, 0xff, 0xff, 0xef, 0xfe, 0xff, 0xff, 0xfb, 0xff, 0x7f, 0xff },
{ 0x08, 0x10, 0x10, 0x08, 0x10, 0x08, 0x10, 0x10, 0x10, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x40, 0x40, 0x20, 0x00, 0x01, 0x02, 0x02, 0x01, 0x02, 0x02, 0x01, 0x02, 0x21, 0x40, 0x40 },
{ 0xff, 0x1f, 0x5b, 0x3f, 0xeb, 0xdd, 0xff, 0xf7, 0xbb, 0xef, 0xfd, 0x7f, 0xe3, 0xcb, 0xe3, 0xff, 0xff, 0xc7, 0x96, 0xc7, 0xff, 0xff, 0xef, 0xfd, 0xff, 0xe3, 0xcb, 0xe3, 0xff, 0xff, 0x7b, 0xff },
{ 0xff, 0xdf, 0x7b, 0x3f, 0x9f, 0x6f, 0x77, 0xab, 0xdb, 0xd7, 0xcd, 0x5f, 0xbf, 0x77, 0xff, 0xff, 0xff, 0xc1, 0xdc, 0xd3, 0xaf, 0x9f, 0xae, 0xb0, 0xbb, 0xbd, 0xbd, 0xba, 0xd7, 0xcc, 0x63, 0xff }
};

#define WORLD_WIDTH		14
#define WORLD_HEIGHT	7
#define GRASS			0
#define WATER			1
#define TREES			2
#define STONE			3
int world[WORLD_HEIGHT][WORLD_WIDTH] = {
{ TREES, GRASS, GRASS, WATER, GRASS, GRASS, GRASS, TREES, GRASS, GRASS, GRASS, GRASS, GRASS, TREES },
{ GRASS, WATER, WATER, WATER, GRASS, WATER, GRASS, GRASS, GRASS, GRASS, GRASS, STONE, GRASS, GRASS },
{ GRASS, GRASS, GRASS, GRASS, GRASS, WATER, STONE, GRASS, GRASS, GRASS, TREES, GRASS, GRASS, GRASS },
{ STONE, GRASS, GRASS, STONE, TREES, WATER, WATER, WATER, GRASS, WATER, WATER, GRASS, TREES, GRASS },
{ GRASS, GRASS, GRASS, GRASS, TREES, GRASS, GRASS, GRASS, TREES, WATER, GRASS, GRASS, STONE, TREES },
{ GRASS, GRASS, GRASS, WATER, STONE, GRASS, GRASS, TREES, TREES, TREES, GRASS, GRASS, WATER, WATER },
{ GRASS, WATER, WATER, TREES, GRASS, WATER, WATER, TREES, TREES, GRASS, GRASS, GRASS, GRASS, STONE }
};

#define TILE_SIZE	16
void drawworld() {
const int tileswide = WIDTH / TILE_SIZE + 1;
const int tilestall = HEIGHT / TILE_SIZE + 1;

for(int y = 0; y < tilestall; y++) {
for(int x = 0; x < tileswide; x++) {
arduboy.drawBitmap(x * TILE_SIZE + mapx % TILE_SIZE, y * TILE_SIZE + mapy % TILE_SIZE, tiles[world[y][x]], TILE_SIZE, TILE_SIZE, WHITE);

}
}

arduboy.fillRect(0, 0, 48, 8, BLACK);
arduboy.setCursor(0, 0);
arduboy.print(mapx);
arduboy.print(",");
arduboy.print(mapy);
}

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

void playerinput() {
if(arduboy.pressed(UP_BUTTON)) {
mapy += 1;
}
if(arduboy.pressed(DOWN_BUTTON)) {
mapy -= 1;
}
if(arduboy.pressed(LEFT_BUTTON)) {
mapx += 1;
}
if(arduboy.pressed(RIGHT_BUTTON)) {
mapx -= 1;
}
}

void gameplay() {
playerinput();
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();
}
``````

Notice that every time we move the screen 16 pixels in any direction, the screen jumps back. If you move slowly and watch, you’ll notice that the tiles align perfectly, even if there is a jump. The only thing that is wrong is that that the next tiles are not displayed and instead, the same tiles are displayed over and over.

In order to display the next tiles, we need to figure out which tiles we are going to draw. We must keep track every time the map jumps around. We could do this with another variable, but we could also calculate this based off of `mapx` and `mapy`.

Remember from Part 7, whenever we divide an `int` in C++, it only returns whole numbers! Dividing gets rid of the remainder. We could divide the `mapx` and `mapy` by 16 to find out how many times we’ve jumped the map around.

In the `drawworld()` function, we should change what we’re printing out:

``````	arduboy.print(mapx / TILE_SIZE);
arduboy.print(",");
arduboy.print(mapy / TILE_SIZE);
``````

Run this code and move the map around!

Remember that we are drawing the top-right of the map, so moving left and down is what we really want to focus on. When we use the left and down buttons to move, notice that it does what we want except the result of our calculations are negative… This isn’t a big deal! There is a way to reverse it by subtracting the values from 0.

``````	arduboy.print(0 - mapx / TILE_SIZE);
arduboy.print(",");
arduboy.print(0 - mapy / TILE_SIZE);
``````

Now that we can calculate these values to keep track of how many times we jump the map around, we can add that to our `arduboy.drawBitmap()` function to offset which `tile` from `world` we are going to draw.

Currently, we are just using the `x` and `y` variables. Let’s create new `const int` variables called `tilex` and `tiley` to hold the values of `x` and `y` plus the calculations, then use those instead of `x` and `y`.

``````	for(int y = 0; y < tilestall; y++) {
for(int x = 0; x < tileswide; x++) {
const int tilex = x - mapx / TILE_SIZE;
const int tiley = y - mapy / TILE_SIZE;
arduboy.drawBitmap(x * TILE_SIZE + mapx % TILE_SIZE, y * TILE_SIZE + mapy % TILE_SIZE, tiles[world[tiley][tilx]], TILE_SIZE, TILE_SIZE, WHITE);

}
}
``````

Just to clarify, to get the tile bitmap of the tile that is being drawn, we need to get the value inside of `world` at (`tilex`,`tiley`), then use that value as the index of the `tiles` array.

``````tiles[    world[    tiley    ][    tilex    ]    ]
``````

Now, what I want you to do is compile your code and put it onto the Arduboy. (I’m including mine right below this.) Use the bottom and right buttons to move around. You should see the screen scroll, but what happens if you keep going and view parts of the map that don’t exist, yet?

``````//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 mapx = 0;
int mapy = 0;

const unsigned char tiles[4][32] PROGMEM  = {
{ 0xff, 0x7f, 0xfb, 0xff, 0xff, 0xbf, 0xff, 0xff, 0xf7, 0xff, 0xfd, 0xff, 0xff, 0xf7, 0x7f, 0xff, 0xdf, 0xff, 0xff, 0xfb, 0x7f, 0xff, 0xff, 0xff, 0xef, 0xfe, 0xff, 0xff, 0xfb, 0xff, 0x7f, 0xff },
{ 0x08, 0x10, 0x10, 0x08, 0x10, 0x08, 0x10, 0x10, 0x10, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x40, 0x40, 0x20, 0x00, 0x01, 0x02, 0x02, 0x01, 0x02, 0x02, 0x01, 0x02, 0x21, 0x40, 0x40 },
{ 0xff, 0x1f, 0x5b, 0x3f, 0xeb, 0xdd, 0xff, 0xf7, 0xbb, 0xef, 0xfd, 0x7f, 0xe3, 0xcb, 0xe3, 0xff, 0xff, 0xc7, 0x96, 0xc7, 0xff, 0xff, 0xef, 0xfd, 0xff, 0xe3, 0xcb, 0xe3, 0xff, 0xff, 0x7b, 0xff },
{ 0xff, 0xdf, 0x7b, 0x3f, 0x9f, 0x6f, 0x77, 0xab, 0xdb, 0xd7, 0xcd, 0x5f, 0xbf, 0x77, 0xff, 0xff, 0xff, 0xc1, 0xdc, 0xd3, 0xaf, 0x9f, 0xae, 0xb0, 0xbb, 0xbd, 0xbd, 0xba, 0xd7, 0xcc, 0x63, 0xff }
};

#define WORLD_WIDTH		14
#define WORLD_HEIGHT	7
#define GRASS			0
#define WATER			1
#define TREES			2
#define STONE			3
int world[WORLD_HEIGHT][WORLD_WIDTH] = {
{ TREES, GRASS, GRASS, WATER, GRASS, GRASS, GRASS, TREES, GRASS, GRASS, GRASS, GRASS, GRASS, TREES },
{ GRASS, WATER, WATER, WATER, GRASS, WATER, GRASS, GRASS, GRASS, GRASS, GRASS, STONE, GRASS, GRASS },
{ GRASS, GRASS, GRASS, GRASS, GRASS, WATER, STONE, GRASS, GRASS, GRASS, TREES, GRASS, GRASS, GRASS },
{ STONE, GRASS, GRASS, STONE, TREES, WATER, WATER, WATER, GRASS, WATER, WATER, GRASS, TREES, GRASS },
{ GRASS, GRASS, GRASS, GRASS, TREES, GRASS, GRASS, GRASS, TREES, WATER, GRASS, GRASS, STONE, TREES },
{ GRASS, GRASS, GRASS, WATER, STONE, GRASS, GRASS, TREES, TREES, TREES, GRASS, GRASS, WATER, WATER },
{ GRASS, WATER, WATER, TREES, GRASS, WATER, WATER, TREES, TREES, GRASS, GRASS, GRASS, GRASS, STONE }
};

#define TILE_SIZE	16
void drawworld() {
const int tileswide = WIDTH / TILE_SIZE + 1;
const int tilestall = HEIGHT / TILE_SIZE + 1;

for(int y = 0; y < tilestall; y++) {
for(int x = 0; x < tileswide; x++) {
const int tilex = x - mapx / TILE_SIZE;
const int tiley = y - mapy / TILE_SIZE;
arduboy.drawBitmap(x * TILE_SIZE + mapx % TILE_SIZE, y * TILE_SIZE + mapy % TILE_SIZE, tiles[world[tiley][tilex]], TILE_SIZE, TILE_SIZE, WHITE);

}
}

arduboy.fillRect(WIDTH / 2 - 8, HEIGHT / 2 - 8, 16, 16, BLACK);

arduboy.fillRect(0, 0, 48, 8, BLACK);
arduboy.setCursor(0, 0);
arduboy.print(0 - mapx / TILE_SIZE);
arduboy.print(",");
arduboy.print(0 - mapy / TILE_SIZE);
}

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

void playerinput() {
if(arduboy.pressed(UP_BUTTON)) {
mapy += 1;
}
if(arduboy.pressed(DOWN_BUTTON)) {
mapy -= 1;
}
if(arduboy.pressed(LEFT_BUTTON)) {
mapx += 1;
}
if(arduboy.pressed(RIGHT_BUTTON)) {
mapx -= 1;
}
}

void gameplay() {
playerinput();
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();
}
``````

Isn’t it cool that you can navigate outside of our `world` array? Like I said near the end of Part 8, what’s being displayed is data from other variables and other parts of the Arduboy’s memory.

# 6. Bounding the map

Next, what we should do is prevent the player from seeing all this extra stuff outside of our `world`. We also should prevent them from moving the black square outside of it, as well! Let’s start by choosing to only draw the tiles that are in range during our `drawworld()` function’s `for` loops.

Remember, we are already taking into consideration which tiles are being drawn from `world` with `tilex` and `tiley`. What we could do is draw the tile `if` `tilex`/`tiley` are positive (greater than `-1`) and less than the `WORLD_WIDTH`/`WORLD_HEIGHT`.

``````if(tilex >= 0 && tiley >= 0 && tilex < WORLD_WIDTH && tiley < WORLD_HEIGHT) {
arduboy.drawBitmap(x * TILE_SIZE + mapx % TILE_SIZE, y * TILE_SIZE + mapy % TILE_SIZE, tiles[world[tiley][tilex]], TILE_SIZE, TILE_SIZE, WHITE);
}
``````

Now, to make sure the player cannot move outside of our `world`, we need to realize that when the world map is at (0, 0), the player is not at (0, 0). That is because we’re drawing it in the middle of what’s on screen. Incidentally, we are also drawing the character in the `drawworld()` function. Let’s remove that `arduboy.fillRect()` call and do in a better place.

Let’s make a new function right above `drawworld()` and call it `drawplayer()`. We want to also reference it inside of `gameplay()`, right after `drawworld()`. Remember, if we draw something to the screen before `drawworld()`, then it will be drawn prior, and drawn under the tiles. We want to see the player on top of them.

Alongside `drawworld()`, we should define size of our player as 16 using `#define PLAYER_SIZE 16` . We can use that when drawing our rectangle similar to what we did earlier:

``````arduboy.fillRect(WIDTH / 2 - PLAYER_SIZE / 2, HEIGHT / 2 - PLAYER_SIZE / 2, PLAYER_SIZE, PLAYER_SIZE, BLACK);
``````

That is a little hard to read, so let’s actually make another definition! Let’s store the rectangle’s X offset and rectangle’s Y offset and use those values when drawing our rectangle.

``````#define PLAYER_SIZE			16
#define PLAYER_X_OFFSET		WIDTH / 2 - PLAYER_SIZE / 2
#define PLAYER_Y_OFFSET		HEIGHT / 2 - PLAYER_SIZE / 2
void drawplayer() {
arduboy.fillRect(PLAYER_X_OFFSET, PLAYER_Y_OFFSET, PLAYER_SIZE, PLAYER_SIZE, BLACK);
}
``````

Storing the `PLAYER_X_OFFSET` and `PLAYER_Y_OFFSET` can come in handy when figuring out if the player’s character is touching the edge of the `world`.

For instance, if we are letting the map slide down the screen, we don’t want the top of it to slide below the top of the player’s square. The top of the map is at `mapy` and the top of the player’s square is at `PLAYER_Y_OFFSET`. This works with the X variables, too. Let’s update the `playerinput()` functions to include these restrictions.

``````void playerinput() {
if(arduboy.pressed(UP_BUTTON)) {
if(mapy < PLAYER_Y_OFFSET) {
mapy += 1;
}
}
if(arduboy.pressed(DOWN_BUTTON)) {
mapy -= 1;
}
if(arduboy.pressed(LEFT_BUTTON)) {
if(mapx < PLAYER_X_OFFSET) {
mapx += 1;
}
}
if(arduboy.pressed(RIGHT_BUTTON)) {
mapx -= 1;
}
}
``````

Testing this code, you will see that it works, but only on the top and left-side of the map.

For the bottom of the map, we only want the down button to move the map up if the bottom of the map is still below the bottom of the player’s square. These can be calculated with `mapy + TILE_SIZE * WORLD_HEIGHT` and `PLAYER_Y_OFFSET + PLAYER_SIZE` You can do this for the X axis, too!

``````void playerinput() {
if(arduboy.pressed(UP_BUTTON)) {
if(mapy < PLAYER_Y_OFFSET) {
mapy += 1;
}
}
if(arduboy.pressed(DOWN_BUTTON)) {
if(PLAYER_Y_OFFSET + PLAYER_SIZE < mapy + TILE_SIZE * WORLD_HEIGHT) {
mapy -= 1;
}
}
if(arduboy.pressed(LEFT_BUTTON)) {
if(mapx < PLAYER_X_OFFSET) {
mapx += 1;
}
}
if(arduboy.pressed(RIGHT_BUTTON)) {
if(PLAYER_X_OFFSET + PLAYER_SIZE < mapx + TILE_SIZE * WORLD_WIDTH) {
mapx -= 1;
}
}
}
``````

Here is our 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 PLAYER_SIZE		16
int mapx = 0;
int mapy = 0;

const unsigned char tiles[4][32] PROGMEM  = {
{ 0xff, 0x7f, 0xfb, 0xff, 0xff, 0xbf, 0xff, 0xff, 0xf7, 0xff, 0xfd, 0xff, 0xff, 0xf7, 0x7f, 0xff, 0xdf, 0xff, 0xff, 0xfb, 0x7f, 0xff, 0xff, 0xff, 0xef, 0xfe, 0xff, 0xff, 0xfb, 0xff, 0x7f, 0xff },
{ 0x08, 0x10, 0x10, 0x08, 0x10, 0x08, 0x10, 0x10, 0x10, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x40, 0x40, 0x20, 0x00, 0x01, 0x02, 0x02, 0x01, 0x02, 0x02, 0x01, 0x02, 0x21, 0x40, 0x40 },
{ 0xff, 0x1f, 0x5b, 0x3f, 0xeb, 0xdd, 0xff, 0xf7, 0xbb, 0xef, 0xfd, 0x7f, 0xe3, 0xcb, 0xe3, 0xff, 0xff, 0xc7, 0x96, 0xc7, 0xff, 0xff, 0xef, 0xfd, 0xff, 0xe3, 0xcb, 0xe3, 0xff, 0xff, 0x7b, 0xff },
{ 0xff, 0xdf, 0x7b, 0x3f, 0x9f, 0x6f, 0x77, 0xab, 0xdb, 0xd7, 0xcd, 0x5f, 0xbf, 0x77, 0xff, 0xff, 0xff, 0xc1, 0xdc, 0xd3, 0xaf, 0x9f, 0xae, 0xb0, 0xbb, 0xbd, 0xbd, 0xba, 0xd7, 0xcc, 0x63, 0xff }
};

#define WORLD_WIDTH			14
#define WORLD_HEIGHT		7
#define GRASS				0
#define WATER				1
#define TREES				2
#define STONE				3
int world[WORLD_HEIGHT][WORLD_WIDTH] = {
{ TREES, GRASS, GRASS, WATER, GRASS, GRASS, GRASS, TREES, GRASS, GRASS, GRASS, GRASS, GRASS, TREES },
{ GRASS, WATER, WATER, WATER, GRASS, WATER, GRASS, GRASS, GRASS, GRASS, GRASS, STONE, GRASS, GRASS },
{ GRASS, GRASS, GRASS, GRASS, GRASS, WATER, STONE, GRASS, GRASS, GRASS, TREES, GRASS, GRASS, GRASS },
{ STONE, GRASS, GRASS, STONE, TREES, WATER, WATER, WATER, GRASS, WATER, WATER, GRASS, TREES, GRASS },
{ GRASS, GRASS, GRASS, GRASS, TREES, GRASS, GRASS, GRASS, TREES, WATER, GRASS, GRASS, STONE, TREES },
{ GRASS, GRASS, GRASS, WATER, STONE, GRASS, GRASS, TREES, TREES, TREES, GRASS, GRASS, WATER, WATER },
{ GRASS, WATER, WATER, TREES, GRASS, WATER, WATER, TREES, TREES, GRASS, GRASS, GRASS, GRASS, STONE }
};

#define PLAYER_SIZE			16
#define PLAYER_X_OFFSET		WIDTH / 2 - PLAYER_SIZE / 2
#define PLAYER_Y_OFFSET		HEIGHT / 2 - PLAYER_SIZE / 2
void drawplayer() {
arduboy.fillRect(PLAYER_X_OFFSET, PLAYER_Y_OFFSET, PLAYER_SIZE, PLAYER_SIZE, BLACK);
}

#define TILE_SIZE			16
void drawworld() {
const int tileswide = WIDTH / TILE_SIZE + 1;
const int tilestall = HEIGHT / TILE_SIZE + 1;

for(int y = 0; y < tilestall; y++) {
for(int x = 0; x < tileswide; x++) {
const int tilex = x - mapx / TILE_SIZE;
const int tiley = y - mapy / TILE_SIZE;
if(tilex >= 0 && tiley >= 0 && tilex < WORLD_WIDTH && tiley < WORLD_HEIGHT) {
arduboy.drawBitmap(x * TILE_SIZE + mapx % TILE_SIZE, y * TILE_SIZE + mapy % TILE_SIZE, tiles[world[tiley][tilex]], TILE_SIZE, TILE_SIZE, WHITE);
}
}
}

arduboy.fillRect(0, 0, 48, 8, BLACK);
arduboy.setCursor(0, 0);
arduboy.print(0 - mapx / TILE_SIZE);
arduboy.print(",");
arduboy.print(0 - mapy / TILE_SIZE);
}

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

void playerinput() {
if(arduboy.pressed(UP_BUTTON)) {
if(mapy < PLAYER_Y_OFFSET) {
mapy += 1;
}
}
if(arduboy.pressed(DOWN_BUTTON)) {
if(PLAYER_Y_OFFSET + PLAYER_SIZE < mapy + TILE_SIZE * WORLD_HEIGHT) {
mapy -= 1;
}
}
if(arduboy.pressed(LEFT_BUTTON)) {
if(mapx < PLAYER_X_OFFSET) {
mapx += 1;
}
}
if(arduboy.pressed(RIGHT_BUTTON)) {
if(PLAYER_X_OFFSET + PLAYER_SIZE < mapx + TILE_SIZE * WORLD_WIDTH) {
mapx -= 1;
}
}
}

void gameplay() {
playerinput();
drawworld();
drawplayer();

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();
}
``````

Upload your code to your Arduboy and play around with it! If you want a challenge while waiting for the next tutorial, you should try to make a bigger map and put a cool design into it.

# Next Tutorial

This isn’t really a game, but we’ve got the hard part done so it’ll come along really quickly! In the next tutorial, we’ll draw our dinosaur, create playerstates, read the player’s position, create buildings, demolish buildings… and so much more!

# 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.

(Simon) #3

I love the enthusiasm in these articles - its infectious. There are no less the 48 exclamation marks in this article alone

Another great article. My only criticism is the use of a two dimensional tile array and the arduboy.drawBitmap() approach to rendering the world. It might have been a good time to introduce the Sprites class and the ‘frames’ concept.

(Holmes) #4

This game will be possible without the implementation of classes. The people following my tutorials don’t even know how to do sound or save to EEPROM, yet! I don’t want to scare anyone away!

(Pharap) #5

You don’t need to implement a class to use the `Sprites` class.

What @filmote is saying is that pretty much nobody uses 2D arrays of images with `arduboy.drawBitmap` anymore because the `Sprites` class supports image frames with all draw modes.

(Holmes) #6

By implement, I mean learn about and write out code that involves any new classes. They don’t need to learn how to code them or understand them. That is for later tutorials. As I said: `I don’t want to scare anyone away!`

I don’t think it’s a priority to teach them that, yet.

I do? Besides, it’s not about how popular something is, it’s about easing the beginner into learning more and more complex programming practices. There are kids that are 13-years old that are following along with my tutorials and fully understand them. That’s because I’m not rushing into teaching them every concept under the sun at the earliest opportunity. Those things will come!

#7

Please can you make a part ten.
Thanks.

(Holmes) #8

Worked on it all this past weekend! It is coming soon!

#10

ok thanks

#11

Which application do you you use to make these sprites?

(Holmes) #12

Microsoft Paint!

#13

Ok thanks a lot. Now I can get onto making my game.

(Cody) #14

Hey I’m back. This tutorial looks great I will look at the previous tomorrow

(JohnnydCoder) #15

Hey @crait!

I’ve been following your tutorials after getting an arduboy and I was wondering when the next tutorial will be posted.

When is soon?
Thanks!

(Daniel) #16

thanks for everything you do in this tutorials I really appreciated.

(T Malo) #17

I hope we can get a part 10 soon. You have a lot of people that have read your awesome lessons and many of us really hope to see part 10. Thanks

(Simon) #18

@crait some of these people might wish to enter the Jam and will desperately need Part 10 !

(T Malo) #19

wait, what is the Jam?

(Simon) #20

There is a Game Jam on starting tomorrow. To enter you need to build a game or an application that matches the theme in the allotted 10 days.

You can look at the entries for previous Jams at the top of this page >

https://community.arduboy.com/c/games

(Josh) #21

Can’t wait to finish this game! Thanks for all of the tutorials.