How Does the Game Work?

Foreword 

This Arduino sketch is long with many new concepts, even for builders of all the previous kits. By the end of this section you should understand all the gears and cogs that make this game run.

Foundations 

There are many separate components that the IMU game uses with the Arduino being the electrical ringmaster. Let's take a look at what the Arduino must juggle.

Download file Copy to clipboard
1
2
3
4
#include "I2Cdev.h" //Library to communicate with the IMU
#include "EEPROM.h" //Library to read and write data to and from the memory
#include "MPU6050_6Axis_MotionApps20.h" //Library of command that the IMU can do
#include "U8glib.h" // Library for controlling and writing to the OLED screen

There are two libraries for the IMU. Those being the I2Cdev and MPU6050 libraries. A library for reading and writing to the onboard Arduino library, EEPROM. And lastly, a library for interfacing with the OLED screen, U8Gglib. By including these libraries in our sketch the Arduino can use their commands throughout the sketch.

With a library like the EEPROM, there is no need to initialize it. Every Arduino has an EEPROM section so the library gets added with every compiled program. But for our OLED library, there are many different displays types and each can be wired up in different ways. So after including it we must declare is. Using lines in our code that look like ...

Download file Copy to clipboard
1
2
U8GLIB_SH1106_128X64 u8g(13, 11, 10, 9, 12);
MPU6050 mpu;

...tell the library how these devices are connected. This way when we tell our screen to print out "Hello world" it already knows where to send this information. Some features we rely on are more simple.

Download file Copy to clipboard
1
2
3
4
#define INTERRUPT_PIN 2   // pin used by the mpu to send data
#define MOTOR_PIN 5       // vibration motor
#define trigBUTTON_PIN 7  // game action button
#define calBUTTON_PIN 4   // button to calibrate MPU6050 offsets

Implementing a vibration motor or the microswitch only need a few lines of code so we don't need a separate library since we can write it ourselves.

IMU

The next lines you'll see are the settings and variables where we store variables for the IMU.

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
// MPU control/status vars
bool dmpReady = false;  // set true if DMP init was successful
uint8_t mpuIntStatus;   // holds actual interrupt status byte from MPU
uint8_t devStatus;      // return status after each device operation
uint16_t packetSize;    // expected DMP packet size (default is 42 bytes)
uint16_t fifoCount;     // count of all bytes currently in FIFO
uint8_t fifoBuffer[64]; // FIFO storage buffer

// Orientation/motion vars
Quaternion q;           // [w, x, y, z]
VectorInt16 aa;         // [x, y, z]
VectorInt16 aaReal;     // [x, y, z]
VectorInt16 aaWorld;    // [x, y, z]
VectorFloat gravity;    // [x, y, z]
float euler[3];         // [psi, theta, phi]
float ypr[3];           // [yaw, pitch, roll]

// indicates whether MPU interrupt pin has gone high
volatile bool mpuInterrupt = false;

void dmpDataReady() {
  mpuInterrupt = true;
}

Why are these settings here if we aren't using the IMU yet? They go before all the functional code so that they are global. Meaning that they can be accessed no matter where you are in the sketch. Since the game relies on motion its important that the IMU data be available.

Game data

There are a couple of other variables that need to be accessible.

Download file Copy to clipboard
1
2
3
4
5
6
7
8
9
10
11
//Used to store the position of the kit
int x;
int y;

//Used to store the difficulty or area of your playgrid
uint8_t difficulty;

float yawOff;
float pitchOff;
bool xVisible;
bool yVisible;

These variables track the position the kit is facing as well as the play area of the game.

The Finite State Machine

The game code is organized in what's called a finite state machine, or FSM. Every behavior our code has is filed under a certain state.

Download file Copy to clipboard
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//****************************//
//******* Game States *******//
//**************************//

