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.
- Converting & drawing some images
- Formatting our world’s images
- Moving the map
- Cropping the map
- Cycling the map
- Bounding the map
Our Current Code
//DinoSmasher
#include <Arduboy2.h>
Arduboy2 arduboy;
#define GAME_TITLE 0
#define GAME_PLAY 1
#define GAME_OVER 2
#define GAME_HIGH 3
int gamestate = GAME_TITLE;
#define WORLD_WIDTH 20
#define WORLD_HEIGHT 4
int world[WORLD_HEIGHT][WORLD_WIDTH] = {
{ 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1 },
{ 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0 },
{ 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0 }
};
void drawworld() {
for (int y = 0; y < WORLD_HEIGHT; y++) {
for (int x = 0; x < WORLD_WIDTH; x++) {
arduboy.print(world[y][x]);
}
arduboy.print("\n");
}
}
void titlescreen() {
arduboy.setCursor(0, 0);
arduboy.print("Title Screen\n");
if (arduboy.justPressed(A_BUTTON)) {
gamestate = GAME_PLAY;
}
}
void gameplay() {
arduboy.setCursor(0, 0);
arduboy.print("Gameplay\n");
drawworld();
if (arduboy.justPressed(A_BUTTON)) {
gamestate = GAME_OVER;
}
}
void gameoverscreen() {
arduboy.setCursor(0, 0);
arduboy.print("Game Over Screen\n");
if (arduboy.justPressed(A_BUTTON)) {
gamestate = GAME_HIGH;
}
}
void highscorescreen() {
arduboy.setCursor(0, 0);
arduboy.print("High Score Screen\n");
if (arduboy.justPressed(A_BUTTON)) {
gamestate = GAME_TITLE;
}
}
void gameloop() {
switch(gamestate) {
case GAME_TITLE:
titlescreen();
break;
case GAME_PLAY:
gameplay();
break;
case GAME_OVER:
gameoverscreen();
break;
case GAME_HIGH:
highscorescreen();
break;
}
}
void setup() {
arduboy.begin();
arduboy.setFrameRate(45);
arduboy.display();
arduboy.initRandomSeed();
arduboy.clear();
}
void loop() {
if(!(arduboy.nextFrame())) {
return;
}
arduboy.pollButtons();
arduboy.clear();
gameloop();
arduboy.display();
}
1. Converting & drawing some images
In the previous tutorial, we made the world
2D array out of 0’s and 1’s. Instead of drawing those numbers, let’s draw pictures, instead! Where we see a 0, let’s draw some grass, and where we see a 1, let’s draw some water!
Our maps will be made up of 16x16 images. Here are two sprites that I’ve made for us to use. They are 16x16 pixels.
GRASS:
WATER:
Converting them using 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!
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:
STONE:
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.
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();
}
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!