Skip to content

How to Develop a Game for the Tiny Arcade


The Goal

Our goal for this tutorial is to show you how to develop a simple game for the Tiny Arcade that is written entirely in the Arduino IDE. While many of our other games have been developed using game engines, it is outside the scope of this tutorial.

Without the use of other game engines, some tasks may become more tedious or time-consuming in this case, but the barrier to entry is much lower, allowing users with minimal coding knowledge to begin experimenting with game development in the C language.

We'll start with some basics and concepts (we are assuming you have some basic experience with Arduino, but if not, you may want to check out some beginner tutorials), then work our way up to developing a LITE version of Tiny Brick. Here's an outline of what we'll go over:

  • Materials
  • Background: Using Bootloader and Connecting Hardware to Arduino IDE
  • Programming Basics and Tiny Brick Beginning
  • Adding Bitmap Sprites
  • Updating the Screen
  • Upload Code
  • Adding Controls & Motion
  • Implementing Collision Detection
  • Collisions Between Sprites
  • From Concepts to Tiny Brick
  • Final Game-play Details
  • Limits of Tiny Brick LITE
  • Exporting Binary files from the Arduino IDE
  • Links

Materials

Hardware

  • A TinyDuino Processor Board
    • TinyScreen+ or Tiny Arcade (this tutorial will be specific to the TinyArcade in most instances, but there are some software changes to interface with the TinyScreen+ hardware that would make this applicable to the TinyScreen+)
  • Micro USB Cable
  • Optional: Battery

Software

You will also need the willpower to get through this long tutorial. You can do it!


Background

  • If this is your first time using a Tiny Arcade or a TinyScreen+, follow this tutorial for information on how to set up the boards and libraries needed to work with your Tiny Arcade.
  • Here's a Tiny Arcade tutorial for uploading games if you want to try some other games after you make your own!
  • The screens used by TinyCircuits have a different-than-you'd-expect pixel assignment that is worth a note. The origin (0,0) point on the screen is in the top left corner, so with a screen size of 96x64, the bottom right corner is the point (95, 63).
  • Booting/Bootloader: The main fix to any problems you may run into with your Tiny processor board is putting the board into Bootloader mode. In this case, we want to use the Bootloader mode to make sure the TinyArcade or TinyScreen+ is visible to the Arduino IDE so that we can program it. Later on while using the board, some software may crash the board (which will appear as an unresponsive/broken board) and you will have to reset it with the bootloader. The 'Upload' section will give more details on putting a processor into this mode.

Programming Basics and Tiny Brick Beginning

At this point, you will need to have the Arduino IDE downloaded and opened so we can start programming our Tiny Brick game. The Arduino IDE when opened will normally include a few lines, but we can ignore those lines for now.

I will include the entire program here so you can see where we're going, but we will break it down step by step as we go so you can understand how to make your own games in the future from empty file to full functionality:

Game Tutorial Arduino Sketch
//**************************************************************
// Tiny Arcade Game Tutorial - Tiny Brick LITE
// Written by: Brandon Farmer for TinyCircuits
// Initiated:  Mon. 3/27/2017 @  4:00pm
//   Updated: Thur. 3/30/2017 @  2:30pm
//**************************************************************
#include <TinyScreen.h>
#include <Wire.h>
#include <SPI.h>
#include "TinyArcade.h"
#include "GameTutorialSprites.h"

TinyScreen display = TinyScreen(TinyScreenPlus);

typedef struct {
  int x;
  int y;
  int width;
  int height;
  int collisions;
  const unsigned int *bitmap;
} ts_sprite;

ts_sprite ball = {offscreen, offscreen, 4, 4, 0, ballBitmap};
ts_sprite platform = {41, 56, 14, 4, 0, platformBitmap};
ts_sprite gameLogo = {offscreen, offscreen, 94, 21, 0, tinyBrickLogoBitmap};
ts_sprite lives = {offscreen, offscreen, 16, 2, 0, livesBitmap};

ts_sprite redBrick1 = { 1,  1, 10, 4, 0, redBrickBitmap};
ts_sprite redBrick2 = {13,  1, 10, 4, 0, redBrickBitmap};
ts_sprite redBrick3 = {25,  1, 10, 4, 0, redBrickBitmap};
ts_sprite redBrick4 = {37,  1, 10, 4, 0, redBrickBitmap};
ts_sprite redBrick5 = {49,  1, 10, 4, 0, redBrickBitmap};
ts_sprite redBrick6 = {61,  1, 10, 4, 0, redBrickBitmap};
ts_sprite redBrick7 = {73,  1, 10, 4, 0, redBrickBitmap};
ts_sprite redBrick8 = {85,  1, 10, 4, 0, redBrickBitmap};

ts_sprite yelBrick1 = { 1,  7, 10, 4, 0, yellowBrickBitmap};
ts_sprite yelBrick2 = {13,  7, 10, 4, 0, yellowBrickBitmap};
ts_sprite yelBrick3 = {25,  7, 10, 4, 0, yellowBrickBitmap};
ts_sprite yelBrick4 = {37,  7, 10, 4, 0, yellowBrickBitmap};
ts_sprite yelBrick5 = {49,  7, 10, 4, 0, yellowBrickBitmap};
ts_sprite yelBrick6 = {61,  7, 10, 4, 0, yellowBrickBitmap};
ts_sprite yelBrick7 = {73,  7, 10, 4, 0, yellowBrickBitmap};
ts_sprite yelBrick8 = {85,  7, 10, 4, 0, yellowBrickBitmap};

