Programming: Arduino

Check out my other articles:


Blockade / Snake

I remember first seeing a snake game in the late '70s in a video arcade - a quick sarch of the internet has led me to a game called Blockade by a company called Gremlin. According to the brochures, this machine could simply mint money! All you had to do was put in on location, plug it in .. and watch it pull the coins!

   

There are many variations of this game. The Blockade version is a two player game where opponents are trying to force each other to crash into a wall, themselves or their opponent by boxing them into a corner. Other versions are single player and the game player must eat objects on the screen while avoiding walls, themselves and other fixed objects. In both versions the snake grows and speeds up as the game progresses making it increasingly more difficult.


Overview

The game was simplicity itself - block graphics in black and white or black and green and few if any sound affects. Sounds like a perfect candidate to tuen into an Arduino game! Unlike the Galaga game which would require a lot of screen rendering each time the invading army moves, a game like snake only requires the rendering of the snakes head and the clearing of its tail as it moves forward.

Below are sample screen shots from the first five levels of the game. In the provided code, I have designed levels explicitly for the 480 x 320 pixel 3.5" SmartGPU2. I do not have any of the other SmartGPU2 devices and will leave it to others to implement the levels for these different screen resolutions.

Game play is split into multiple levels which get progressively harder than the previous as there are more fixed objects to avoid and the snake moves faster. A level is complete after 25 apples are collected. The game ends when the player crashes into a wall, bomb or themselves.

Δ Top


Code Fragments

The following code highlights areas of interest in the application. It is not intended to be a tutorial or a complete solution. I have provided the complete code for download and you may use or modify it as required.

