Software

This section explains what the blocks of code do. 

Follow along by reading the code you downloaded on your computer. We explain the code from top to bottom.

Libraries

This kit uses some pretty complex components and due to that there are a few key libraries needed to run the game code. RGB_sprite was created to manage shapes and move them on the screen . masterScreen handles sending data to the screen via the 4 shift registers. UsbKeyboard adds keyboard emulation via the USB-B socket.

Download file Copy to clipboard
1
2
3
4
5
6
7
8
//Kit specific libraries
#include <RGB_sprite.h>
#include <masterScreen.h>

//File and library for USB emulation
#include <usbconfig.h>
#include <UsbKeyboard.h> //Library needed for USB input
bool usbEnabled = false;

Button Arrays

Here we store the format of our buttons. The row and column scanning button arrays are ordered. A separate array house the button placements beyond just the pin numbering.

Download file Copy to clipboard
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//Settings for the button scan array, left side
int leftInputs[] = {A0, A1}; //INPUT_PULLUP reading pins
int leftOutputs[] = {A2, A3, A4}; //Column HIGH or LOW pins
//Used to translate  the reading to a real button
String leftButtons[2][3] = {
  {"LL", "LU", "SELECT"},
  {"LR", "LD", "LShoulder"}
};

//Settings for the button scan array, left side
int rightInputs[] = {10, A5}; //INPUT_PULLUP reading pins
int rightOutputs[] = {11, 12, 13}; //Column HIGH or LOW pins
//Used to translate  the reading to a real button
String rightButtons[2][3] = {
  {"RL", "RU", "START"},
  {"RR", "RD", "RShoulder"}
};

Next are varibles associated with the two button arrays. 'Bytes' are used to track button presses and the paddle position variable is created.

Download file Copy to clipboard
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//***********Paddle Global Variables************
// Left Paddle
int LScore = 0;
int leftPaddlePosition            = 0;
byte leftUpButton_currentState   = 0;
byte leftDwnButton_currentState  = 0;
byte leftUpButton_previousState   = 0;
byte leftDwnButton_previousState  = 0;

//Right Paddle
int RScore = 0;
int rightPaddlePosition           = 0;
byte rightUpButton_currentState   = 0;
byte rightDwnButton_currentState  = 0;
byte rightUpButton_previousState  = 0;
byte rightDwnButton_previousState = 0;

Game States

All possible states for the game are defined and arranged in the 'possibleStates' enumeration. A separate array is created that holds directions the ball is able to travel after being served.

Download file Copy to clipboard
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//***********Game States************
enum possibleStates {
  UPRIGHT,
  RIGHT,
  DWNRIGHT,
  UP,
  UPLEFT,
  LEFT,
  DWNLEFT,
  DOWN,
  PAUSE,
  SCORE,
  SERVE,
  WIN,
  STARTGAME,
  POINT
};

possibleStates gameState;

//Starting direction. We don't want UP or DOWN as starting states
int startDir[] = {UPRIGHT, RIGHT, DWNRIGHT, UPLEFT, LEFT, DWNLEFT};

bool isCollisionResult = 0;

Screen Declarations

The screen is declared in addition to all game sprites. There are two sprite arrays created. One for the paddles and ball. The other for the scores.

Download file Copy to clipboard
1
2
3
4
5
6
7
8
9

//***********Game Screen and Sprite Global Variables************
//Declare the screen
masterScreen gameScreen;
//Declare the paddles and ball
RGB_Sprite sprite_lst[num_sprites];
//Declare the score counters
RGB_Sprite score[2];

Setup 

This loop only runs once.

Pin Definitions

Using two 'for loops' we iterate through all the used pins and set to their correct state. There is an additional step for the output pins. We set them high which for them is their off state.

Download file Copy to clipboard
1
2
3
4
5
6
7
8
9
10
11
12
//Set the rows of the button array as inputs
for (int i = 0; i < 2; i++) {
  pinMode(leftInputs[i], INPUT_PULLUP);
  pinMode(rightInputs[i], INPUT_PULLUP);
}
//Set the columns as outputs
for (int i = 0; i < 3; i++) {
  pinMode(leftOutputs[i], OUTPUT);
  digitalWrite(leftOutputs[i], HIGH);
  pinMode(rightOutputs[i], OUTPUT);
  digitalWrite(rightOutputs[i], HIGH);
}

Screen and Sprites

We initialize the screen and store all the sprites with their desired qualities to their arrays. The first two values are width and height respectively. The next numbers are the color values of each pixel. Red is 1, Green is 2, and Blue is 4.