ts_sprite grnBrick1 = { 1, 13, 10, 4, 0, greenBrickBitmap};
ts_sprite grnBrick2 = {13, 13, 10, 4, 0, greenBrickBitmap};
ts_sprite grnBrick3 = {25, 13, 10, 4, 0, greenBrickBitmap};
ts_sprite grnBrick4 = {37, 13, 10, 4, 0, greenBrickBitmap};
ts_sprite grnBrick5 = {49, 13, 10, 4, 0, greenBrickBitmap};
ts_sprite grnBrick6 = {61, 13, 10, 4, 0, greenBrickBitmap};
ts_sprite grnBrick7 = {73, 13, 10, 4, 0, greenBrickBitmap};
ts_sprite grnBrick8 = {85, 13, 10, 4, 0, greenBrickBitmap};

int backgroundColor = TS_16b_Black;
unsigned int frame = 0;
int ballDirection = 2;
int score = 0;
bool start = 0;
int gameLives = 3;
const uint16_t ALPHA = 0x1111;
int offscreen = -100;

int numBricks = 24;
int amtSprites = 28;
ts_sprite * spriteList[28] = {&redBrick1, &redBrick2, &redBrick3, &redBrick4, &redBrick5, &redBrick6, &redBrick7, &redBrick8,
                              &yelBrick1, &yelBrick2, &yelBrick3, &yelBrick4, &yelBrick5, &yelBrick6, &yelBrick7, &yelBrick8,
                              &grnBrick1, &grnBrick2, &grnBrick3, &grnBrick4, &grnBrick5, &grnBrick6, &grnBrick7, &grnBrick8,
                              &ball, &platform, &gameLogo, &lives,
                             };


unsigned long frameStart = 0;
unsigned long frameTime = 18;

void setup() {
  arcadeInit();
  display.begin();
  display.setBitDepth(TSBitDepth16);
  display.setBrightness(15);
  display.setFlip(false);

  USBDevice.init();
  USBDevice.attach();
  SerialUSB.begin(9600);
}

void loop() {
  unsigned long timer = millis();                          
  while (millis() - frameStart < frameTime) {              
    delay(1);                                      
  }
  frameStart = millis();                                    
  showLogo();
  drawBuffer();
  frame++;
  movePlatform();
  checkPushbuttons();
  moveBall();
  ballWallCollision();
  ballBrickCollision();
  ballPlatformCollision();
  if (score == numBricks) resetBricks();
  if (gameLives == 0) resetBricks();
  timer = millis() - timer;  
}

void drawBuffer() {                                                             //Routine for drawing the pixel data to the screen
  uint8_t lineBuffer[96 * 64 * 2];                                              //8-bit integer named lineBuffer, equivalent to twice the number of pixels comprising the OLED (For 16-bit colors?)
  display.startData();                                                          //Activates the OLED driver chip to ready it for receiving commands (drives CS line HIGH)
  for (int y = 0; y < 64; y++) {                                                //Increments through the y-axis of the screen pixel-by-pixel
    for (int b = 0; b < 96; b++) {                                              //Increments throught the x-axis of the screen pixel-by-pixel. (Nested for loop using variable b)
      lineBuffer[b * 2] = backgroundColor >> 8;                                 //Loads the lineBuffer variable byte by byte (>>8 is bitshift Right 8 bits, or, load bits, then move them 8 positions over)
      lineBuffer[b * 2 + 1] = backgroundColor;                                  //Because we start at 0, we must also ensure to set the very last bit to the specified color
    }
    for (int spriteIndex = 0; spriteIndex < amtSprites; spriteIndex++) {        //Increment through each sprite and draw them to the screen
      ts_sprite *cs = spriteList[spriteIndex];                                  //The pointer (named cs) will store the ADDRESSES of the spriteList items
      if (y >= cs->y && y < cs->y + cs->height) {                               //This if statement compares the active pixel's y value to the y values of the active sprite
        int endX = cs->x + cs->width;                                           //The integer named endX is set equal to the active sprite's xValue+Width (The xValue on the rightmost side of the Sprite)
        if (cs->x < 96 && endX > 0) {                                           //This if statement checks to see if the active sprite's left side is on the screen and if the active sprite's right side is on the screen (No part is off screen)
          int xBitmapOffset = 0;                                                //Initializes the variable named xBitmapOffset equal to 0
          int xStart = 0;                                                       //Initializes the variable named xStart equal to 0
          if (cs->x < 0) xBitmapOffset -= cs->x;                                //If the x coordinate value of the active sprite is less than 0, set xBitmapOffset equal to itself - the x coordinate value of the active bitmap
          if (cs->x > 0) xStart = cs->x;                                        //If the x coordinate value of the active sprite is greater than 0, set xStart equal to the x coordinate value of the active bitmap
          int yBitmapOffset = (y - cs->y) * cs->width;                          //Initializes the variable named yBitmapOffset to (the yValue of the active pixel - the y-coordinate value of the active sprite * the width of the active sprite)
          for (int x = xStart; x < endX; x++) {                                 //Draws the screen starting at the lowest x-coordinate value and ending at the greatest x value
            unsigned int color = cs->bitmap[xBitmapOffset + yBitmapOffset++];   //The color to write to the active pixel is equal to the color of that pixel of the active sprite
            if (color != ALPHA) {                                               //As long as the color is NOT ALPHA, write that color to the lineBuffer byte by byte. ALPHAs will end up being the background color
              lineBuffer[(x) * 2] = color >> 8;                                 //Load byte, shift right, load byte, shift right, and so on...
              lineBuffer[(x) * 2 + 1] = color;                                  //Write the very last pixel to its specified color
            }
          }
        }
      }
    }
    display.writeBuffer(lineBuffer, 96 * 2);                                    //Write all of the pixel data (saved in lineBuffer) to the screen (update what is being displayed on screen)
  }
  display.endTransfer();                                                        //Deactivate the OLED driver chip now that it has been updated (drives CS pin LOW)
}