enum possibleStates {
  CALIBRATION, //The first thing the kit does when powered up. Gets a few readings and calibrates itself
  TLBOUND,    //Waits for the user to set the top left bound of the playgrid
  BRBOUND,    //Waits for the user to set the bottom right bound of the playgrid
  READY,      //Displays the splash screen and waits for the user to press the microswitch to start the game
  GAME,       // The user plays the game in this state
  OVER,       // Game over screen displaying the score
  INPUTSCORE, // If you get a highscore you choose your name from the list of names
  HIGHSCORES  // Shows the highscores
};

enum possibleStates currentState; //Holds what state you are in right now
enum possibleStates nextState;  //Holds the state the you will enter next loop
//**************************//

Those are a list of all the possible game states for the IMU game. The Arduino will stay in its state until told otherwise. That command could be in the form of a microswitch press for menu screens or a time limit like the main game has.

At the bottom of that snippet of code we see two variables, currentState and nextState. We use those to track what state we are in right now and which state we will be moving to. There are some commands we want to activate when in a state and others when moving into a state. That's when we compare the current and next state. If they aren't the same then we know we are about to or just moved states.

Memory

The calibration function can take a couple of minutes to complete and if you had to do that every time the IMU game kit started, well it wouldn't be much fun. We can use the onboard memory of the Arduino to store and use the calibration data so we only have to run that function once. The Arduino reads the memory for stored values in the code below.

Download file Copy to clipboard
1
2
3
4
5
6
7
8
//read MPU6050 offsets from EEPROM
// constructing int from 2 single-byte memory locations
int xAccelOff = EEPROM.read(1) << 8 | EEPROM.read(0);
int yAccelOff = EEPROM.read(3) << 8 | EEPROM.read(2);
int zAccelOff = EEPROM.read(5) << 8 | EEPROM.read(4);
int xGyroOff = EEPROM.read(7) << 8 | EEPROM.read(6);
int yGyroOff = EEPROM.read(9) << 8 | EEPROM.read(8);
int zGyroOff = EEPROM.read(11) << 8 | EEPROM.read(10);

Setup 

Download file Copy to clipboard
1
void setup() {

Anything in the setup loop of the Arduino sketch only runs once. Anything declared above will be initialized here. First, communication with the IMU is started.

Download file Copy to clipboard
1
2
Wire.begin();          // We initialize a channel to the IMU from the Arduino
Wire.setClock(400000); // We choose a speed for the Arduino and IMU to communicate at (400kHz I2C clock)

A serial connection is started to aid with any debugging.

Download file Copy to clipboard
1
Serial.begin(115200);

We tell the IMU to start sending data and tell the Arduino what things are attached where.

Download file Copy to clipboard
1
2
3
4
5
mpu.initialize(); //We start real communication to the IMU
  pinMode(INTERRUPT_PIN, INPUT); //Here we tell the Arduino that pin 2 is used for inputs
  pinMode(calBUTTON_PIN, INPUT);
  pinMode(trigBUTTON_PIN, INPUT);
  pinMode(MOTOR_PIN, OUTPUT);

Any values in the EEPROM are sent to the IMU. We also tell the IMU what settings to use.

Download file Copy to clipboard
1
2
3
4
5
6
7
8
9
10
11
12
13
14
mpu.setXAccelOffset(xAccelOff); //when we calibrate the IMU it gives us unique offsets and there is where we use those
  mpu.setYAccelOffset(yAccelOff); // inorder to get more accurate results
  mpu.setZAccelOffset(zAccelOff);
  mpu.setXGyroOffset(xGyroOff);
  mpu.setYGyroOffset(yGyroOff);
  mpu.setZGyroOffset(zGyroOff);

  mpu.setDMPEnabled(true);
  attachInterrupt(digitalPinToInterrupt(INTERRUPT_PIN), dmpDataReady, RISING); //We assign pin 2 to act as an interupt pin
  mpuIntStatus = mpu.getIntStatus(); // Since the IMU has a processor that works inpedendantly of the Arduino's, it will make
  dmpReady = true;                   // pin 2 go HIGH when it has data ready for us.

  // get expected DMP packet size for later comparison
  packetSize = mpu.dmpGetFIFOPacketSize();

We then choose what font the screen should print in.

Download file Copy to clipboard
1
2
// set font used for all u8g prints
u8g.setFont(u8g_font_6x10);

Lastly, we give the Arduino a starting state. In this case it should start in the CALIBRATION state.

Download file Copy to clipboard
1
 currentState = CALIBRATION;

Game variables 

Between the setup and main loop of this sketch we define some more variables. Defining them here still makes them global because they are out of any loop or function. For the purpose of readability, it just makes more sense to place them closer where they are actually used in the code. These variables dictate how big targets can be, how long the game lasts, and the rest are used to store important values.

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
//****************************//
//******* Game Variables ****//
//**************************//

// Minimum and Maximum target sizes to be randomized within
const uint8_t radMin = 3;
const uint8_t radMax = 12;

// milliseconds that the game runs for before Game Over
#define gameTime 20000

// Initialize the first target at the origin
int xTarg = 0;
int yTarg = 0;
int score = 0;
int hits = 0;
int shots =  0;

// Radius of the first target (randomized after first hit)
uint8_t targRadius = 9;

// Flag so holding the microswitch pulled doesn't work
bool notHeld = false;

// Bounds of the gameplay area
static int topBound;
static int bottomBound;
static int leftBound;
static int rightBound;

static unsigned long startTime;
static unsigned long endTime;
static unsigned long motorTime;

long calibrationTime;
//**************************//

The Main loop 

The first piece of code you'll see is one calling the firstpage of the screen. This activates the screen so any code within it's do loop can write to the screen. This is just how the U8glib library works so we have to roll with it. Everything after do { is in the do loop.

Looking at the next block of code, it's role is to catch if the IMU PCB button is being pressed. If so, the Arduino will tell the IMU it wants to calibrate it, start doing so, and when it is finished the values are stored in the EEPROM. Once stored, the Arduino will tell the IMU to function normally and the game state of CALIBRATION is set. It's important to note that the state of CALIBRATION is different than the long calibrate function seen below. The state of CALIBRATION is just a few seconds long, it's meant to give the IMU time to power up. While the calibration function takes several minutes and only needs to run once.

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
u8g.firstPage();

do {

if (!digitalRead(calBUTTON_PIN)) {
      mpu.setDMPEnabled(false);
      dmpReady = false;
      detachInterrupt(digitalPinToInterrupt(INTERRUPT_PIN));

      // store calibration offsets in 6 variables
      calibrate(&xAccelOff, &yAccelOff, &zAccelOff,
                &xGyroOff, &yGyroOff, &zGyroOff);


      // Store those varibles in the ROM of the Arduino so they won't be erased on powerdown
      EEPROM.write(0, xAccelOff & 0xFF);
      EEPROM.write(1, xAccelOff >> 8 & 0xFF);
      EEPROM.write(2, yAccelOff & 0xFF);
      EEPROM.write(3, yAccelOff >> 8 & 0xFF);
      EEPROM.write(4, zAccelOff & 0xFF);
      EEPROM.write(5, zAccelOff >> 8 & 0xFF);
      EEPROM.write(6, xGyroOff & 0xFF);
      EEPROM.write(7, xGyroOff >> 8 & 0xFF);
      EEPROM.write(8, yGyroOff & 0xFF);
      EEPROM.write(9, yGyroOff >> 8 & 0xFF);
      EEPROM.write(10, zGyroOff & 0xFF);
      EEPROM.write(11, zGyroOff >> 8 & 0xFF);

      mpu.setDMPEnabled(true);
      dmpReady = true;
      attachInterrupt(digitalPinToInterrupt(INTERRUPT_PIN),
                      dmpDataReady,
                      RISING);
      currentState = CALIBRATION;
    }

Every time the do loop runs we wait for data from the IMU and do a little processing on it. That involves taking the yaw and pitch values and translating them to some thing more readable. In this case, a number between -180 and 180 for the x axis and between -90 and 90 for the y.

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
//If the IMU says there is data ready then the Arduino stops here until all of it is sent and received
while (!mpuInterrupt && fifoCount < packetSize);

mpuIntStatus = mpu.getIntStatus();
fifoCount = mpu.getFIFOCount();

// check for overflow
if ((mpuIntStatus & 0x10) || fifoCount == 1024) {
  mpu.resetFIFO();        // reset so we can continue cleanly
} else if (mpuIntStatus & 0x02) {   // normal operation

  while (fifoCount < packetSize) fifoCount = mpu.getFIFOCount();

  mpu.getFIFOBytes(fifoBuffer, packetSize);

  fifoCount -= packetSize;


  mpu.dmpGetQuaternion(&q, fifoBuffer);
  mpu.dmpGetGravity(&gravity, &q);
  mpu.dmpGetYawPitchRoll(ypr, &q, &gravity);

  //**********************************************//
  // X and Y Coordinate Plane//
  //**********************************************//

  // This needs to happen IMMEDIATELY after getting data
  if (currentState == GAME || currentState == INPUTSCORE) {
    //ypr[0] -= yawOff;
    //ypr[1] -= pitchOff;
  }

  //this translates the yaw and pitch from the IMU to a valve between -180 and 180 for the X axis
  if (ypr[0] > M_PI) ypr[0] -= 2 * M_PI;
  if (ypr[0] < -M_PI) ypr[0] += 2 * M_PI;

  // and between -90 and 90 for the Y axis
  if (ypr[1] > M_PI) ypr[1] -= 2 * M_PI;
  if (ypr[1] < -M_PI) ypr[1] += 2 * M_PI;

  // set current position variables
  x = ypr[0] * 180 / M_PI;
  y = ypr[1] * -180 / M_PI;

The State Machine in Practice

Now we get to our first implementation of the state machine. After powering on the IMU game kit the first state will be the CALIBRATION state. It waits a few seconds, which gi ves the IMU some time to get its bearings and after that time is up. It takes what ever direction it is pointing at as its origin for the game. The break; line in the code signifies the end of that state. Before the CALIBRATION state ends, if the calibrating is done then TLBOUND is chosen as the next state.

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
switch (currentState) {
case CALIBRATION:     // If the state is CALIBRATION is goes here
        yawOff = ypr[0];
        pitchOff = ypr[1];
        if (!calibrating) { //When the Arduino runs the calibration state for the first time it waits for 5000 milliseconds
          calibrating = true;
          calibrationTime = millis() + 5000;
        }
        if (millis() > calibrationTime) {  //Waiting for the 5000 millis
          done = true;
          calibrating = false;
        } else {
          done = false;
          calibrating = true;
        }
        if (!done) { //If its still waiting then write the time left to the screen
          u8g.setPrintPos(50, 50);
          u8g.print(calibrationTime - millis());
          u8g.setPrintPos(1, 20);
          u8g.print("Put on flat surface ");
          u8g.setPrintPos(1, 31);
          u8g.print("facing middle of wall");
        }
        if (done) { //Once its done set the next state to the top left bound state
          nextState = TLBOUND;
        }
        break;

The next case is TLBOUND which stands for Top Left Bound. This state and the one after it are very simple. They draw a square, print some text to the screen, and wait for the microswitch input. After the switch is activated, the direction of the kit is stored and the state advanced.

Download file Copy to clipboard
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
case TLBOUND: //This state draws the square and its instructions for creating the top left bound
  u8g.drawFrame(0, 0, 15, 15);    // Draws the Square seen on the screen
  u8g.setPrintPos(1, 30);         // Set position to print
  u8g.print("Shoot top left");    // Print at the last set position

  // When microswitch pulled, activate motor and set bound
  if (!digitalRead(trigBUTTON_PIN) && notHeld) { // If you press the microswitch the code below here runs
    notHeld = false;
    digitalWrite(MOTOR_PIN, HIGH);
    motorTime = millis();
    topBound = y;   //This is where the actual bound variables are set
    leftBound = x;
    nextState = BRBOUND; //Here the nextState is set to the bottom right bound
  }
  break;

BRBOUND stands for Bottom Right Bound. It functions similar to the previous state. After the microswitch is pressed in this state, the READY state is displayed.

Download file Copy to clipboard
1
2
3
4
5
6
7
8
9
10
11
12
13
14
case BRBOUND: //This state draws the square and its instructions for creating the bottom right bound
  u8g.drawFrame(103, 43, 15, 15);   // Draws the Square seen on the screen
  u8g.setPrintPos(1, 30);           // Set position to print
  u8g.print("Shoot bottom right");  // Print at the last set position

  if (!digitalRead(trigBUTTON_PIN) && notHeld) { // If you press the microswitch the code below here runs
    notHeld = false;
    digitalWrite(MOTOR_PIN, HIGH);
    motorTime = millis();
    bottomBound = y;  //This is where the actual bound variables are set.
    rightBound = x;
    nextState = READY;  //Here the nextState is set to the READY
  }
  break;

The READY state waits for the microswitch input and then starts the game. On press, this state stores the current time so it can be used to keep track of how long the game should run.

Download file Copy to clipboard
1
2
3
4
5
6
7
8
9
10
11
12
13
case READY: // If the currentState is READY the code here runs
  u8g.setPrintPos(20, 39);  //The code here writes the words to the screen at the desired positions
  u8g.setFont(u8g_font_6x10);
  u8g.print("Shoot To Start");
  u8g.setPrintPos(20, 21);
  u8g.print("Point at Center");

  if (!digitalRead(trigBUTTON_PIN) && notHeld) { //When the microswitch is pressed the nextState is set to start the GAME
    notHeld = false;
    nextState = GAME;
    startTime = millis();
  }
  break;

The GAME state is long but broken up into understandable chunks. The first thing that happens in this state is a test to see if the target you are supposed to hit is in view. The Arduino compares the direction you are aiming with where the target exists in x and y plane. If you can see it then the test returns true.

Download file Copy to clipboard
1
2
xVisible = x > xTarg - (66 + targRadius) && x < xTarg + (66 + targRadius);
yVisible = y > yTarg - (33 + targRadius) && y < yTarg + (33 + targRadius);

If the target is visible, the next block of code runs. It draws the target as a circle, then does another test. Is the crosshair over the target? It stores this result in the OnTarget variables which can be 'TRUE' or 'FALSE'.

Download file Copy to clipboard
1
2
3
4
5
if (xVisible && yVisible) { // If the target is visible in the x and y...
          u8g.drawCircle(xTarg - x + 64, yTarg - y + 32 , targRadius); // Draw target

          bool xOnTarget = x > xTarg - targRadius && x < xTarg + targRadius; //These are similar to the visible test but these
          bool yOnTarget = y > yTarg - targRadius && y < yTarg + targRadius; //test if the target aimed at

If you press the microswitch when over the target then the next code block runs. It draws a square over the target as a hit marker, picks a new target placement and size, and makes your score increase.

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
if (xOnTarget && yOnTarget && notHeld) {    // If aiming directly at target...

  // Draw on-target indicator
  u8g.drawFrame(xTarg - x + (64 - targRadius - 2), yTarg - y + (32 - targRadius - 2),
                (2 * targRadius + 5), (2 * targRadius + 5));

  //and trigger is pressed
  if (!digitalRead(trigBUTTON_PIN)) {

    // Randomize make a new target position at random
    randomSeed(micros());
    xTarg = random(leftBound + 2, rightBound - 2);
    randomSeed(2 * micros());
    yTarg = random(topBound + 2, bottomBound - 2);
    randomSeed(5 * micros());
    targRadius = random(radMin, radMax);

    //Make the score go up
    score += 2;
    hits += 100;

If the microswitch was pressed and you weren't over the target then that's a miss. The code below runs, which takes away a point from your score.

Download file Copy to clipboard
1
2
3
4
5
if (!digitalRead(trigBUTTON_PIN) && notHeld) {
            notHeld = false;
            score--;
            shots += 1;
          }

All the actions above happen if you can see the target but what if you can't? That's when the code below runs which draws a small circle and a line pointing to the direction the target is in and it will continue to be drawn until the target is visible.

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
} else { //This code runs when the target is so far away is it not on the screen anymore
          // Find out how far away the target is in the x and y axis
          int xDist = xTarg - (x);
          int yDist = yTarg - (y);

          // cap off distance variables
          if (xDist < -6 * 63) xDist = -6 * 63;
          if (xDist > 6 * 63) xDist = 6 * 63;
          if (yDist < -6 * 32) yDist = -6 * 32;
          if (yDist > 6 * 32) yDist = 6 * 32;

          // draw line segment towards target
          u8g.drawLine(63, 32, 63 + xDist / 6, 32 + yDist / 6);
          u8g.drawCircle(63 + xDist / 6, 32 + yDist / 6, 2);

          //Is the trigger is pressed the target will not be hit so make the score go down
          if (!digitalRead(trigBUTTON_PIN) && notHeld) {
            notHeld = false;
            score--;
            shots += 1;
          }
        }

What follows in this state are parts of code that should run regardless of if you can or cannot see the target. Things like displaying the crosshair, your score, and the remaining time.

Download file Copy to clipboard
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/ Draw reticle
        u8g.drawLine(63, 29, 63, 35);
        u8g.drawLine(60, 32, 66, 32);

        // Print score in the upper left
        u8g.setPrintPos(6, 12);
        u8g.print("Score: " + String(score));

        // Print time remaining with a line on the right
        u8g.drawLine(125, 63, 125, 63 - ( (gameTime - (millis() - startTime)) / 1000 ) );
        if ((gameTime - (millis() - startTime)) / 1000 > 9) {
          u8g.setPrintPos(113, 63 - ( (gameTime - (millis() - startTime)) / 1000 ) );
        } else {
          u8g.setPrintPos(119, 63 - ( (gameTime - (millis() - startTime)) / 1000 ) );
        }
        u8g.setFont(u8g_font_6x10);
        u8g.print((gameTime - (millis() - startTime)) / 1000);
        u8g.setPrintPos(84, 63);
        u8g.print("Time:");

Lastly, is the section to transition to the next state. Only if there is no more remaining time.

Download file Copy to clipboard
1
2
3
4
5
6
7
8
9
if (millis() - startTime > gameTime) {
          nextState = OVER;    // If time is up, set the nextState to gameOVER
          endTime = millis();
          difficulty = ((rightBound - leftBound) * (bottomBound - topBound) / 100);
          if (topBound > bottomBound || leftBound > rightBound) {
            difficulty = 0; //user played in invalid boundary
          }
        }
        break;

The next state is called OVER. In this state there is a choice to be made about your next state. If your score was higher than another on the highscore list then you have a new highscore and need to enter your name, INPUTSCORE. If your score wasn't high enough then you just go to the HIGHSCORES state.

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
case OVER: //When the game is over code here will run
        u8g.setPrintPos(5, 9);    //Code below prints out the score,accuracy, and area to the screen
        u8g.print("Score: " + String(score));
        u8g.setPrintPos(5, 61);
        u8g.print("Accuracy: " + String(hits / shots) + "%");
        u8g.setPrintPos(5, 33);
        u8g.print("Area: " + String(difficulty));

        if (millis() - endTime > 2000) { // After waiting 200 milliseconds the player can press the trigger to go to the next screen
          if (!digitalRead(trigBUTTON_PIN) && notHeld) {
            notHeld = false;
            nextState = HIGHSCORES;

            for (int i = 0; i < 5; i++) { // If the player got a high score....
              if (score > scores[i]) {
                highscorePlace = i;
                nextState = INPUTSCORE;  // then they should go to the highscore input state
                break;
              }
            }
          }
        }
        break;

In the INPUTSCORE state, names are read from the list at the beginning and on your choice of name, your score is written into onboard memory.

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
case INPUTSCORE: //State for entering a score
  u8g.setPrintPos(5, 20);
  u8g.print("High Score!");
  u8g.setPrintPos(5, 40);
  u8g.print("Name:"); //This code prints the names from the name array earier
  for (int i = 0; i < maxNames; i++) {
    if (y < (12 * i) - 25 && y > (12 * i) - 13 - 25) {
      u8g.setPrintPos(40, -y + 10 + (12 * i));

      if (!digitalRead(trigBUTTON_PIN) && notHeld) { //When you press the trigger, a name is choosen and the score is set in memory
        notHeld = false;
        scorePopulate = true; //populate new scores
        for (int j = 17; j > highscorePlace + 13; j--) {
          EEPROM.write(j, EEPROM.read(j - 1));
          EEPROM.write(j + 5, EEPROM.read(j + 5 - 1));
          EEPROM.write(j + 10, EEPROM.read(j + 10 - 1));
        }


        EEPROM.write(highscorePlace + 18, i);
        EEPROM.write(highscorePlace + 13, score);
        EEPROM.write(highscorePlace + 23, difficulty);

        nextState = HIGHSCORES; //Once the score is set then go to the state where you can see all the scores
      }

    } else {
      u8g.setPrintPos(90, -y + 10 + (12 * i));
    }
    u8g.print(names[i]);
  }

  u8g.setPrintPos(5, 9);
  u8g.print("Score: " + String(score));
  u8g.setPrintPos(5, 61);
  u8g.print("Accuracy: " + String(hits / shots) + "%");
  break;

The last state is the HIGHSCORES state. It reads scores from memory and displays them. On a press of the microswitch, it resets score variables, and sends us back to the READY state to start a new game.

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
case HIGHSCORES: //Here the highscores are shown
    if (scorePopulate) { //Only if you have set to show them. A setting for this is above the setup loop
      scorePopulate = false;
      populateScores();
    }

  u8g.setPrintPos(2, 10); // This prints out the format for the scores
  u8g.print("Name    Score   Area");

  for (int i = 0; i < 5; i++) {

    //print 1 through 5 on begininng of each line
    u8g.setPrintPos(2, 10 * (i) + 22);
    u8g.print(String(i + 1));

    //print the names associated with the indexed highscore-holders
    u8g.setPrintPos(10, 10 * (i) + 22);
    u8g.print(names[scoreNames[i]]);

    //print each corresponding score
    u8g.setPrintPos(60, 10 * (i) + 22);
    u8g.print(scores[i]);

    //print each corresponding area
    u8g.setPrintPos(100, 10 * (i) + 22);
    u8g.print(areas[i]);
  }

  if (!digitalRead(trigBUTTON_PIN) && notHeld) { //When the trigger is pressed....

    // Reset game state to start screen, time, and score
    notHeld = false;
    nextState = READY;
    score = 0;
    shots = 0;
    hits = 0;
    delay(500);
  }
  break;

A few extra blocks run before the loop ends. One check to make sure some variables aren't overwritten. The code to make the motor buzz. And the code checking if the microswitch is held down. All these happen here at the end before a state change can happen.

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
switch (nextState) { //This is a check to make sure that when you go back to the CALIBRATION state the wait time isn't over written
   case CALIBRATION:
     if (currentState != CALIBRATION) {
       calibrating = false;
       done = false;
     }
     break;
 }


 // When ever the vibration motor is turned on. It turns off after 100 milliseconds
 if (millis() - motorTime > 100) {
   digitalWrite(MOTOR_PIN, LOW);
 }

 //When the trigger is released notHeld is true
 if (digitalRead(trigBUTTON_PIN)) {
   notHeld = true;
 }

 //Advances the to the next state
 currentState = nextState;
} while ( u8g.nextPage() );  //do-while loop end
}//void loop end

Extras 

Below the main loop code there are two functions. One that makes the scores show up and another that calibrates the IMU. These are global functions and can be called from anywhere in the code. We put them in the bottom for readability.