Download file Copy to clipboard
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//***********Initialize gameScreen************
  gameScreen.createScreen();

  //*************Sprite Initialization***********
  //Create and store a sprite (width, height, colors to use ....)
  sprite_lst[leftPad].Sprite(1, 4, 1,
                             1,
                             1,
                             1);

  sprite_lst[rightPad].Sprite(1, 4, 2,
                              2,
                              2,
                              2);

  sprite_lst[ball].Sprite(1, 1, 4);

  score[0].Sprite(1, 8, 0,
                  0,
                  0,
                  0,
                  0,
                  0,
                  0,
                  0);
  score[0].updateOrigin(3, 0);
  score[1].Sprite(1, 8, 0,
                  0,
                  0,
                  0,
                  0,
                  0,
                  0,
                  0);
  score[1].updateOrigin(4, 0);

USB Magic

Here is some disabled code to show how USB emulation would work.

Download file Copy to clipboard
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//start with the game at the STARTGAME state
  gameState = STARTGAME;

  //Magically make everything work
  tone(8, 459, 96);
  tone(9, 459, 96);
  if (usbEnabled) {
    // Clear interrupts while performing time-critical operations
    cli();

    // Force re-enumeration so the host will detect us
    usbDeviceDisconnect();
    delayMs(250);
    usbDeviceConnect();

    // Set interrupts again
    sei();
  }

Main Loop 

Button Scanning

The first thing on entrance to the main loop is to check the state of the buttons. The function 'buttonScan()' is called and the full function code is found near the bottom of the sketch. All settings from the past looping are passed to the previous variables. The current state variables are reset to 0. What happens next is the row and column scanning. A column is turned LOW and then the two inputs are checked. If a button is pressed the inputs will read a 0. After both inputs are read the column goes back to HIGH and the next column is set LOW. The iterates through every row and column for both left and right below finishing and returning to the main loop.

Download file Copy to clipboard
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
//Run the row and column button scans
  buttonScan();

...

void buttonScan() {
  // Left side
  // Reset varibles
  leftUpButton_previousState = leftUpButton_currentState;
  leftDwnButton_previousState = leftDwnButton_currentState;
  leftUpButton_currentState = 0;
  leftDwnButton_currentState = 0;
  for (int y = 0; y < 3; y++) { //Outputs
    digitalWrite(leftOutputs[y], LOW); //Turn column on
    for (int x = 0; x < 2 ; x++) { //Inputs
      if (!digitalRead(leftInputs[x])) { //Read row
        if (x == 0  && y == 1) {
          //Left Up is pressed down
          leftUpButton_currentState = 1;
        } else if (x == 1 && y == 1) {
          //Left Down is pressed down
          leftDwnButton_currentState = 1;
        }
      }
    }//End of x
    digitalWrite(leftOutputs[y], HIGH); //turn column off
  }//End of y

  // Similar for right side
  rightUpButton_previousState = rightUpButton_currentState;
  rightDwnButton_previousState = rightDwnButton_currentState;
  rightUpButton_currentState = 0;
  rightDwnButton_currentState = 0;
  for (int y = 0; y < 3; y++) { //Outputs
    digitalWrite(rightOutputs[y], LOW); //Turn column on
    for (int x = 0; x < 2 ; x++) { //Inputs
      if (!digitalRead(rightInputs[x])) { //Read row
        if (x == 0  && y == 1) {
          rightUpButton_currentState = 1;
        } else if (x == 1 && y == 1) {
          rightDwnButton_currentState = 1;
        }
      }
    }//End of x
    digitalWrite(rightOutputs[y], HIGH); //turn column off
  }//End of y

}

Paddle Movements

If the correct button(s) for paddle movement are pressed then below is where they are moved. First a check runs if the paddle would be going off the screen and if so, adjusts itself. After any needed adjustments the position of the paddle is updated in the sprite array and the old sprite is cleared and the new one redraw.

Download file Copy to clipboard
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
//If the up button of the leftpad was pressed...
if (leftUpButton_currentState != leftUpButton_previousState && !leftUpButton_previousState) {
  //Move the paddle up
  leftPaddlePosition++;
  //Left correction for going off the screen
  if (leftPaddlePosition > 4) {
    leftPaddlePosition = 4;
  }
  if (leftPaddlePosition < 0) {
    leftPaddlePosition = 0;
  }
  //These lines erase the sprite, write its new position, then push that to the screen
  gameScreen.clearSprite(sprite_lst[leftPad]);
  sprite_lst[leftPad].updateOrigin(0, leftPaddlePosition);
  gameScreen.updateMasterScreen(sprite_lst[leftPad]);
}
  //Checking for down button press
else if (leftDwnButton_currentState != leftDwnButton_previousState && !leftUpButton_previousState) {
  leftPaddlePosition--;
  //Left correct
  if (leftPaddlePosition > 4) {
    leftPaddlePosition = 4;
  }
  if (leftPaddlePosition < 0) {
    leftPaddlePosition = 0;
  }
  gameScreen.clearSprite(sprite_lst[leftPad]);
  sprite_lst[leftPad].updateOrigin(0, leftPaddlePosition);
  gameScreen.updateMasterScreen(sprite_lst[leftPad]);
}

