Make Your Own Arduboy Game: Part 9 - Mapping 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, 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: grass

WATER: water

Converting them using the TeamARG Image Converter (https://teamarg.github.io/arduboy-image-converter/) gives:

const unsigned char PROGMEM grass[] = {
// width, height,
16, 16,
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 PROGMEM water[] = {
// width, height,
16, 16,
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) {
        Sprites::drawOverwrite(x, y, grass, 0);
      }
      if(world[y][x] == 1) {
        Sprites::drawOverwrite(x, y, water, 0);
      }
    }
  }
}

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) {
        Sprites::drawOverwrite(x * TILE_SIZE, y * TILE_SIZE, grass, 0);
      }
      if (world[y][x] == 1) {
        Sprites::drawOverwrite(x * TILE_SIZE, y * TILE_SIZE, water, 0);
      }
    }
  }
}

There we go!

image

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 using a feature of the Sprites::drawOverwrite() function.

Let’s combine the water and grass data as shown below.

const unsigned char tiles[] PROGMEM  = {
// width, height,
16, 16,

//Grass
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, 

//Water
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, 
};

See how the data is constructed. The first two bytes of the array detail the width and height of the images. Following these two bytes are the data for the first image (the grass) and then the second image (the water).

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

Sprites::drawOverwrite(a, b, tiles, 0);  // Will draw grass
Sprites::drawOverwrite(c, e, tiles, 1);  // Will draw water

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! 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++) {
      Sprites::drawOverwrite(x * TILE_SIZE, y * TILE_SIZE, tiles, world[y][x]);
      
    }
  }
}

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[] PROGMEM  = {
// width, height,
16, 16,

//Grass
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, 

//Water
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++) {
      Sprites::drawOverwrite(x * TILE_SIZE, y * TILE_SIZE, tiles, world[y][x]);
      
    }
  }
}

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: trees
STONE: rock

I converted the images with the TeamARG Image Converter and then added them to the tiles array. The tree image should be the third and stone image should be the fourth image.

const unsigned char tiles[] PROGMEM  = {
// width, height,
16, 16,

//Grass
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, 

//Water
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, 

//Tree
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, 

//Stone
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. :slight_smile:

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[] PROGMEM  = {
// width, height,
16, 16,

//Grass
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, 

//Water
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, 

//Tree
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, 

//Stone
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    20
#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++) {
      Sprites::drawOverwrite(x * TILE_SIZE, y * TILE_SIZE, tiles, world[y][x]);
      
    }
  }
}

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

image

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[] PROGMEM  = {
// width, height,
16, 16,

//Grass
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, 

//Water
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, 

//Tree
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, 

//Stone
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++) {
      Sprites::drawOverwrite(x * TILE_SIZE + mapx, y * TILE_SIZE + mapy, tiles, world[y][x]);
      
    }
  }
}

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++) {
      Sprites::drawOverwrite(x * TILE_SIZE + mapx, y * TILE_SIZE + mapy, tiles, world[y][x]);     
    }
  }
}

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. The modulo operator could be used to do this. The modulo function returns the remainder of a division operation. For example 12 divided by 3 returns a remainder of 0 whereas 13 divided by 3 returns 1.

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++) {
      Sprites::drawOverwrite(x * TILE_SIZE + mapx % 10, y * TILE_SIZE + mapy % 10, tiles, world[y][x]);
      
    }
  }
}

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!

2 Likes

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[] PROGMEM  = {
// width, height,
16, 16,

//Grass
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, 

//Water
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, 

//Tree
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, 

//Stone
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++) {
      Sprites::drawOverwrite(x * TILE_SIZE + mapx % TILE_SIZE, y * TILE_SIZE + mapy % TILE_SIZE, tiles, world[y][x]);
      
    }
  }

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

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 Sprites::drawOverwrite() 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;
      Sprites::drawOverwrite(x * TILE_SIZE + mapx % TILE_SIZE, y * TILE_SIZE + mapy % TILE_SIZE, tiles, world[tiley][tilex]);
      
    }
  }

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.

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[] PROGMEM  = {
// width, height,
16, 16,

//Grass
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, 

//Water
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, 

//Tree
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, 

//Stone
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;
      Sprites::drawOverwrite(x * TILE_SIZE + mapx % TILE_SIZE, y * TILE_SIZE + mapy % TILE_SIZE, tiles, world[tiley][tilex]);
    }
  }

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

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) {
  Sprites::drawOverwrite(x * TILE_SIZE + mapx % TILE_SIZE, y * TILE_SIZE + mapy % TILE_SIZE, tiles, world[tiley][tilex]);
}

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;

int mapx = 0;
int mapy = 0;

