Thimble Learning Platform
Back to full view

IMU Game

Introduction

This tutorial will guide you through soldering, assembling, and programming the IMU Game kit. The final product will be able to display virtual targets for you to shoot. You aim by physically moving the project as if the targets were in real space! If you run into problems at any point during this project, please see our Troubleshooting page for help.

Objectives

  • Assemble and solder the IMU Game
  • Learn how to program the project
  • Learn to make and modify a game using an inertial measurement unit (IMU)
  • Electrical Assembly

    Gather Materials 

    Introduction
    Let's take a look at the new kit

    Gather the materials below and get ready to build the PCB for the IMU Game.

    Parts

    All Parts x Qty
    PCB x 1
    10KΩ x 3
    Capacitor x 1
    Push Button x 1
    MOSFET x 1
    8-pos stackable header x 1
    8-pos header x 1
    6-pos stackable header x 1
    4-pos stackable header x 1
    How to Use a Soldering Iron
    A short instructional video to learn how to use a soldering iron safely and properly

    Tools you'll need

    Soldering Iron
    Solder Wire
    Wire Cutters
    Electrical Assembly Video
    Follow along to complete the PCB

    Solder Resistors 

    Get the three 10k resistors. They are the same value so no need to worry about which is which. Resistors are also not polar so there is no putting them in backwards. Bend the leads and place one resistors in each spot labeled with an 'R'. Once the resistors are through the PCB, bend the leads to the side a bit so they don't fall out. Flip over the PCB and solder the resistors. Once all the resistors are soldered in place their leads can be trimmed.

    A bent 10k resistor ready to be placed
    A placed 10k resistor
    All resistors placed
    The flipped PCB
    Solder Resistors
    All resistors soldered
    Trim the leads
    Trimmed leads

    Solder Button and Capacitor 

    Place the Button and Capacitor. Both of these components are not polar so they can't be put in backwards. Once placed, flip over the PCB. Solder the 4 legs of the button and the 2 leads of the capacitor. Once all the solder joints are made trim the leads of the capacitor.

    Place the button and capacitor
    A placed button
    A placed capacitor
    The flipped PCB
    Solder the button
    Soldered button
    Solder the capacitor
    Soldered capacitor
    Trim the capacitor leads
    Trimmed leads

    Solder Headers 

    There are two types of the headers included with the IMU Game kit. One type is to go through the PCB and into the Arduino. The other type of header is for the IMU and that's the one we'll be starting with. Take the 8 pin non-stackable header and place it in the PCB. Flip the PCB over and just solder one pin for now. Once soldered take a look at the header and make sure its straight. If not, reheat the solder joint and move the header into place. Once straight, the rest of the pins can be soldered.

    Find the other 3 headers. These are the stackable headers. Placed them in their appropriate spots based on their number of pins. Flip the PCB over and repeat the same process. Tack one pin of each header into place. Make any adjustments if necessary. Then solder the rest of the pins.

    Non-stackable Header and Stackable Headers
    There is where the 8 pin header goes
    A placed 8 pin header
    The flipped PCB
    Pin to tack
    A tacked 8 pin header
    Double checking the alignment
    A soldered header
    The rest of the headers
    The rest of the headers placed
    Tacked headers
    Double checking the headers
    All the headers soldered

    Solder MOSFET 

    The last component is the MOSFET. This is polar so the orientation matters. Place the MOSFET in the bottom middle with the metal side facing away or off the PCB. Flip over the PCB and solder all 3 leads. Once soldered, trim all 3 leads of the MOSFET.

    PCB and MOSFET
    Metal side facing out
    Metal side facing out
    Ready to solder
    A soldered MOSFET
    Trim leads
    Finished PCB!

    Motor and Switch Prep 

    The PCB is all done but before we move on to the Mechanical Assembly, two parts need some work. The first is the vibration motor. First, strip off some of the insulation surrounding the two wires coming from the motor. Next, take about 8 inches of the red and black hook up wire and strip that too. Twist one of the stranded wires together with a wire coming from the motor and solder them to make a more permanent connection. Remember to heat up the wires before you try and apply solder to them. Do this for the other set of wires too.

    The vibration motor
    Tiny stripped wires
    Stripped hook up wire
    Motor and hook up wire
    Soldered together
    And the other

    Now we must do the same thing to the microswitch. Start by taking a new length of the red and black hook up wire and strip a bit off each side. IT doesn't maker which color is used. Feed one stripped end through one of the holes in the tabs of the microswitch. The middle tab will not be used. Apply solder to connect the two. Using the remaining wire, feed that through the unpopulated tab of the micro switch and solder that too.

    The microswitch
    Stripped hook up wire
    switch and hook up wire
    Soldered together

    That's most of the electrical assembly but don't put your soldering iron away yet. We'll need it to solder the other end of those wires.

    Mechanical Assembly

    Gather materials and a friendly warning 

    Some pieces of the handle snap together in such a way that they cannot be separated. If a piece is attached out of order, it may become very difficult to assemble this project. Please follow these instructions very carefully in order to ensure that assembly stays easy and fun!

    Also note that if you choose to screw the battery holder in, it will not be removable when the handle is fully assembled. The microswitch will also be trapped inside the handle when it is fully assembled, and would be very difficult to remove. Keep these points in mind when building this project!

    Mechanical Parts

    All Parts x Qty
    OLED Mount x 1
    Arduino Mount x 1
    Side Plate x 2
    Center Plate x 1
    Short M3 Screws x 22
    Long M3 Screws x 4
    M3 Nuts x 8
    M3 Male Standoffs x 9
    M3 Female Standoffs x 9
    15mm Standoffs x 2
    M2.3 Screws x 2
    M2.3 Nuts x 2

    Electrical Parts

    All Parts x Qty
    Finished PCB x 1
    OLED Screen x 1
    GY521 (MPU6050 Breakout) x 1
    Microswitch x 1
    Vibration Motor x 1
    24 AWG Wire x 1
    Battery Pack x 1
    VHB tape x 1

    Tools you'll need

    Soldering Iron
    Solder Wire
    Screwdriver
    Mechanical Assembly Video
    Let's build the kit

    Standoffs 

    We'll be starting with the center plate. Its handle is a bit wider than the side plates and the top portion is a bit shorter. Take note of all the holes in the center plate. There are two holes close to each other and smaller than the rest. Those are not for standoffs. The rest are.

    Start by placing a male M3 standoff through a hole. Screw a female M3 standoff to it from the other side of the center plate. Hand-tighten the standoffs so that they are not loose, but be sure not to over-tighten. Using excessive force can cause the acrylic to crack! Repeat this for the other 8 standoff holes.

    Center and Side plates
    Center plate holes
    A placed male standoff
    A placed male standoff
    A placed female standoff
    Not too tight
    All standoffs placed
    All standoffs placed
    All standoffs placed

    First Side Plate 

    Take a side plate and line it up with the center plate. Which side you line up the first side plate with will determine which side the microswitch is on. Screw in the side plate with M3 screws. You may use a screwdriver for this step, but do not over-tighten.

    Line up the holes
    Not too tight
    All screws in place

    Microswitch 

    Fit the microswitch in between the plates and line up the mounting holes so that the actuator sticks out the front. The wires should also stick out the top. Place a M2.3 screw through each microswitch mounting hole. Once through, attach a nut to each screw.

    Where the microswitch will be placed
    Holes lined up
    M2.3 placed
    Attach nuts
    Done!

    Vibration Motor 

    You may peel off the tape on the vibration motor and stick it to the center plate. We recommend putting it somewhere on the lower grip for maximum haptic feedback. Once stuck on feed the wires up like you did the microswitch wires.

    Peeling
    Sticking
    Done!

    Other Side Plate 

    You may now take the other side plate and line it up on the other side of the center plate. Use M3 screws to screw it in place. At this point, you can grip the handle and make sure the microswitch is on the side you want it to be. If you'd like to switch it, unscrew the side plates and microswitch, and put it on the preferred side.

    Line up the holes
    Screw one
    Not too tight
    All screws in place

    OLED Mounting 

    Line up the OLED screen with the OLED Mount. Have the OLED pins in line with the slot on top of the mounting piece as shown. Then screw in the OLED with and place an M3 nut on the opposite side.

    The OLED screen and mount
    Insert the screw
    Just hand tighten
    All the screwed are placed

    Slide the OLED Mount onto the back of the handle. Note that this piece may be difficult to remove once in place. Make sure the microswitch and the vibration motor are in place.

    Lining up the OLED
    OLED in socket
    OLED in socket

    Mounting the Arduino and PCB 

    Take the Arduino Mount and screw the four 15mm standoffs in the 4 Arduino mounting holes. Use a M3 nut to hold each one in it's place. Place your Arduino so it's holes line up with the standoffs then screw a 5mm M3 bolt in each one. Once in place, align the finished PCB shield with their mating Arduino headers and press.

    All the pieces to mount to Arduino
    Screwing in a 15mm standoff
    Fastening the standoff
    All fastened
    Lining up the Arduino
    Don't over tighten the screws
    Lining up the PCB
    Ahhhhh perfect

    IMU headers 

    Take the IMU out of its bag. There are two sets of headers, take the straight ones. Place headers from the bottom and solder the tips that go through the board on top. Once soldered, place it in the IMU headers on the PCB. The bulk of the board should hang off the PCB.

    Using the straight headers
    Placed headers
    Solder the IMU

    Motor and microswitch Leads 

    With the PCB in the Arduino and the Arduino on the Arduino Mount, align the Arduino Mount to the rest of the handle, but don't attach it yet. Take the leads for the vibration motor and microswitch and feed them through the middle two of four holes in the Arduino Mount, then into the PCB. Starting with the vibration motor wires, bring them close to the pcb and trim any extra wire. Strip the new ends. Insert the wires into the two holes to the left of the MOSFET. Orientation doesn't matter. Don't make them flush with the PCB since we'll need to solder these from above.

    Thread wires here
    Threaded wires
    Soldering the motor leads
    Soldering the microswitch leads

    Arduino Mount and Battery Pack 

    Place the back of the mount into the back of the handle, then pivot the piece downwards onto the clips on the handle. Apply force on either side of the clips until you hear a 'snap', at which point the handle will be fully assembled.

    Sliding the Arduino Mount
    Pivoting the mount
    Press down
    Snapped in place

    Take the foam double sided tape and attach it to the battery pack. Peel off the other side of the tape and align the battery pack so the barrel jack faces the Arduino.

    Stick one side to the battery pack
    Peel tape and apply to the pack
    Peel tape and apply to Mount
    All done!

    Congratulations. The mechanical assembly is complete and you can move onto software.

    All done!All done!

    Software - Inputs and Outputs

    Hardware Testing and Library Installation 

    Software Walkthrough
    Follow along to test the hardware modules

    The IMU Game uses a few pieces of hardware beyond just the Arduino. We have a vibration motor, the microswitch, an OLED screen, and the IMU itself. This section will test a few of these components.

    OLED Check

    The OLED Screen must be wired to the Arduino. Follow the wiring diagram and table. Your wire colors might be different than the guide.

    OLED Pins Arduino Pins
    GND GND
    VCC 5V
    CLK Pin 13
    MOSI Pin 11
    RES Pin 12
    DC Pin 9
    CS Pin 10
    OLED Wiring
    Arduino Wiring

    The OLED screen needs a library installed. It is called U8glib and is used when using monochromatic displays. To install it open up the Arduino IDE then by using the bar at the top go to Sketch > Include Library > Manage Libraries. In the search bar, type in U8glib. As of writing this, I get the following 3 results; LCDMenu, U8g2, and U8glib. We want the last one so press install.

    With that library installed download this sketch and upload to your Arduino with OLED attached. You should see a rectangle that grows and shrinks on the screen continuously. If it does not appear, use the comments within the code to check your wiring.

    Navigating to Manager
    Installing Library

    IMU

    The IMU needs two libraries. One for the protocol the sensor uses and another for the actual sensor itself. Both however cannot be found in the Arduino IDE Library Manager so installing them are a bit different.

    Download these two zip folders.

    I2Cdev Library

    MPU6050 Library

    To add these libraries to the Arduino IDE we'll again go to the top bar and go to Sketch > Include Library > Add .ZIP Library. Navigate to where you have the folders downloaded and add them one at a time.

    Next we'll check if the gyroscope is working properly. Download this sketch and upload to your Arduino with IMU attached. Open the Serial Monitor under Tools > Serial Monitor in the Arduino software. You should see two numbers, one for yaw and one for pitch. These numbers should correspond to the gyroscope's orientation, so try rotating it left and right (yaw) and up and down (pitch) and see if the numbers change accordingly. If they do change, but somewhat erratically, try pressing the button on the IMU Game PCB and keep the accelerometer level until it completes calibration.

    Ready to go 

    After these tests the hardware is ready to go. Download the full game code here and upload it to IMU game kit.

    All done!
    Congratulations!
    System Breakdown

    Block Diagram 

    At the core of each functional block is a piece of hardware. One step above that is the additional hardware needed to control or interact with the core hardware element. The highest step away from the core is how we interface with it via software. It's how we would use the hardware via code. The blocks themselves are just a portion of how our system works. We always have arrows to indicate the flow of data between these blocks. Let's start at the core of each block and work our way out to the software.

    Blocks show a part of the full functionality of the kit. The other part being the arrows the show the flow of data between the blocks.

    Block Breakdown 

    Hardware Highlighted
    Schematic

    We'll start looking at just those first two layers of our blocks. The pure hardware component layer and the driver or controller layer. Along side the block diagram from earlier is the schematic of the PCB. Then we'll see how software can interface with the driver or controller sub-block.

    Motor

    The Motor Block
    The Motor Block Schematic

    The motor block is composed of the vibration motor and then the support circuitry to drive it. The motor draws too much current to be able to drive it directly from the Arduino. Instead, we are using a MOSFET to act like a switch. If we replace the MOSFET with just a wire (short circuit), the motor would constantly be on as it would be connected right from 5V, through the motor, to ground. The MOSFET is placed between the motor and its connection to ground. When the MOSFET is LOW the gate shuts the flow of electricity to ground and turns off the motor. When set HIGH, electricity is allowed to pass through the MOSFET and then to ground, allowing the motor to run.

    The motor is a pretty simple thing to drive. We set a pin output which in turn sets the MOSFET to drive the motor. LOW being off and HIGH being on. In the sketch below, the pin which the motor driver circuit will use is defined. Then it is turned on for 100 milliseconds and then off for 1000 milliseconds.

    Download file Copy to clipboard
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #define MOTOR_PIN 5
    
    void setup() {
      pinMode(MOTOR_PIN, OUTPUT);
      digitalWrite(MOTOR_PIN,LOW);
    }
    
    void loop() {
      digitalWrite(MOTOR_PIN,HIGH);
      delay(100);
      digitalWrite(MOTOR_PIN,LOW):
      delay(1000);
    }
    

    Buttons

    The Button Block
    The Button Block Schematic

    The calibration button and microswitch are the same type of component. They only complete a circuit when the button or switch is pressed. Even though they are simple they still need some support circuitry. In this case a network of pull-up resistors. We use pull up resistors because they give two distinct state, GND or VCC. Without them the circuit would be GND > Button > pin. And while this would run it won't give you distinct states. When the circuit was closed you would get the logical 0 (GND) but when it was left open you would not be able to be certain it's returning to a logical 1 (VCC). But by using a pull down or up resistor we can guarantee a logical 1 or 0. That's why the calibration button and microswitch each get a 10k resistor. Also by using a resistor, when a connection from VCC to GND is made it isn't a short.

    The code to check if the buttons are pressed is below. It just checks if the value of the pin assigned to that button is either HIGH or LOW and based on that, it turns the LED on the Arduino on or off.

    Download file Copy to clipboard
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #define BUTTON_PIN 4
    
    void setup() {
      pinMode(BUTTON_PIN, INPUT);
      pinMode(13,OUTPUT);
      digitalWrite(13,LOW);
    }
    
    void loop() {
      if(!digitalRead(BUTTON_PIN){
        digitalWrite(13,HIGH);
      } else {
        digitalWrite(13,LOW);
      }
    }
    

    OLED screen

    If you flip over the OLED module you can see a lot of pins coming out of the screen yet we only use 7 pins to interface with it. That is because also on the back of the OLED screen is the SH1106 IC with acts as an intermediary between the screen and the Arduino. We give the SH1106 commands and it deals with writing the pixels to the screen. For more information on the SH1106 check out this datasheet. To make coding the OLED screen even easier we use a library. By using the U8glib library, we can set the pins we are using once and just use a name like screen to make references to the OLED screen in our code. Below is an example of how we do just that. We make an object that holds our screen values called 'screen'. Then in 'void draw(void)' we tell it what to draw. And when we tell it to draw that in the main loop, instructions are sent via the pins defined in the U8glib library to the SH1106 which then drawn the characters to the OLED screen.

    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
    
    #include "U8glib.h"
    
    U8GLIB_SH1106_128X64 screen(13, 11, 10, 9, 12);	// SW SPI Com: SCK = 13, MOSI = 11, CS = 10, A0 = 9
    
    void draw(void) {  
      screen.setFont(u8g_font_unifont);
      screen.drawStr( 0, 22, "Hello World!");
    }
    
    void setup(void) {
    }
    
    void loop(void) {
      // picture loop
      screen.firstPage();  
      do {
        draw();
      } while( screen.nextPage() );
    
      // rebuild the picture after some delay
      //delay(50);
    }
    
    

    IMU

    The IMU Block
    The IMU Block Schematic

    The black square on the IMU pcb has a lot of functionality. It houses the a 3-axis gyroscope and a 3-axis accelerometer. But what's special is it also contains a digital motion processor (DMP). The DMP takes all the data coming from each sensor and uses a process called sensor fusion to make the results more accurate. The DMP also can send us those results in many different formats. To use the DMP we'll need a library called, MPU6050 library. Using this library we can create an object which the can be referenced and called on in the Arduino IDE. In the example we open communication with the DMP on the IMU and ask for some simple gyro and accelerometer data, then write that to the serial monitor.

    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
    
    #include "I2Cdev.h"
    #include "MPU6050.h"
    
    MPU6050 accelgyro;
    
    int16_t ax, ay, az;
    int16_t gx, gy, gz;
    
    void setup() {
        Serial.begin(38400);
    
        // initialize device
        Serial.println("Initializing I2C devices...");
        accelgyro.initialize();
    }
    
    void loop() {
        // read raw accel/gyro measurements from device
        accelgyro.getMotion6(&ax, &ay, &az, &gx, &gy, &gz);
    
        #ifdef OUTPUT_READABLE_ACCELGYRO
            // display tab-separated accel/gyro x/y/z values
            Serial.print("a/g:\t");
            Serial.print(ax); Serial.print("\t");
            Serial.print(ay); Serial.print("\t");
            Serial.print(az); Serial.print("\t");
            Serial.print(gx); Serial.print("\t");
            Serial.print(gy); Serial.print("\t");
            Serial.println(gz);
        #endif
    }
    
    Game State Machine

    Finite State Machines 

    Game State MachineGame State Machine

    The game code is organized in what's called a finite state machine, or FSM. A representation of that is above. States as well as the transitions 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.

    Inside each state is code that pertains to that state. The GAME state contains all the code necessary to run the game and it is only reachable after the IMU game kit is calibrated and the bounds are set, as seen by the path from CALIBRATION > TLBOUND > BRBOUND.

    Software implementation 

    A visual to organize our code is nice but how is the finite state machine actually traversed? In the code we use on variable to track what state we are in, and another to track what the next state will be. To do this in the Arduino IDE, we use a switch case.

    Download file Copy to clipboard
    1
    2
    3
    4
    5
    6
    7
    8
    switch (var) {
        case 1:
          //do something when var equals 1
          break;
        case 2:
          //do something when var equals 2
          break;
      }
    

    Where our 'var' will be the current state variable. The switch will go down the line of cases until it finds the one that fits and runs whatever is inside. So using a switch case as a state machine would look like this.

    Download file Copy to clipboard
    1
    2
    3
    4
    5
    6
    7
    8
    switch (currentState) {
        case stateOne:
          //do state one things
          break;
        case stateTwo:
          //do state two things
          break;
      }
    

    Lets build one.

    A Simple Game 

    We're going to use some of the original functions from the IMU Game kit code and make a new project from scratch. Let's make a simple shape viewer. With a press of the microswitch a circle appears. Another press, another shape.

    State Diagram

    Let's plan out our viewer as a state diagram. When the kit powers up there should be a start screen that waits for you to press the switch. After you press it a shape appears. With each press a new shape. That state diagram looks like this.

    Simple Game StatesSimple Game States

    Open Up a Blank Arduino Sketch

    Now let's formally define what hardware and software libraries we'll need. The microswitch uses pin 7 and doesn't need a library. The pin it uses will have to be set as an input in the setup. The screen needs to be defined and needs a library, U8glib. The font must be set in the setup. For the screen to work some extra code must be placed in the 'loop()' so adding that too makes our code look like this.

    Download file Copy to clipboard
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    #include <U8glib.h>
    U8GLIB_SH1106_128X64 screen(13, 11, 10, 9, 12);
    
    #define SWITCH_PIN 7
    
    void setup() {
      pinMode(SWITCH_PIN, INPUT);
      screen.setFont(u8g_font_6x10);
    }
    
    void loop() {
      screen.firstPage();
      do {
    
      } while ( screen.nextPage() );
    }
    

    Adding in the State Machine

    We need to formally define our states so we can use them in the switch case state machine. Add this code before the setup. We also need variables to keep track of states. Call them currentState and nextState .

    Download file Copy to clipboard
    1
    2
    3
    4
    5
    6
    7
    8
    9
    enum possibleStates {
      START_SCREEN,
      SQUARE,
      CIRCLE,
      DISC
    };
    
    enum possibleStates currentState;
    enum possibleStates nextState;
    

    At the end of the setup function let's set those two variables to equal our first state, START_SCREEN.

    Download file Copy to clipboard
    1
    2
    currentState = START_SCREEN;
    nextState = START_SCREEN;
    

    Now we are going to add the state machine into the main loop() . At the end of the loop() we want to add 'currentState = nextState' so that way we can change state. All together it looks like this.

    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
    void loop() {
      screen.firstPage();
      do {
        switch (currentState) {
          case START_SCREEN:
            //show start screen
            break;
          case CIRCLE:
            //show CIRCLE
            break;
          case SQUARE:
            //show SQUARE
            break;
          case DISC:
            //show DISC
            break;
        }
        //Stuff not in the state machine
        currentState = nextState;
      } while ( screen.nextPage() );
    }
    

    Start Screen

    For this state we want to write to the screen and wait for the microswitch. Writing to the screen consists of telling the SH1106 where we want to write and what we want to write. A simple message like "Press to Start" looks like this. Place this code underneath case START_SCREEN :

    Download file Copy to clipboard
    1
    2
    screen.setPrintPos(1, 20);
    screen.print("Press to Start");
    

    You can play around with the '(1,20)'' section to change where it writes to the screen.

    Tracking Button Input

    The common digitalRead() to track button presses won't give us the full functionality we need for this project. We don't just want on or off. We want the act of pressing or letting go to be our state transition. To do this we need a variable to keep track of whether or not the button is not being held. This goes above setup.

    Download file Copy to clipboard
    1
    bool notHeld = false;
    

    We also need something out side of the state machine to check if this is true or not based on the reading from the microswitch. Adding this code right above 'currentState = nextState;' will do the trick.

    Download file Copy to clipboard
    1
    if(digitalRead(SWITCH_PIN)){ notHeld = true; }
    

    Start Screen Continued

    Now that we can use the button let's add a if statement that if the button is not pressed but it was just held a fraction of a second ago then the user has just lifted their finger off the microswitch and the state should change. The entire STARTSCREEN state looks like this.

    Download file Copy to clipboard
    1
    2
    3
    4
    5
    6
    7
    8
    9
    case START_SCREEN:
            //show start screen
            screen.setPrintPos(1, 20);
            screen.print("Press to Start");
            if (!digitalRead(SWITCH_PIN) && notHeld) {
              notHeld = false;
              nextState = CIRCLE;
            }
            break;
    

    Shape States

    For each shape state we want to print a shape and also change the state if the switch is pressed. So drawing each shape uses this code and reuse the code from the start screen case to change the state.

    Download file Copy to clipboard
    1
    2
    3
    screen.drawCircle(20, 20, 14);
    screen.drawBox(10, 10, 30, 30);
    screen.drawDisc(20, 20, 14);
    

    Adding those draw functions and the button check to each state finishes our shape viewer! The full code is 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
    #include <U8glib.h>
    U8GLIB_SH1106_128X64 screen(13, 11, 10, 9, 12);
    
    #define SWITCH_PIN 7
    
    
    enum possibleStates {
      START_SCREEN,
      SQUARE,
      CIRCLE,
      DISC
    };
    
    enum possibleStates currentState;
    enum possibleStates nextState;
    
    bool notHeld = false;
    
    
    void setup() {
      pinMode(SWITCH_PIN, INPUT);
      screen.setFont(u8g_font_6x10);
    
      currentState = START_SCREEN;
      nextState = START_SCREEN;
    }
    
    void loop() {
      screen.firstPage();
      do {
        switch (currentState) {
          case START_SCREEN:
            //show start screen
            screen.setPrintPos(1, 20);
            screen.print("Press to Start");
            if (!digitalRead(SWITCH_PIN) && notHeld) {
              notHeld = false;
              nextState = CIRCLE;
            }
            break;
          case CIRCLE:
            //show CIRCLE
            screen.drawCircle(20, 20, 14);
            if (!digitalRead(SWITCH_PIN) && notHeld) {
              notHeld = false;
              nextState = SQUARE;
            }
            break;
          case SQUARE:
            //show SQUARE
            screen.drawBox(10, 10, 30, 30);
            if (!digitalRead(SWITCH_PIN) && notHeld) {
              notHeld = false;
              nextState = DISC;
            }
            break;
          case DISC:
            //show DISC
            screen.drawDisc(20, 20, 14);
            if (!digitalRead(SWITCH_PIN) && notHeld) {
              notHeld = false;
              nextState = CIRCLE;
            }
            break;
        }
        //Stuff not in the state machine
        if (digitalRead(SWITCH_PIN)) {
          notHeld = true;
        }
        currentState = nextState;
      } while ( screen.nextPage() );
    }
    
    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.