//Same checks but for the rightpad
if (rightUpButton_currentState != rightUpButton_previousState && !rightUpButton_previousState) {
  rightPaddlePosition++;
  //right correct
  if (rightPaddlePosition > 4) {
    rightPaddlePosition = 4;
  }
  if (rightPaddlePosition < 0) {
    rightPaddlePosition = 0;
  }
  gameScreen.clearSprite(sprite_lst[rightPad]);
  sprite_lst[rightPad].updateOrigin(7, rightPaddlePosition);
  gameScreen.updateMasterScreen(sprite_lst[rightPad]);
}
else if (rightDwnButton_currentState != rightDwnButton_previousState && !rightUpButton_previousState) {
  rightPaddlePosition--;
  //right correct
  if (rightPaddlePosition > 4) {
    rightPaddlePosition = 4;
  }
  if (rightPaddlePosition < 0) {
    rightPaddlePosition = 0;
  }
  gameScreen.clearSprite(sprite_lst[rightPad]);
  sprite_lst[rightPad].updateOrigin(7, rightPaddlePosition);
  gameScreen.updateMasterScreen(sprite_lst[rightPad]);
}

Ball Movements

This code block checks if the ball should move biased on the time that has passed since its last movement. If so then it clears the ball from the screen and redraws it in the right place. After that it checks if the ball has collided with anything. The position of the ball and its collision state are relayed to the 'stateChange()' fucntion.

Download file Copy to clipboard
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//this code check the current time to see if the ball should move
sprite_lst[ball].current_time = millis();
if ((sprite_lst[ball].current_time - sprite_lst[ball].previous_time) >= sprite_lst[ball].duration)
{
  sprite_lst[ball].previous_time = sprite_lst[ball].current_time;

  if (sprite_lst[ball].x_ > 7) {
    sprite_lst[ball].x_ = 7;
  } else if (sprite_lst[ball].x_ < 0) {
    sprite_lst[ball].x_ = 0;
  }

  if (sprite_lst[ball].y_ > 7) {
    sprite_lst[ball].y_ = 7;
  } else if (sprite_lst[ball].y_ < 0) {
    sprite_lst[ball].y_ = 0;
  }

  //Checks if the ball hit something
  isCollisionResult = sprite_lst[ball].isCollisionNoScreen(num_sprites, ball, sprite_lst, sprite_lst[ball].x_, sprite_lst[ball].y_);

  //if no collision redraw the ball
  if (isCollisionResult == false) {
    //clear Sprite
    gameScreen.clearSprite(sprite_lst[ball]);

    //update origin
    sprite_lst[ball].updateOrigin(sprite_lst[ball].x_, sprite_lst[ball].y_);

    //update screen matrix
    gameScreen.updateMasterScreen(sprite_lst[ball]);
  } else {
    //Collision with paddle sound
    tone(9, 459, 96);

  }
  //Figure out the next move of the ball
  stateChange();
}
}//End of loop()

State Machine 

Ball Movement State MachineBall Movement State Machine

The game that we have coded for you is the classic, Pong: 2 paddles on either end of the screen and a ball being hit back and forth. The paddles are simple to code but getting realistic movement from the ball is tricky. There are many different ways the balls travels across the screen and we have to factor in movement behavior when it bounces off walls and paddles. To keep track of these behaviors we will employ a state machine. A representation of that is above. States are defined as well as the transitions and connections between them. We can look at our visual state machine and see that only certain states are connected or that some states can only be reached by passing through other states. The state machine in this game is used to apply logical movements to the ball. If the ball hit the top of the screen then it should bounce towards the bottom of the screen. What is shown in the picture above is written in the code below.