const unsigned char tiles[] PROGMEM  = {
// width, height,
16, 16,

//Grass
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, 

//Water
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, 

//Tree
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, 

//Stone
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) {
        Sprites::drawOverwrite(x * TILE_SIZE + mapx % TILE_SIZE, y * TILE_SIZE + mapy % TILE_SIZE, tiles, world[tiley][tilex]);
      }
    }
  }

  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)) {
    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! :slight_smile: 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. :smile:

7 Likes

This is so awesome, thank you. Instead of wracking my brain trying to figure out exactly how drawing the world works, could you elaborate a little bit on that drawworld() function?

Specifically the part where you’re plotting the x and y coordinates in the drawOverwrite function. I get the x * TILE_SIZE part, it’s basically moving to the right every 16 pixels to plot out the next tile. It’s the buffer part that stumps me though, when you’re adding + mapx % TILE_SIZE. What exactly is this determining? How many tiles to draw on the opposite side of the player?

For example, when mapx is 116, 116 mod 16 = 4. How is that addition of 4 helping to determine which tiles to draw?

It has nothing to do with which tiles are drawn,
but rather where on the screen those tiles are drawn.


Doing:

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

Draws the map flat onto the screen with no offset.
The player character can walk around and the tiles will change,
but the map as drawn on the screen never moves.

Using + mapx and + mapy offsets the map so it moves when the player presses the directional buttons,
but it ends up drawing part of the map offscreen once it gets past a certain amount.

Using % TILE_SIZE limits mapx and mapy to be between 0 and -15,
so instead of drawing the map offscreen it only ever offsets the map by a few pixels.

The important thing to remember is that the loop isn’t drawing the whole map, it’s only drawing part of the map.
x * TILE_SIZE + mapx % TILE_SIZE and y * TILE_SIZE + mapy % TILE_SIZE aren’t map coordinates - they’re screen coordinates.

It would be a lot easier to understand what’s going on if you could see what would get drawn ‘beyond the screen’ through this technique.
Unfortunately you can’t see that because there’s no screen to draw to beyond the screen boundaries,
however what you can do is shrink the part of the map being drawn to get a similar effect…

Try replacing drawworld with this:

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)
				Sprites::drawOverwrite(TILE_SIZE + x * TILE_SIZE + mapx % TILE_SIZE, TILE_SIZE + y * TILE_SIZE + mapy % TILE_SIZE, tiles, world[tiley][tilex]);
		}

	arduboy.fillRect(0, 0, 48, 8, BLACK);
	arduboy.setCursor(0, 0);
	arduboy.print(mapx);
	arduboy.print(",");
	arduboy.print(mapy);
	arduboy.print('\n');
	arduboy.print(mapx % TILE_SIZE);
	arduboy.print(",");
	arduboy.print(mapy % TILE_SIZE);
}

Essentially I’ve changed

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

To

const int tileswide = WIDTH / TILE_SIZE - 1;
const int tilestall = HEIGHT / TILE_SIZE - 1;

Which decreases the number of tiles drawn on each axis by 2,
and changed this

x * TILE_SIZE + mapx % TILE_SIZE, y * TILE_SIZE + mapy % TILE_SIZE

to this

TILE_SIZE + x * TILE_SIZE + mapx % TILE_SIZE, TILE_SIZE + y * TILE_SIZE + mapy % TILE_SIZE

so I’m offsetting the drawing by the size of one whole tile.
It means the player rectangle is a bit out of sync, but that’s not important.
What’ important is that if you run it you’ll see how the map keeps ‘snapping back’ to a previous position.
That ‘snapping back’ is caused by the modulo limiting the offset to 0 to -15 pixels.

I’ve also added some code to write mapx & TILE_SIZE and mapy % TILE_SIZE on the screen so you can see how the values count from 0 to -15 over and over again.

Also, try removing the + mapx % TILE_SIZE and + mapy % TILE_SIZE afterwards.
You’ll see the tiles change, but the actual map as drawn on the screen never moves.

Hopefully seeing that will help you understand what’s going on.
(If not I could draw a diagram that labels all the measurements but then I’d have to fish out the scanner.)

1 Like

Thank you so much for this. Shrinking the size of the screen did make it clearer, since I was confused about how the actual drawing was happening. And I think the modulo makes more sense now - it’s causing the smoother effect of the map moving because it’s keeping the offset smaller than mapx and mapy? Or am I still thinking of that wrong?

And when you mention “drawing part of the map offscreen”, do you mean actually drawing tiles that you can’t see on the screen or removing tiles that are no longer within the screen view?

No need to bust out your scanner, I’m sure I’ll get this after reading through your post and dissecting it some more.

Sort of.

The non-smooth map moving is what happens when the tiles are drawn to the same location on screen every time.

The smooth map moving is what happens when the tiles are offset by a value from 0 to -15.
(The offset is the remainder of the map offset when divided by the tile size.)