void showLogo() {
  if (!start) {
    gameLogo.x = -94; gameLogo.y = 23;
    for (int i = 0; i <= 94; i++) {
      gameLogo.x += 1;
      drawBuffer();
    }
    while (!checkButton(TAButton1)) {
      delay(1);
    }
    for (int i = 0; i <= 100; i++) {
      gameLogo.x += 1;
      drawBuffer();
    }
    start = 1;
    ball.x = random(40, 48);
    ball.y = random(38, 46);
    ballDirection = 2;
    lives.x = 89; lives.y = 61;
    drawBuffer();
    delay(100);
  }
}

void restart() {
  if (gameLives > 0) {
    platform.x = 41; platform.y = 56;
    start = 1;
    ball.x = random(40, 48);
    ball.y = random(38, 46);
    ballDirection = 2;
    drawBuffer();
    delay(1000);
  }
}

void movePlatform() {
  //Gets Joystick Data and moves platform accordingly
  if ((platform.x + platform.width) < 95) {
    if (checkJoystick(TAJoystickRight)) platform.x += 1;
  }
  if (platform.x > 0) {
    if (checkJoystick(TAJoystickLeft))  platform.x -= 1;
  }
}

void checkPushbuttons() {
  //Gets Pushbutton Data, and resets brick locations and game variables
  //if (checkButton(TAButton1)) ball.x = 0;
  if (checkButton(TAButton2)) resetBricks();
}

#define zmax(a,b) ((a)>(b)?(a):(b))
#define zmin(a,b) ((a)<(b)?(a):(b))

bool testBitmapCollision(ts_sprite *s1, ts_sprite *s2) {
  if ((s1->x < s2->x + s2->width) && (s1->x + s1->width > s2->x))
    if ((s2->y < s1->y + s1->height) && (s2->y + s2->height > s1->y))
      return true;
  return false;
}

bool testPixelCollision(ts_sprite *s1, ts_sprite *s2) {
  if (!testBitmapCollision(s1, s2))return false;
  int startX = zmax(s1->x, s2->x);
  int endX = zmin(s1->x + s1->width, s2->x + s2->width);
  int startY = zmax(s1->y, s2->y);
  int endY = zmin(s1->y + s1->height, s2->y + s2->height);
  for (int y = startY; y < endY; y++) {
    for (int x = startX; x < endX; x++) {
      if (s1->bitmap[(y - s1->y)*s1->width + (x - s1->x)] != ALPHA && s2->bitmap[(y - s2->y)*s2->width + (x - s2->x)] != ALPHA)
        return true;
    }
  }
  return false;
}

void ballBrickCollision() {
  for (int i = 0; i < numBricks; i++) {
    ts_sprite *brick = spriteList[i];
    if (testPixelCollision(&ball, brick)) {
      brick->x = offscreen;
      score += 1;
      ballBrickBounce(brick);
    }
  }
}

void ballBrickBounce(ts_sprite *brick) {
  if (ballDirection == 0) ballDirection = 1;
  if (ballDirection == 1) ballDirection = 0;
  if (ballDirection == 2) ballDirection = 1;
  if (ballDirection == 3) ballDirection = 0;
}

void moveBall() {
  if (ballDirection == 0) {
    ball.x -= 1;
    ball.y += 1;
  } else if (ballDirection == 1) {
    ball.x += 1;
    ball.y += 1;
  } else if (ballDirection == 2) {
    ball.x += 1;
    ball.y -= 1;
  } else if (ballDirection == 3) {
    ball.x -= 1;
    ball.y -= 1;
  }
}

void ballWallCollision() {
  if (ball.x <= 0 && ballDirection == 0) ballDirection = 1;
  else if (ball.x <= 0 && ballDirection == 3) ballDirection = 2;
  else if ((ball.x + ball.width) >= 95 && ballDirection == 1) ballDirection = 0;
  else if ((ball.x + ball.width) >= 95 && ballDirection == 2) ballDirection = 3;
  else if (ball.y <= 0 && ballDirection == 2) ballDirection = 1;
  else if (ball.y <= 0 && ballDirection == 3) ballDirection = 0;
  else if (ball.y >= 64) {
    gameLives -= 1;
    lives.x += 4;
    restart();
  }
  //else if ((ball.y + ball.height) >= 63 && ballDirection == 0) ballDirection = 3;
  //else if ((ball.y + ball.height) >= 63 && ballDirection == 1) ballDirection = 2;
}

void ballPlatformCollision() {
  if ( ((ball.y + ball.height) == (platform.y)) && ((ball.x <= platform.x + platform.width) && (ball.x + ball.width >= platform.x)) ) {
    if (ballDirection == 0) ballDirection = 3;
    if (ballDirection == 1) ballDirection = 2;
  }
}

void resetBricks() {
  ball.x = offscreen;
  lives.x = offscreen;
  platform.x = 41; platform.y = 56;
  gameLives = 3;
  score = 0;
  start = 0;
  int rstX[] = {1, 13, 25, 37, 49, 61, 73, 85};
  int rstY[] = {1, 7, 13};
  for (int i = 0; i <= numBricks; i++) {
    ts_sprite *rst = spriteList[i];
    if (i <= (numBricks / 3) - 1) {
      rst->x = rstX[i];
      rst->y = rstY[0];
    } else if (i >= (numBricks / 3) && i <= (2 * (numBricks / 3)) - 1) {
      rst->x = rstX[i - (numBricks / 3)];
      rst->y = rstY[1];
    } else if (i >= (2 * (numBricks / 3)) && i <= (numBricks - 1)) {
      rst->x = rstX[i - (2 * (numBricks / 3))];
      rst->y = rstY[2];
    }
  }
}

Typical Arduino Coding Standards recommend a comment at the top of each program you write including your name, the date, and a small description of what the code does.

Initial comment of code

I follow this same format each time I write code, but you should use whatever comment style works for you!

Libraries

After these comment lines, we will want to include some libraries: the TinyScreen library, Wire library, and SPI library. These libraries can be downloaded in the Arduino IDE Library Manager (Click 'Sketch' -> 'Include Library' -> 'Manage Libraries...' -> and then type in the name of the library in the search bar and click 'Install').