Download file Copy to clipboard
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
void stateChange()
{
  sprite_lst[ball].prevState = sprite_lst[ball].state;
  //Refer to the ball state diagram in the learning module
  switch (gameState) {

    case UPRIGHT:
      sprite_lst[ball].x_++;
      sprite_lst[ball].y_++;
      if (isCollisionResult == true) {
        sprite_lst[ball].x_ -= 2;
        sprite_lst[ball].y_ -= 2;
        gameState = UPLEFT;
      } else if (sprite_lst[ball].isR_boardCollision() == true) {
        //sprite_lst[ball].x_ = 7;
        //sprite_lst[ball].y_ = 6;
        //sprite_lst[ball].state = LEFT;
        LScore++;
        gameState = POINT;
      } else if (sprite_lst[ball].isT_boardCollision() == true) {
        sprite_lst[ball].x_--;
        sprite_lst[ball].y_--;
        gameState = DWNRIGHT;
      }
      break;

    case RIGHT:
      sprite_lst[ball].x_++;
      if (isCollisionResult == true) {
        sprite_lst[ball].x_ -= 2;
        //UPLEFT, LEFT, DWNLEFT
        gameState = random(4, 6);
      } else if (sprite_lst[ball].isR_boardCollision() == true) {
        LScore++;
        gameState = POINT;
      }
      break;

    case DWNRIGHT:
      sprite_lst[ball].x_++;
      sprite_lst[ball].y_--;
      if (isCollisionResult == true) {
        sprite_lst[ball].x_ -= 2;
        sprite_lst[ball].y_ += 2;
        gameState = DWNLEFT;
      } else if (sprite_lst[ball].isR_boardCollision() == true) {
        LScore++;
        gameState = POINT;
      } else if (sprite_lst[ball].isB_boardCollision() == true) {
        sprite_lst[ball].x_--;
        sprite_lst[ball].y_++;
        gameState = UPRIGHT;
      }
      break;

    case UP:
      sprite_lst[ball].y_++;
      if (sprite_lst[ball].y_ >= 7) {
        gameState = DOWN;
      }
      break;

    case UPLEFT:
      sprite_lst[ball].x_--;
      sprite_lst[ball].y_++;
      if (isCollisionResult == true) {
        sprite_lst[ball].x_ += 2;
        sprite_lst[ball].y_ -= 2;
        gameState = UPRIGHT;
      } else if (sprite_lst[ball].isL_boardCollision() == true) {
        RScore++;
        gameState = POINT;
      } else if (sprite_lst[ball].isT_boardCollision() == true) {
        sprite_lst[ball].x_++;
        sprite_lst[ball].y_--;
        gameState = DWNLEFT;
      }
      break;

    case LEFT:
      sprite_lst[ball].x_--;
      if (isCollisionResult == true) {
        sprite_lst[ball].x_ += 2;
        //UPRIGHT, RIGHT, DWNRIGHT
        gameState = random(0, 2);
      } else if (sprite_lst[ball].isL_boardCollision() == true) {
        RScore++;
        gameState = POINT;
      }
      break;

    case DWNLEFT:
      sprite_lst[ball].x_--;
      sprite_lst[ball].y_--;
      if (isCollisionResult == true) {
        sprite_lst[ball].x_ += 2;
        sprite_lst[ball].y_ += 2;
        gameState = DWNRIGHT;
      } else if (sprite_lst[ball].isL_boardCollision() == true) {
        RScore++;
        gameState = POINT;
      } else if (sprite_lst[ball].isB_boardCollision() == true) {
        sprite_lst[ball].x_++;
        sprite_lst[ball].y_++;
        gameState = UPLEFT;
      }
      break;

    case DOWN:
      sprite_lst[ball].y_--;
      if (sprite_lst[ball].y_ <= 0) {
        gameState = UP;
      }
      break;

    case PAUSE:
      if ((LScore >= 8) || (RScore >= 8)) {
        gameState = WIN;
      } else {
        score[0].write(0, (LScore - 1), 1);
        score[1].write(0, (RScore - 1), 2);
        gameScreen.updateScreen(2, score);
        //delay() used here to stop sketch to flash score, delay() is not
        //permitted elsewhere
        delay(2500);
        gameScreen.clearSprite(score[0]);
        gameScreen.clearSprite(score[1]);
        gameState = SERVE;
      }
      break;

    case SERVE:
      sprite_lst[ball].x_ = 3;
      sprite_lst[ball].y_ = 3;
      gameState = startDir[random(1, 6)];
      break;

    case WIN:
      score[0].write(0, (LScore - 1), 1);
      score[1].write(0, (RScore - 1), 2);
      gameScreen.updateScreen(2, score);
      delay(10000);  //10s
      gameState = STARTGAME;
      break;

    case STARTGAME:
      //*******Game board Specific Sprite starting Position Stuff*********

      //gameScreen.clearMasterScreen();
      sprite_lst[ball].duration = 200;
      sprite_lst[ball].x_ = 3;
      sprite_lst[ball].y_ = 3;

      sprite_lst[leftPad].updateOrigin(0, leftPaddlePosition);
      gameScreen.updateMasterScreen(sprite_lst[leftPad]);

      sprite_lst[rightPad].updateOrigin(7, rightPaddlePosition);
      gameScreen.updateMasterScreen(sprite_lst[rightPad]);

      LScore = 0;
      RScore = 0;
      delay(1000);
      clearScore();
      //This might change to something smarter
      gameState = startDir[random(1, 6)];
      break;

    case POINT:
      //Two tones to make the wall collision sound
      //UsbKeyboard.sendKeyStroke(KEY_A);
      tone(9, 226, 16);
      noTone(226);
      tone(9, 490, 257);
      gameState = PAUSE;
      break;
  }
}