If mapx and mapy were positive then they’d represent the player’s position in world coordinates and mapx % tileWidth and mapy % tiley would actually be the player’s offset into the particular tile that they’re standing on, but crait has chosen the approach of moving the map rather than moving the player.

I mean that technically the code tries to draw tiles (or part of the tiles) outside of the screen’s boundaries,
but obviously it can’t actually draw outside the screen because there’s nothing to draw to,
(it’s like a painter trying to paint outside his canvas - he just ends up waving his brush in the air)
but the Sprites code accounts for that and ignores pixels that would be drawn ‘outside the screen’.

I had a quick go anyway:

So you can see the + mapx % TILE_SIZE part actually causes the map to start being rendered a few pixels outside the top left corner of the screen.

If it were just + mapx then after the first tile the map representation would end up being drawn further and further outside the screen until it wasn’t actually being rendered on-screen at all.

The diagram on the left is supposed to illustrate how the code isn’t actually drawing the whole map,
it’s looking at a rectangular section and only drawing the tiles touched by that section.

(The grids are really imprecise because I’m too impatient to draw accurate grids, and I couldn’t be bothered to grab my graph paper.)

I also found something online that might help to illustrate how the screen is only rendering a portion of the map:

There’s another thread on here where I discussed tile rendering in detail,
I can dig that out too if your interested in tile maps,
though I’m now sure how much it would answer your question.

Edit:

It was this thread:

I don’t directly explain everything, but the diagrams might be useful for understanding tile rendering.

Thank you! It’s making more sense. One of the diagrams you drew in that other thread you linked to is really helpful. I’m going to digest it some more and I’ll be back with more questions, if I still can’t get it straight.

At the end of the day I understand what it’s doing, I’m just struggling to understand exactly (mathematically) what every number in this function is doing. Maybe that isn’t necessary.

2 Likes

The important thing to remember is that you’re working with multiple coordinate systems.

x * TILE_SIZE and y * TILE_SIZE are screen coordinates.
mapx and mapy are map offsets in world coordinates.
Then % TILE_SIZE is used to get the player’s offset within a tile (sort of),
which is then used to offset the rendered representation of the world (the map is not the territory).

You could try working out the numbers on paper (maybe with a calculator) to see how they behave.
Often just stepping through the code manually makes it easier to understand, and that’s probably the closest you can get tp being able to do that.

You could also try this:

constexpr uint8_t 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)
			{
				Sprites::drawOverwrite(x * TILE_SIZE + mapx % TILE_SIZE, y * TILE_SIZE + mapy % TILE_SIZE, tiles, world[tiley][tilex]);
			}
		}
	}

	arduboy.fillRect(0, 0, 48, 8, BLACK);
	arduboy.setCursor(0, 0);
	
	// Map coordinates
	arduboy.print(mapx);
	arduboy.print(',');
	arduboy.print(mapy);
	arduboy.print('\n');
		
	// Map coordinate remainder
	arduboy.print(mapx % TILE_SIZE);
	arduboy.print(',');
	arduboy.print(mapy % TILE_SIZE);
	arduboy.print('\n');
	
	// Render coordinate without modulo
	arduboy.print(1 * TILE_SIZE + mapx);
	arduboy.print(',');
	arduboy.print(1 * TILE_SIZE + mapy % TILE_SIZE);
	arduboy.print('\n');
		
	// Render coordinate with modulo
	arduboy.print(1 * TILE_SIZE + mapx % TILE_SIZE);
	arduboy.print(',');
	arduboy.print(1 * TILE_SIZE + mapy % TILE_SIZE);
	arduboy.print('\n');
}

Smasher.hex (26.4 KB)

Hopefully you’ll be able to see that not using the modulo increases the size indefinitely, while using the modulo just provides the 0 to -15 offset (the player’s offset into the tile they’re currently on).

2 Likes

I’m getting this now :). I did working through this stuff manually but was doing something wrong with the modulo, so it wasn’t matching up with the numbers being printed. With all these numbers being printed it started making sense, I was doing the process wrong with the additions and modulo stuff. I appreciate your patience.

1 Like

If you’re on Windows, the calculator actually has a Mod button (in both scientific and programmer mode).

It’s a shame regular calculators don’t tend to have it.

Ah, ok, that makes sense.

No problem.
Answering people’s questions and explaining things is something I do a lot.
If you have any more questions, don’t hesitate to ask.

I still think you missed your calling in life (a teacher!). Actually, its not too late.

I would, but it would mean spending N years getting teaching qualifications,
and then I’d have to mark students’ work and put up with their bad behaviour,
and try to make the students not hate me.

Maybe I’ll become a tutor instead. (£20-£40 an hour is a nice incentive.)

4 Likes

5 posts were split to a new topic: Tile Collisions?