Header Files

We'll also need some header files specific to this program that we intend to use and create: TinyArcade.h and GameTutorialSprites.h. Click here to download TinyArcade.h, and copy it into the same folder as your .ino sketch program. GameTutorialSprites.h will be a file we create. To create a file you'll click on the arrow to the far right of your tab bar:

Create a new file: Press the down arrow to the right of all your tabs and select "New Tab." Then look for a yellow prompt toward the bottom of the IDE to name the file "GameTutorialSprites.h" and click "Ok" to add the file tab.

Including

The image below shows how to reference your header files and your libraries. You should be able to see the included header files in the tabs next to your main program at the top of the editing environment. If you cannot see the tabs of your header files, you may need to close and reopen your Arduino IDE.

How to include header files and libraries

Game Variables

Once all of the libraries and header files are set up, we'll start by defining any extra pieces we'll need throughout our game code and our setup routine. We'll be defining a type that holds information for a sprite, a 2-D image that is a part of the graphics for our game. This type needs to hold the information of the sprite's pixels, the dimensions of the sprite, the sprite's initial position coordinates, and any other information that may be relevant to gameplay. We'll be using a struct to hold this information.

Data structure that holds pieces of data relevant to a sprite for Tiny Brick

For the Tiny Brick sprite type, we'll define our sprite's initial x and y coordinates, its height and width, the number of times another sprite has collided with it, and an "unsigned" int that will reference a map in GameTutorialSprites.h used to generate the sprite. We will name this type ts_sprite, which comes at the end as shown above. We can now use the type name ts_sprite to make a sprite in our code.

We'll then specify our screen as the TinyScreen+ and create the display object.

In the setup routine, we'll configure the screen settings and initialize the processor's serial port. This will allow us to access the port directly and print Serial data to the Serial Monitor during code execution. This will also allow us to program the Tiny Arcade without putting it into bootloader mode each time we want to update the program.

At this point, you should have the following:

Steps covered so far: Library and Header files, Screen variable, Sprite type, and setup() steps


Adding Bitmap Sprites

Click on the GameTutorialSprites.h tab to open and edit it. We will need to include the TinyScreen library in our header file in order to use the predefined variables in this header file. We will declare ALPHA as an external variable, which we will use as a placeholder for transparent or "empty" pixels. Next, we'll create our first bitmap! We are going to make a simple ball using white pixels.

We want our ball to look like the image below. Each pixel has been labeled with its color.

Pixelated ball

In order to code this into our game, we have to turn these pixels into an array of colors. The code below demonstrates how to do that.

The image above shows what we now have in our sprites header file. The ballBitmap is a linear array of colors, arranged how we would like the ball to appear in the game using line breaks. Let's go back to the main sketch and add the code to make the ball appear on the screen!

NOTE: You can find our TinyScreenReferenceManual online at GitHub. In it, you'll find useful information about the library's functions and how to use them in your code. The last few pages also include the pre-defined colors that you can use in creating bitmaps/sprites. You can also declare your own 8-bit or 16-bit colors.


Updating the Screen

In order to manipulate our screen image, we'll need to update the screen each time we want our graphics to move. For this task, we'll use the drawBuffer() routine that was used in our FlappyBirdz game.

The drawBuffer() routine uses pointers to increment through the sprites and draws the screen bit by bit, line by line from the top to the bottom. It also finds the pixels named ALPHA and assigns them the same color as the background, essentially making them transparent.

Now that we have a bitmap to draw and a method for drawing it, let's complete the task of declaring the sprite in our main code. Above the setup routine, we will declare our sprite named ball. Just like a variable, it has a type (ts_sprite), a name (ball) and a value or values. As described above, these values represent that the ball's initial location will be at (44, 28) on the screen, its bitmap is 4 pixels wide and 4 high, it hasn't collided with any other bitmaps, and is drawn by using ballBitmap[] from the header file. (To declare any other sprites, we would use this same list of variables in the same order between curly braces.)

The variable amtSprites will need to be set equal to the number of sprites used in the code. For now, we only have 1. As we add more sprites, we will need to update the value of this variable.

The next variable, *spriteList[ ], is an array containing the addresses of each of our sprites (the & symbol indicates an address). The drawBuffer() function will use each array to draw the sprite the respective array references on the screen. To use drawBuffer() in our game, we add it to our loop() routine as shown above.

I have also added another variable named backgroundColor, which we can use to change the background color of the screen.


Upload Code

With our basic variables and setup complete, we can upload the code to our TinyArcade.

IF IT'S YOUR FIRST TIME UPLOADING TO A TINYARCADE OR TINYSCREEN+:

Put your respective processor into bootloader mode:

  • TinyArcade: Plug your arcade into a USB port on your PC, then hold down both push-buttons (next to the joystick) while powering on the unit. A black screen reading "TinyArcade Bootloader Menu" should appear.
  • TinyScreen+: Plug the USB cable into the TinyScreen+ and your computer. Then press and hold the button closest to the USB connector (top left) while sliding the switch to the ON position. (For possible issues later on: Try uploading your program to the TinyScreen+ after booting and it should work. You may need to try this several times if it does not work the first time.)

Once you see the bootloader screen, you can stop pressing any buttons. Your processor should now be visible to the Arduino IDE after booting and have an assigned COM port under 'Tools'. (If you have issues finding your port, this article may be helpful. )

In order to upload, your selected "Programmer" should be "Arduino as ISP" as shown below and the Board should reference the name of your processor.

First Port connection from TinyArcade after booting

With all the right selections made, you can click the right arrow button at the top of the IDE to upload the program to your screen. If the upload is successful, you should see a small white ball in the center of a black screen. Nothing too exciting, but now we can begin to do the fun work of making things move, collide, deflect, etc.

NOTE: Bootloading should only be necessary during the first programming. After that, the USB port on the chip will be enabled and we can reprogram our TinyArcade without putting it into bootloader mode each time. But remember, you can use the bootloader mode to reset the board if it seems to lock up.