Supporting Different Resolutions / Screen Sizes
The SmartGPU2 comes in a range of different sizes and screen resolutions - from a baby 160 x 128 pixel 1.8" screen, to a 320 x 240 pixel 2.4" screen through to the 480 x 320 pixel 3.5" screen I bought. I can see in the Arduino libraries that come with the device that there are likely to be two new versions of the device with screen sizes of 480 x 272 pixels (4.3") and 800 x 480 pixels (7.0").

To support as many of these screen resolutions as possible, I wanted my snake variant to support these different resolutions and to render the snake, bomb and wall components in a number of different sizes. Ignoring the smallest screen and the future resolution of 480 x 272, I determined that all screen dimensions were divisible by 10, 16 and 20 so if I made my imaages for the screen elements 10 x 10 pixels, 16 x 16 pixels and 20 x 20 pixels I could support I could evenly divide the screen into numerous grid sizes. For example, my 480 x 320 screen could host a grid of 48 x 32 cells if using the 10 x 10 pixel graphics, a 32 x 20 cell grid at 16 x 16 pixel and a 24 x 16 cell grid when using the 20 x 20 pixel graphics.

I set about drawing grpahics at the three different resolutions in the graphic desinger's tool of choice, Microsoft Excel. The apples were easy ..


But the bombs proved to be much harder to get right. I wanted the bomb to be easily recognisable at even the smallest size and could not find anything on google to plagarise. I finally came up with the designs below. Note they images will be drawn on a black background not yellow.



Having worked out the dimensions of my graphics and therefore the possible grid layouts of the various screens, I wanted the my application written in a screen agnostic way. The only configurable item should be the icon size - the remainder of the application should be able to determine how to render itself.

I achieved this by creating a single variable, called mode (line 005), and assigning this one of the three predefined constants that represent the icon sizes of 10, 16 and 20 pixels (line 001 - 003). Using this mode, three variables that represent the width (and height) of each cell, the maximum number of cells in the X direction and the maximum number of cells in the Y direction are calculated. These fields are used throughout the application when determining how to render the game.

001
002
003
004
005
006
007
008
009
#define MODE_10 10
#define MODE_16 16
#define MODE_20 20
 
int mode = MODE_16;

int CELL_WIDTH = mode;
int CELL_MAXIMUM_X = ((MAX_X_LANDSCAPE + 1) / CELL_WIDTH);
int CELL_MAXIMUM_Y = ((MAX_Y_LANDSCAPE + 1) / CELL_WIDTH);

The MAX_X_LANDSCAPE and MAX_Y_LANDSCAPE constants are defined in the SmartGPU2's Arduino libraries and represent the maximum pixel in each planes. For a 480 x 320 screen, these values will be 479 and 319 respectively.

Δ Top

Maintaining Play State

Similar to my Sudoku implementation, I planned to maintain the state of the game in an array with each cell holding a byte of data that would indicate what object, if any, should be rendered in that position. However a quick calculation of a version of the game running on a 480 x 320 pixel screen using the 10 x 10 pixel set would result in a matrix that contained 1,536 elements. As the Arduino Uno only has 1K of memory this simply wasn't going to fit!

In this simple game, each cell would be either blank or would contain an apple, a bomb or a wall (ignoring the snake as it would be handled separately). As there are only four combinations, I could store the value in 2 bits and therefore store the values for four cells in a single byte. This reduce my storage requirements from 1,536 bytes to a much more manageable 384 bytes. To allow for resolutions up to 800 x 320 and graphics sizes of 10 x 10, I defined an array of 20 x 32 elements (remembering that the dimension of 20 would store 4 cells per byte) in line 001.

001
byte screen[20][32] = {0};

The following functions allow the 'compressed' screen array to be read and updated using coordinates of the cell. The provided X coordinate is divided by 4 to determine which element in the X dimension of the array to address (line 003). Retrieving the remainder allows us to determine which bits in the element to address, line 004. When reading the value, the value retieved from the screen array is logically ANDed with a relevant mask based on the position of the bits to retrieve. Finally the result is shifted bitwise to the left and the retrieved value returned to the caller.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
int getBoardValue(byte x, byte y) {
  
  byte xMaj = x / 4;
  byte xMin = x % 4;
  
  int val = screen[xMaj][y];
  
  switch (xMin) {
    
    case 0:
      val = val & 3;
      break;
      
    case 1:
      val = (val & 12) >> 2;
      break;
      
    case 2:
      val = (val & 48) >> 4;
      break;
      
    case 3:
      val = (val & 192) >> 6;
      break;
      
  }

  return val;
  
}

Like the getBoardValue above, the provided X coordinate is divided by 4 to determine which element in the X dimension of the array to address (line 003). Retrieving the remainder allows us to determine which bits in the element to address, line 004. When reading the value, the value retieved from the screen array is logically ORed with a relevant mask based on the position of the bits to update.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
int setBoardValue(int x, int y, byte value) {

  byte mask = 0;  
  byte xMaj = x / 4;
  byte xMin = x % 4;
  
  int val = screen[xMaj][y];
  
  switch (xMin) {
    
    case 0:
      mask = B11111100;
      val = (val & mask) | value;
      break;
      
    case 1:
      mask = B11110011;
      val = (val & mask) | (value << 2);
      break;
      
    case 2:
      mask = B11001111;
      val = (val & mask) | (value << 4);
      break;
      
    case 3:
      mask = B00111111;
      val = (val & mask) | (value << 6);
      break;
      
  }

  screen[xMaj][y] = val;
  
}
Δ Top

Snake Handling

Unlike the 'fixed' components of the game, the snake is stored in a single dimension array which is 50 elements in length.

001
int snake[50] = {0};

At the start of a new game, the snake is 4 units long and the first four elements of the array hold the cell coordinates where the snakes parts are to be rendered - line 001 shows the array on start up with the snake occupying cells 1,1 through to 1,4. Three separate variables keep track of the index of the tail element, the head elment and the length of the snake. Initially, these are 0, 3 and 4 respectivel.

 
 
001
002
003
004
005
006
007
   0    1    2    3    4    5	Tail  Head  Len	 Description
   
 1,1  2,1  3,1	4,1                0     3    4  Start
 5,1  2,1  3,1	4,1                1     0    4  Move forward.
 5,1  6,1  3,1	4,1                2     1    4  Move forward.
 5,1  6,1  7,1	3,1  4,1           3     2    5  Move forward and extend.
 5,1  6,1  7,1	8,1  4,1           4     3    5  Move forward.
 5,1  6,1  7,1	8,1  9,1           0     4    5  Move forward.
10,1  5,1  6,1	7,1  8,1  9,1      1     0    6  Move and extend forward.

As stated, the snake initially is located in the four cells between 1,1 and 4,1. As the snake moves forward into cell 5,1, the index of the head cell is pointed to the old tail index and the tail index is incremented. The new cell coordinates (5,1) are written to the head cell (line 002). At the end of the first move the array still contains four elements, the tail of the snake is described in element 1 and the head is in element 0.

A second move forward to cell 1,6 (line 003) shows the head index being updated with the previous tail index, the tail index being incremented and the head element being updated with the new cell coordinates.

Let's assume that cell 7,1 has an apple in it. As the snake moves forward into this cell, the snake grows by one unit. This time, all elements from the tail index onwards are moved one element to the right leaving an empty element in the middle of the occupied elements. This empty element becomes the snake's new head and is updated with the 7,1. The tail index is incremented, the head index is pointed to the newly populated index and length is incremented (line 004).

Lines 005 and 006 are simple moves forward and follow the same process as lines 002 and 003. Likewise the last line (line 007) details a second move into a cell occupied by an apple and mimics that of line 004.

Each element in the snake array holds the coordinates of the body part. To conserve memory, the coordinates are 'compressed' into a single integer value using the getCoords function shown below. The getCoordX and getCoordY functions take the 'compressed' coordinates integer and extract out an X and Y coordinate repsectively.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
int getCoords(byte x, byte y) {
 
 return (y * CELL_MAXIMUM_X) + x;
 
}

int getCoordX(int value) {
 
 return value % CELL_MAXIMUM_X;
 
}

int getCoordY(int value) {
 
 return value / CELL_MAXIMUM_X;
 
}

The moveSnake() function moves the snake from its existing position to the new position provided in the coords parameter. The old tail position is retrieved and stored into a temporary variable for later use (line 003). The head pointer is updated to point to the existing tail (line 006) and the contents of the new head element replaced with the new coords (line 007).

The new head is rendered by first drawing a black rectangle (lines 009 - 012) and then a green circle (013 - 015). Note that the actual screen coordinates are calculated using the cell width dimensions that were determined on startup of the application and the size of the square is determined by the nominated graphic size. The tail is cleared by drawing a black rectangle in the old tail position that was previously stored away (lines 016 - 018).

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
void moveSnake(int coords) {
 
  int oldTail = snake[tail];
  
  head = tail;
  snake[head] = coords;
  tail++;  if (tail == length) { tail = 0; }
  
  lcd.drawRectangle(getCoordX(snake[head]) * CELL_WIDTH, 
      getCoordY(snake[head]) * CELL_WIDTH, (getCoordX(snake[head]) 
      * CELL_WIDTH) + (CELL_WIDTH - 1), (getCoordY(snake[head]) 
      * CELL_WIDTH) + (CELL_WIDTH - 1), BLACK, solidFill);
  lcd.drawCircle((getCoordX(snake[head]) * CELL_WIDTH) 
      + CELL_WIDTH_HALF - 1, (getCoordY(snake[head]) * CELL_WIDTH) 
      + CELL_WIDTH_HALF - 1, CELL_WIDTH_HALF, GREEN, solidFill);
  lcd.drawRectangle(getCoordX(oldTail) * CELL_WIDTH, getCoordY(oldTail) 
      * CELL_WIDTH, (getCoordX(oldTail) * CELL_WIDTH) + (CELL_WIDTH - 1), 
      (getCoordY(oldTail) * CELL_WIDTH) + (CELL_WIDTH - 1), 
      BLACK, solidFill);
  
}

Moving the snake to a cell that is already occupied by an apple is handled differently as the snake also grows in length. All elements from the tail to the end of the array are shuffled to the right to make way for the new head element (lines 003 - 007). The head index is updated with the old tail index and the corresponding element in the array is updated with the new coordinates (lines 088 - 012). Finally, the new snake head is drawn in lines 014 - 020 exactly as per the moveSnake routine.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
void moveSnakeAndGrow(int coords) {

  for (byte q=length; q > tail; q--) { 
    
    snake[q] = snake[q-1]; 
  
  } 

  length++;
  head = tail;  
  tail++;  
  snake[head] = coords; 

  lcd.drawRectangle(getCoordX(snake[head]) * CELL_WIDTH, 
      getCoordY(snake[head]) * CELL_WIDTH, (getCoordX(snake[head]) 
      * CELL_WIDTH) + (CELL_WIDTH - 1), (getCoordY(snake[head]) 
      * CELL_WIDTH) + (CELL_WIDTH - 1), BLACK, solidFill);
  lcd.drawCircle((getCoordX(snake[head]) * CELL_WIDTH) 
      + CELL_WIDTH_HALF - 1, (getCoordY(snake[head]) * CELL_WIDTH) 
      + CELL_WIDTH_HALF - 1, CELL_WIDTH_HALF, GREEN, solidFill);
  
}
Δ Top

Rendering the Board

Rendering the static components of the game - the walls, apples and bombs - is achieved by simply clearing the screen (lines 003 and 004) then iterating through the board array using the dimensions calculated at start up (lines 006 - 008). If the board is not blank, then the correct item is rendered by calling the drawItem function and passing the correct object type.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
void drawScreen() {
  
  lcd.setEraseBackColour(BLACK);
  lcd.erase();
  
  for (byte y=0; y < CELL_MAXIMUM_Y; y++) {
  
    for (byte x=0; x < CELL_MAXIMUM_X; x++) {
      
      if (getBoardValue(x, y)>0) { drawItem(x, y, getBoardValue(x, y));}  
    
    }
    
  }

}

The drawItem function is straight forward with rendering being conditioned on the item type and then the graphic size. The example below shows how the 10 x 10 pixel apple is rendered in horizontal lines of green (for the leaf) and red (for the apple's body).

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
void drawItem(byte cellX, byte cellY, byte itemType) {
 
  int x = cellX * CELL_WIDTH;
  int y = cellY * CELL_WIDTH;
  
  switch (itemType) {
    
    case BOARD_BLANK:
      lcd.drawRectangle(x, y, x + CELL_WIDTH - 1, y + CELL_WIDTH - 1, 
                        BLACK, solidFill);
      break;
        
    case BOARD_APPLE:
    
      switch (mode) {
        
        case MODE_10:
        
          lcd.drawLine(x + 6, y, x + 9, y, GREEN);
          lcd.drawLine(x + 5, y + 1, x + 8, y + 1, GREEN);
          lcd.putPixel(x + 5, y + 2, GREEN);
          lcd.drawLine(x + 2, y + 1, x + 3, y + 1, RED);
          lcd.drawLine(x + 1, y + 2, x + 4, y + 2, RED);
          lcd.drawLine(x + 6, y + 2, x + 7, y + 2, RED);
          lcd.drawLine(x, y + 3, x + 8, y + 3, RED);
          lcd.drawLine(x, y + 4, x + 8, y + 4, RED);
          lcd.drawLine(x, y + 5, x + 8, y + 5, RED);
          lcd.drawLine(x, y + 6, x + 8, y + 6, RED);
          lcd.drawLine(x, y + 7, x + 8, y + 7, RED);
          lcd.drawLine(x + 1, y + 8, x + 7, y + 8, RED);
          lcd.drawLine(x + 2, y + 9, x + 3, y + 9, RED);
          lcd.drawLine(x + 5, y + 9, x + 6, y + 9, RED);
          break;
  
        case MODE_16:
        
          lcd.drawLine(x + 10, y, x + 13, y, GREEN);       
          lcd.drawLine(x + 9, y + 1, x + 15, y + 1, GREEN);       
          lcd.drawLine(x + 8, y + 2, x + 14, y + 2, GREEN);       
          ...
          break;
         
        case MODE_20:
        
          ...
          break;
        
      }
      break; 
  
    case BOARD_BOMB:
       
      ...
  
    case BOARD_WALL:
           
      ...
      
  }
  
}
Δ Top

Joystick, Accelerometer or Touchscreen?

As detailed above, I developed my version of snake to be somewhat agnostic to the size of the SmartGPU2 unit it is attached to. Likewise, I wanted my code to work with multiple input devices including the SmartGPU2's touchscreen, a simple joystick and an accelerometer based on the ADXL345 chip.

To simplify the code, I used conditional compilation statements in my code that would include only those lines of code relevant for the selected input device at compilation time. Code for the other input devices are simply ignored at compile time. All of this is controlled by three seperate #define statements at the top of the code. The developer simply comments out the input devices not in use prior to compiling the application (lines 001 - 003).

The accelerometer requires two additional libraries be included at compile time. These libaries are included only if the corresponding USE_ACCELEROMETER directive has been previously declared, lines 007 - 010. Variables can also be conditionally declared and populated as seen in lines 014 - 022.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
//#define USE_TOUCHSCREEN 
//#define USE_JOYSTICK 
#define USE_ACCELEROMETER 

#include <SMARTGPU2.h>   

#ifdef USE_ACCELEROMETER
  #include <Wire.h>
  #include <ADXL345.h>
#endif

...

#ifdef USE_TOUCHSCREEN
POINT point;
int SCREEN_X_THIRD =  ((MAX_X_LANDSCAPE + 1) / 3);
int SCREEN_Y_THIRD =  ((MAX_Y_LANDSCAPE + 1) / 3);
#endif

#ifdef USE_ACCELEROMETER
ADXL345 adxl;
#endif

Input from the nominated device is handled at the top of the main loop(). Regardless of the input method, the snakes motion is controlled by two variables momentumX and momentumY which desscribe motion in the X and Y coordinates. A value of -1 describes a movement to the left or upwards for the momentumX and momentumY variabels respectively. A value of 1 describes a movement to the right or downwards. The snake cannot move diagonally and thus if either variable has a value other than zero, the alternate variable must equal zero.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
#ifdef USE_TOUCHSCREEN

      lcd.touchScreen(&point);
      inputY = (point.x > SCREEN_X_THIRD && point.x < SCREEN_X_THIRD * 2
                  ? (point.y < SCREEN_Y_THIRD ? -1 
                  : (point.y > SCREEN_Y_THIRD * 2 ? 1 : 0)) : 0);
      inputX = (point.y > SCREEN_Y_THIRD && point.y < SCREEN_Y_THIRD * 2 
                  ? (point.x < SCREEN_X_THIRD ? -1 
                  : (point.x > SCREEN_X_THIRD * 2 ? 1 : 0)) : 0);

      if (momentumX!=1 && inputX<0)     { momentumX=-1; momentumY=0;  }
      if (momentumX!=-1 && inputX>0)    { momentumX=1;  momentumY=0;  }
      if (momentumY!=1 && inputY<0)     { momentumX=0;  momentumY=-1; }
      if (momentumY!=-1 && inputY>0)    { momentumX=0;  momentumY=1;  }

#endif 

#ifdef USE_JOYSTICK

      inputX = 1024-analogRead(joyPin1);  
      inputY = analogRead(joyPin2);  

      if (momentumX!=1 && inputX<100)   { momentumX=-1; momentumY=0;  }
      if (momentumX!=-1 && inputX>900)  { momentumX=1;  momentumY=0;  }
      if (momentumY!=1 && inputY<100)   { momentumX=0;  momentumY=-1; }
      if (momentumY!=-1 && inputY>900)  { momentumX=0;  momentumY=1;  }
      
#endif

#ifdef USE_ACCELEROMETER

      adxl.readAccel(&inputX, &inputY, &inputZ); 
      inputY = inputY * -1;
      
      if (momentumX!=1 && inputX<-50)   { momentumX=-1; momentumY=0;  }
      if (momentumX!=-1 && inputX>+50)  { momentumX=1;  momentumY=0;  }
      if (momentumY!=1 && inputY<-50)   { momentumX=0;  momentumY=-1; }
      if (momentumY!=-1 && inputY>+50)  { momentumX=0;  momentumY=1;  }
      
#endif

}
Δ Top

Wiring Diagram

The two diagrams below show the wiring diagrams for the games using an ADXL345-based accelerometer (left) and a joystick (right) and a standard Arduino Uno. Click on the images to see a detailed drawing.

Δ Top

Downloads

Feel free to download, modify and adapt this code to your needs. Please credit me if you use some or all of this code in your own project. If you generate more board configruations, please submit them back to me and I will add them to the download.

   Arduino Source Code
Δ Top

Comments?

 Anything to say?
Name :
Email :
Comments :
Confirmation: