Make Your Own Arduboy Game: Part 9 - Mapping DinoSmasher


(Holmes) #1

Before You Start

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

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

If you haven’t done any of the previous tutorials, please be sure to do that before this one! Part 1, Part 2, Part 3, Part 4, Part 5, Part 6, 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 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!

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

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

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[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! :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:


(Simon) #3

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

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! :slight_smile: 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. :stuck_out_tongue: 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 :grinning:


#11

Which application do you you use to make these sprites?


(Holmes) #12

Microsoft Paint! :slight_smile:


#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?:grinning:
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

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