Adding Controls & Motion

Now that we know we can create and display a sprite image, let's make it move around on the screen! To do this, we will need to read the inputs from the joystick and the two push-buttons.

These inputs have already been configured by arcadeInit(), which was added to the setup routine. If we open the TinyArcade.h header file, we can see that the different inputs have assigned names that we can use to read data along with functions to make this step easier.

Joystick and Button variables from TinyArcade.h

NOTE: The header file can be adjusted for the newer or older style of arcade by reading a digital pin on the PCB that indicates which arcade version you have. This is important because the older style arcade uses a true analog joystick, while the newer units use a digital joystick. The header file's two built-in functions, checkJoystick() and checkButton() can be used to check the inputs, rather than using the digitalRead() function and pinouts.

Adding the following lines in the loop() will make the ball move around on the screen as you would expect. The speed of the ball is dictated by the value which we increment or decrement the coordinate values. Here, we use all 1s. If we made this greater, the ball would move faster, or even "jump" several pixels at once. Play around with this to see what combination of speed and pixel distance looks good. You could also make a speed variable to replace the 1, and adjust it throughout your code.

Lines added to loop() to move ball sprite around on screen

Once we see how this logic works, we can move these lines of code out of our loop() routine and put them into a function called moveBall() that we can call in loop(). This will keep our loop routine clean and easy to follow.

Now, if you've uploaded this code and tested it out, you may have noticed a slight problem. Our ball is traveling off the screen and into oblivion! How can we keep the ball from disappearing?


Implementing Collision Detection

Our next step will be to add collision detection in order to prevent the ball from moving past the screen boundaries. All we need are the dimensions of the screen and the location of the ball.The screen's dimensions are 96x64, so we can use this to measure and test the bounds.

NOTE: With Tiny screens, the origin is located in the top left-hand corner of the screen at pixel (0,0) and the largest value is in the bottom right-hand corner (95, 63).

There are four different screen sides that must be considered for a collision. For the top and left wall, we can compare the x and y values of the ball to 0. As for the bottom and the right wall, we will want to compare the values on the bottom of the ball, and the right side of the ball. These would be the x coordinate + the ball's width, or the y coordinate + the ball's height. This is implemented as shown below:

We will place these statements inside the readInputs() function. This means that before the ball can be moved, it must be within the screen boundaries. The ball can now move freely within the boundaries of the screen! Since the screen never moves, it's easy to detect a collision with its boundaries. But what if we wanted to test the collision of the ball with another sprite? Let's create another sprite and test it!


Collisions Between Sprites

To test for collisions between bitmaps and sprites, we'll borrow a few more functions that were created for the Flappy Birdz and Tiny Brick games. These routines use pointers to analyze two different sprites in relation to each other, looks for overlap in their bitmap attributes, and then returns true if there is a possibility the two bitmap sprites are in a collision.

We can now call testPixelCollision(x, y) from the loop routine to test for a collision between sprites x and y. (Note that a collision between two "transparent" pixels is NOT a true collision.) If it returns true, we know the sprites have collided and can react accordingly. Let's make another bitmap image, a simple red brick. Just as we did before, we'll define each bit color in the header file, then add it to the main code.

Now that we have two sprites, we can make them interact. We'll continue using the ball as our movable piece, and we'll use our brick as a reactionary piece. If we upload our code as is, what would happen? Without implementing any collision detection, the ball will simply pass behind the brick. Why behind and not in front of? If you look again at our array named spriteList, you'll see that the ball is listed before the brick. This means the ball is rendered first, then the brick is rendered, placing it over top of the ball. Swap the order of these to get the ball to travel over the top of the brick. This holds true for any additional bricks we add to the spriteList array. Those listed later are rendered later and are drawn over-top of the previously drawn sprites.

Next, we will add the collision. We have already copied over the functions needed; we just need to test for collisions and determine what reaction to take upon the collision of these two sprites. For now, we'll make the brick move to a new random location on the screen when we collide, like a game of tag. We can use the random function and constrain the limits to the bounds of the screen. These few lines of code will do the trick.

We use the ampersand (&) in front of the sprites we want to test in order to pass the address in memory of each sprite object to the testPixelCollision function. If you turn the game off and back on and collide with the brick a few times, you'll notice that it jumps as expected. Now turn the arcade off and back on. You'll notice that the brick is following the same pattern of motion as it did before. How can this be if we used the random() function? The random() function is actually a pseudo-random number generator. It follows a pattern that does produce random results, but the pattern will be the same in each iteration of your game or program. Can you find the Arduino function that helps to avoid this repeatability? (Hint: Look at the reference for random() here. While this Arduino reference uses pin 0, you will have to use a different pin for the Tiny Arcade hardware because that pin is already in use. Luckily, there are plenty of pins up for use. The following are the unused pins on the main Tiny Arcade board: 1, 2, 8-12, 14-16, 29-32, 37, 38)

Let's work on creating some "automatic motion" where the brick moves incrementally rather than just jumping to a random location. We will have to make the ball collide with a moving brick, giving more of a "chase" to this game of tag. Let's experiment with making the brick "teleport" off the right side of the screen and back through the left side of the screen. Enter the following code within the loop routine.

Now we have a brick that slides across the screen smoothly. (Does this motion remind you of another classic game?) Let's try slowing the brick down a bit. Rather than trying to increment it a half a pixel (which would be impossible), we'll declare a variable named frame and increment it upon each cycle through loop(). Then we can evaluate that and move the brick on every other loop cycle (i.e, every odd numbered cycle). We initialize frame as a global variable, and make it an unsigned int set to 0. "frame++" will increase the value of frame by 1. Then, we'll test if it's the lowest bit using & to compare it with 1. The & symbol is a bit-wise AND operator, and compares the LSB of each value.

Want the brick to move even slower? Compare the frame with a higher number! Speed alteration is a good way to introduce variation and challenge to your game!

Another method that will cause the same effect is to use the modulo operator which returns the remainder of a division. Rather than using (frame & 1) we could use (frame%2 !=0). These are two different methods to acquire the same task, so choose whichever you feel comfortable with!


From Concepts to Tiny Brick

You may have noticed that this tutorial uses the same ball and red brick bitmap as was used in one of our newly released retro games, Tiny Brick. What you may not have noticed is that the programming concepts we've just covered are also the same! Let's make a few adjustments to our code to see if we can start to recreate the Tiny Brick game based on what we already know!

We'll add three more bitmaps to our header file, a yellow brick and a green brick (of the same dimensions as our red brick), and a platform.

More bitmaps added to GameTutorialSprites.h

Then we'll add these new sprites to the main .ino program, ensuring that we assign them different initial x and y locations so they aren't stacked on top of each other. Just like before, they are initialized and also added to our spriteList, as well as increasing our amtSprites variable to match.

I've assigned the x and y coordinates of the bricks so that they will appear in the top left hand corner of the screen, and the platform in the lower center of the screen.

We'll modify the loop routine such that the red brick no longer moves by erasing this portion of code.

Code removed that made red brick sprite move

Rather than making the red brick reappear on the screen when the ball touches it, we will move the brick off the screen so it's no longer visible or accessible by the ball. We'll move it to the arbitrary coordinates of (-100, -100).

The yellow and green bricks will have the same functionality as the red brick, so we'll add the logic for that in the loop() at this point as well.

Our updated loop looks like this:

Updated loop(): Each brick sprite that collides with the ball sprite will now disappear from view

Upload this code and give it a try! You'll see the bricks in the upper left hand corner of the screen, the ball centered, and the platform in the lower center of the screen. Move the joystick around and the ball will move just as before. Colliding the ball into the bricks will make them disappear!

To keep the loop() function clean, move these three brick loops to a function outside of the loop() and call it ballBrickCollision(). Then you can call this function in loop().

Coding Advice: Okay, so now that we have some of the game mechanics in place, we'll do some fine tuning. It is important to be patient while developing. Implementing too many changes at once may result in bugs with unintended side effects that are hard to find. Taking small steps allows us to test that our revisions do everything we intend, and nothing we don't. The "Implement, Test, Repeat" cycle will actually take less time than hunting down errors or conflicting code arguments. Slow and steady wins the race!

Moving Platform

Next, we'll correct our motion and control so that the platform is our moveable "character", the bricks will be reactionary, and the ball will serve as the intermediary, moving as it chooses (based on angle calculations).

To make the platform move we'll use the same approach as we did in making the ball move. Check boundaries, then apply movement, in this instance however, the platform will only move along the x-axis, so we will no longer be using the joystick Up and Down directions. We'll change the readInputs() function to be a movePlatform() function, then move the button checking to a separate routine (we'll revamp this later on to reset the bricks). These will be our new functions. Don't forget to rename to movePlatform() in the loop routine and add a call to checkPushbuttons() beneath that.

Re-purposed function readInputs() changed to movePlatform(), and function checkPushButtons()

Ball Movement

Now we need to make the ball move. So we'll use a similar method to the way the brick moves, however, this time we need to have the code calculate angles, or move the ball along discrete paths. For the simplicity of this tutorial, we will make the ball move only in 45 degree paths. (If you're interested in a more advanced method of calculating angles, check out our code for Tiny Brick!)

We'll start by adding a new variable, ballDirection, that will keep track of the ball direction for us.

New initialized variables

Notice that I added a variable named offscreen and set it equal to -100. The use of this variable will be a tidier method of moving sprites off the screen. You can replace each value of '-100' in the code with the offscreen variable at this point.

Next we'll add two more functions to: make the ball move, and to detect wall collisions in order to redirect the ball. The theory is: the ball can only move in 45-degree angles, meaning that it will increment and/or decrement by a single pixel on both axes simultaneously. Because of this, the ball has 4 possible directions:

  • 0) down and to the left
  • 1) down and to the right
  • 2) up and to the right
  • 3) up and to the left.

The moveBall() function uses these direction values to increment and decrement the ball coordinates based upon the ballDirection variable.

The ballWallCollision() function checks the ball coordinates to see if any of the edges are touching the walls. If they are, it will redirect the ball at the appropriate angle. There are 8 cases because the ball can approach any of the 4 walls from two different angles, and will therefore have different deflections.

New functions with definitions: moveBall() and ballWallCollision()

Be sure to add these functions to the main loop!

Updated loop() function that includes all of the functions added and altered above

Upload your code and test it. You should see the ball bouncing around on the screen at 45-degree angles. If the ball collides with any of the bricks, they will disappear, however, the ball is not bouncing off the platform.

Ball & Platform Collision

The next behavior we need is the ability to hit, or collide with, the ball using the moving platform.

The function below checks to see if the ball's x-coordinates are above the platform, and if the y-coordinates are overlapping the platform. If both of these conditions are true, the ball must be touching the top of the platform, and we can now redirect the ball away from the platform.

New function ballPlatformCollision() that detects when the ball sprite hits the moving platform sprite

Add a call to this function in the loop().

You may be wondering why this function differs from the sprite collision function. It's really a matter of how I want the game to work. The sprite collision function will return true if any pixels on any side are touching, which means if we move the platform sideways into the ball, the platform and ball will collide on their sides. For my version of Tiny Brick, I want to limit the interaction to the very top layer of the platform. This is the reason I have used a custom function for this collision as opposed to the already created function that tests for pixel collisions.

Reset & Buttons

Now that we have the ball colliding with the platform, walls, and bricks, let's alter the function of our buttons to reset the bricks to their "home" location on the screen. We'll write another function that resets their x and y coordinate positions.

Function resetBricks() sets brick sprites to their 'home' location

Function checkPushButtons() retrieves pushbutton data and resets ball or brick location depending on buttons pressed

More Bricks

We'll use the same bitmaps we've already created, and add onto our rows of bricks. To add more bricks using the same bitmaps, we'll use variable names including a number to keep the sprites organized.

To use this naming convention, change the existing sprite names to redBrick1, yelBrick1, and grnBrick1 (the consecutive bricks will be numbered incrementally). You'll also want to change these names in the spriteList[ ], the ballBrickCollision() function, and the resetBricks() function. The fasted method to accomplish this is to do a find & replace using 'Ctrl + f.'

More brick sprites added of each color.

After the new brick sprites have been added to the spriteList[], let's add them to the ballBrickCollision() and resetBricks() functions so that they also disappear when hit, and can be reset.

Updated function ballBrickCollision() that includes the new brick sprites

Updated function resetBricks() that includes the new brick sprites

Looking at the functions above, you can see how they will become lengthy and unmanageable if we continue to add more bricks. We can revise these functions to make them more concise and presentable by implementing a few loops, arrays and pointers! Pointers are often an intimidating concept, especially for beginners, but don't lose hope, we only need a basic understanding of them.

Pointer Review

If you're interested in learning more about pointers, I'll explain the basics here and this tutorial/lesson focuses heavily on the basics and builds up to pointers in C.

The tutorial shows us that variables have:

  1. a type
  2. a name
  3. a value
  4. an address where this information is stored

The first three are recognizable because we use them each time we declare a variable and change its value(e.g, int x = 10;), however because the address is never directly made visible to the programmer (at least not in the way we will be writing code in the Arduino IDE) it's normally an unfamiliar concept.

Pointers are often thought as address variables as they hold addresses assigned internally by the compiler. So even though we'll be using pointers, we won't think about the values that will be assigned to these pointers since they will hold really ugly hex values.

Pointers are useful because rather than copying the data of each variable to another memory location, we can use a pointer to direct the program to where that variable data is already located. In this instance, pointers save us significant processing time as we are dealing with many variables and a lot of sprite data that quickly fills memory.

Think about a world without home addresses. You would have to memorize the location of every place you want to go or ask someone for that data. So instead of memorizing everywhere you need to go and taking up valuable time and memory, homes have assigned addresses you can use to find what you need. In the same way, computer programs assign addresses to each piece of data. So instead of making new variables and re-assigning values, you can just use the address.

& is called a Reference Operator - it's used to grab the ADDRESS of a piece of data. We will use it to assign addresses of variables to pointers.

  • is called a Dereference Operator - you can use this symbol to get the value a pointer is 'pointing' to. Please note that * is also used to declare a new pointer variable. We will use this to copy a DATA value to the pointer. On a basic level, once we have been directed to an address by the pointer, we have access to the data that is stored there.

Pointer explanation of common operators and example

-> is called an Arrow Operator - essentially used in the place of a Dot Operator when dealing with pointers. Rather than writing redBrick1.x, we would equivalently write cs->x (given that cs is a pointer and it is currently pointing to the address of redBrick1) to reference the same data.

Applying Pointers

We'll add the variable numBricks to hold the number of bricks in our code. Create this variable above the amtSprites variable. Rewriting the resetBricks() function using pointers will yield the following:

Function resetBricks() rewritten using pointers

While this code may be longer than what we had previously, it is now far easier to add more bricks. No matter how many new bricks we add, this function won't grow any larger in size. This function will work as long as we update the rstX[ ] and rstY[ ] arrays (which hold the "home coordinates" of the bricks.) It automatically calculates everything else, which is the ultimate goal when creating functions.

Now we'll rewrite the ballBrickCollision() function using the same principals. Try it yourself before proceeding for some practice with pointers.

NOTE: We have effectively used the spriteList[ ] array, which already contains our brick sprites, to reset each one of them. Because of this, we must list all of the bricks as the first elements in the array in order for this to work! If you haven't done so, move the other sprites in the array to come after the bricks.

The final spriteList array should look something like this:

The new ballBrickCollision() logic using pointers will look like this:

Function ballBrickCollision() rewritten using pointers

As before, using pointers allows us to add more bricks without increasing the size of this function.

Even More Bricks

Let's get back to adding a few more bricks and see how it affects our code and gameplay. The dimensions of the bricks and their locations on the screen were calculated such that we can fit 8 bricks across the screen. We'll add in the additional bricks to the sprite list, and update the rstX[] array in the resetBricks() function. We should now have 3 rows and 8 columns of bricks spanning across the top of the screen. I've broken up the spriteList[] to make it easier to read and edit.

Edits made to add more bricks to the game

Just a few more features and we'll have a working version of Tiny Brick Lite.

Improving Brick Reset With a Game Score

Rather than having to press the button to reset the bricks, let's make them reset themselves by adding a game score variable. Each time a brick is hit and eliminated, we'll add a point. When the number of points is equal to the number of bricks, we'll reset the bricks. We declare the new variable score along with our other global variables and initialize it to 0. Adding the line 'score += 1;' in ballBrickCollision() will increase the score as we eliminate bricks. When we reset the bricks, we'll also reset the score to 0 in resetBricks() to keep the cycle going.

Updated ballBrickCollision() to keep track of score variable

Updated resetBricks() to reset score variable when all bricks are eliminated

Added some logic to loop() for resetting the bricks when the score is equal to the number of bricks


Final Game-play Details

We only have a few more details to refine in order to get a single-level version of Tiny Brick LITE. We'll add:

  • A simple "Splashscreen" that appears upon game startup or after losing
  • Brick deflection to make the ball bounce off the bricks
  • Lives that can be lost when the ball hits the floor
  • Frame rate control

Splashscreen

Let's begin our final few steps by adding the "Splashscreen" that will keep the game halted until pressing a pushbutton. We'll copy the Tiny Brick logo from the full version of the game and paste it into our sprites header file, then initialize it in the .ino program like the other sprites. The only difference is that we'll set the initial x and y coordinates to be offscreen. That way, we can make a simple animation that will move it on-screen.

We will need a variable to toggle the game on/off. I have called it start and initialized it to 0. Our new showLogo() function will check to see what state start is in and react accordingly. When pressing the button, the logo will slide off the screen, and start the ball in a random location with an upward direction of travel. When all the bricks are gone, the resetBricks() function will set the start variable back to 0 which will make the logo reappear and halt game execution.

Brick Deflection

For simplicity sake, we will only assume a few of the possible cases of collision between the ball and the brick. The most common collisions will be the ball hitting the bottom, and the left side of the brick. We can add this logic to the ballBrickCollision() function. We'll make another function named ballBrickBounce() and we will pass it the brick pointer. ballBrickBounce() will then check the direction of the ball and redirect it accordingly. These two functions interact to give us a very basic ball deflection effect.

(While the ball will not always move in the direction we would like, it is most often correct, which is within the scope of the concepts covered in this tutorial.)

Logic in functions ballBrickCollision() and ballBrickBounce()

Next, we'll add the lives and remove the possibility for the ball to bounce off the floor. We'll need another global variable to keep track of the lives, and some sort of indicator that shows the user how many lives they have left. Modifying our ballWallCollision() function will prevent the ball from bouncing off the floor. It will also take away a life if the ball goes past the boundary, and will move our lives indicator sprite to show 1 less life.

Updated ballWallCollision() with functionality for game lives and death

A simple sprite in the bottom right hand corner of the screen will serve this purpose nicely and keeps as much of the screen open for game-play as possible. The livesBitmap[ ] is simply a repeating set of ball type images. We can slide this sprite to the left and right to show more or less lives as the player loses them. As usual, declare a global variable, I named it gameLives and initialized it to 3. I also created a bitmap/sprite and initialized it offscreen. When the game starts, it will be moved to the bottom right hand portion of the screen. A restart() function will allow us to reset the ball position and platform when the ball is lost and a life is lost. It is similar to the showLogo() function, though it doesn't make the game logo appear and disappear.

Addition of restart() function to use when all game lives are lost

Final Topic: Frame Rate Control

As we add and take away features in the game, our program will begin to run these features at slightly different speeds. To avoid this side effect, we implement some timing that will only update the loop when a certain amount of time has passed. To do this, we'll use two more global variables and a local variable.

Declaration of global frame variables

Updated loop() with frame rate variables

All three of these variables combined can be used to control how fast the code executes. Adding and taking away features will no longer produce speed variations. Upload the game after adding this control to notice how it appears to be slower, yet the relative speeds are all the same. Play around with the frameTime variable to see how it affects the speed of the game! By using the processors built in timers, we avoid using delay functions that would slow down our code considerably! This is good practice to keep things running efficiently and without wasting clock cycles on the processor.

Fin

All of this in just 300 lines of code. There is certainly room for refinement, but hopefully the concepts in this tutorial have left you feeling that you could fill some of that room.


Limits of Tiny Brick LITE

If you've made it to this point in the tutorial, congratulations! We've covered quite a large number of topics within this tutorial and you may feel overwhelmed. But it is important to discuss the limitations and the potential for improvement that exist within this tutorial version of Tiny Brick. Limitations include:

  • Because we are using discrete 45-degree angles only, the ball can often get "stuck" on the same trajectory in which it continues to miss the remaining bricks
  • The ball deflection off the bricks does not account for every possibility, we have simplified the logic by ignoring half of the possibilities.
  • The game is only a single level
  • The ball speed is fixed
  • The ball deflection angle off the platform is also fixed, giving the player very little actual control over the motion of the ball.
  • There are no win/lose screens, only a logo "Splashscreen"
  • There are no sound effects
  • We didn't use the collisions portion of the type we created.

What can you do to improve the gameplay and correct these shortcomings?

The full version of Tiny Brick has implemented slightly more complex techniques to add these features, though the groundwork for the full game is exactly what we have covered in this tutorial.

Play around with the code to see what you can come up with! Can you add Power-Up bricks? A changing background color? Different Levels and Win/Lose screens? Can you make the ball change speed based on which brick it has deflected off of, or use the collisions piece of our type to require that bricks need to be hit multiple times in order to break? Explore the skills you've learned and see what you can do!

If you come up with your own cool game, we would love to share and feature it so remember to share it with us @TinyCircuits


Exporting Binary files from the Arduino IDE

To export your game in the proper format needed to access it from the TinyArcade Menu, you'll need to export your code as a binary. Once you are happy with your game, you can export it. With the Arduino IDE open, go to Tools -> Build Option: -> Binary for SD Card.

Next, you'll need to go to Sketch -> Export Compiled binary. Once you click on this, the Arduino IDE will compile and export your code as binary. You can follow along in the GIF below.

It will automatically be saved to the same folder that your code is saved in. Find the .bin file in the folder and rename it Game_Title.bin. Create a new folder with the same name as your game and place the binary code inside of it. If you have a .tsv file that goes with your game, you will also save that within this folder, making sure again, that the name matches.

See HERE for creating and converting to .tsv files for the arcade menu.

Now, plug in your SD card via an adapter and move your game folder onto it. Once the file has been transferred, safely eject the SD card. With the Tiny Arcade menu loaded, insert the SD card and power on your Tiny Arcade. Scroll through the menu, and you should be able to find your game with it's accompanying .tsv indicator! Select it and enjoy playing your game from the menu selector!


You can download the code for Tiny Brick LITE here.

You can download the sprite header file for Tiny Brick LITE here.


Contact Us

If you have any questions or feedback, feel free to email us at info@tinycircuits.com or make a post on the forum.

Show us what you make by tagging @TinyCircuits on Instagram, Twitter, or Facebook so we can feature it!

Thanks for making with us!