Dragon Simulator 2015

Game Design, Programming, Music, Sound Design

"Dragon Simulator 2015" was my project for the Arduino Games course at Shenkar College. As the name implies, it simulates the life of your typical dragon - the player may fly around the endless (looping) 3D game world and breath fire at the occasional village.

I have been interested in the notion of custom, game-specific controllers since glimpsing such Japanese arcade machines on TV, and am generally fond of games such as Taiko Drum Master and the Initial-D racing game. Mechanically these games could be played with other, more conventional controllers, which is true for most games nowadays due to how sophisticated standard controllers have become, but the custom controllers do much for the suspension of disbelief and make the experience far more intense. Naturally I wanted to spend my time in the Arduino course building some kind of custom controller tailored around a game that it would benefit in a similar fashion.

The game uses a custom control setup consisting of two motion sensors and a microphone hooked up through an Arduino board, and an Oculus Rift DK2 VR headset. The motion sensors track the player's arms to direct the dragon's flight, as if they were wings, the headset detects the head motion (and therefore, the aim) and the microphone (which hangs from the headset) detects the player's blowing, to allow for an immersive flying and fire-breathing interface.

This game also has a dynamic music system. I composed an original background piece for this game which is split into six synchronized tracks. The tracks are placed at different heights in 3D space and in different directions relative to the player character, but otherwise travel with them. As the player changes height and direction of flight, the tracks seamlessly fade in and out while maintaining their "compass" direction. This serves two purposes. The difference in musical texture and harmony in different heights parallels the different kind of gameplay the player may experience there, with intense brass and harpsichord riffs when the player is near ground level and probably causing havoc, and chill violin solos and electronic backing when the player is up in the clouds. The constant compass direction of each track relative to the player is also meant to ease player orientation in the looping 3D space.

Code? Really? Keep scrolling...

 Note the microphone hanging from the headset

Note the microphone hanging from the headset

2015-03-30 21.57.39.jpg

Prototype Gameplay

The code for the Arduino board is as follows:

#include "I2Cdev.h"

#include "MPU6050_6Axis_MotionApps20.h"

#if I2CDEV_IMPLEMENTATION == I2CDEV_ARDUINO_WIRE
    #include "Wire.h"
#endif

MPU6050 mpu(0x69); // <-- use for AD0 high

#define OUTPUT_READABLE_YAWPITCHROLL

#define LED_PIN 13 // (Arduino is 13, Teensy is 11, Teensy++ is 6)
bool blinkState = false;

// 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 (0 = success, !0 = error)
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]         quaternion container
VectorInt16 aa;         // [x, y, z]            accel sensor measurements
VectorInt16 aaReal;     // [x, y, z]            gravity-free accel sensor measurements
VectorInt16 aaWorld;    // [x, y, z]            world-frame accel sensor measurements
VectorFloat gravity;    // [x, y, z]            gravity vector
float euler[3];         // [psi, theta, phi]    Euler angle container
float ypr[3];           // [yaw, pitch, roll]   yaw/pitch/roll container and gravity vector

// packet structure for InvenSense teapot demo
uint8_t teapotPacket[14] = { '$', 0x02, 0,0, 0,0, 0,0, 0,0, 0x00, 0x00, '\r', '\n' };

// ================================================================
// ===               INTERRUPT DETECTION ROUTINE                ===
// ================================================================

volatile bool mpuInterrupt = false;     // indicates whether MPU interrupt pin has gone high
void dmpDataReady() {
    mpuInterrupt = true;
}

// ================================================================
// ===                      MICROPHONE STUFF                    ===
// ================================================================

//micstuff
int ledPin = 13;
int soundSensorPin = A0;

double nowLoud = 0; //most recent mic reading
double lowLoud = 0; //current lowest mic reading in sample set
double highLoud = 0; //current highest mic reading in sample set
double diffLoud = 0; //highLoud - lowLoud = actual volume level. 

double lowDiffLoud =0; //lowest diff over sound dynamic sample cycles
double highDiffLoud =0; //highest diff over sound dynamic sample cycles
double diffDiffLoud =0; //highDiffLoud - lowDiffLoud = range of volume. Used later to reset the ranges


// starting audio levels. These initialization numbers are arbitrary, and dynamically adjusted at the end of the K loop
int levelOne = 0;
int levelTwo = 1;
int levelThree = 20;
int levelFour = 30;

int lastLevel = 0; // remember which level you are fading from

void(* resetFunc) (void) = 0;//declare reset function at address 0

// ================================================================
// ===                      INITIAL SETUP                       ===
// ================================================================

void setup() {
    // join I2C bus (I2Cdev library doesn't do this automatically)
    #if I2CDEV_IMPLEMENTATION == I2CDEV_ARDUINO_WIRE
         boolean error = true;
    while (error) {
        Wire.beginTransmission(105);
        error = Wire.endTransmission(); // if error = 0, we are properly connected
        if (error) { // if we aren't properly connected, try connecting again and loop
          //Serial.println("  ");
          //Serial.println("Not properly connected to I2C, trying again");
          //Serial.println(" ");
          Wire.begin();
          TWBR = 24; // 400kHz I2C clock
        }
    }
    //Serial.println("Properly connected to I2C");
        TWBR = 24; // 400kHz I2C clock (200kHz if CPU is 8MHz)
    #elif I2CDEV_IMPLEMENTATION == I2CDEV_BUILTIN_FASTWIRE
        Fastwire::setup(400, true);
    #endif

    // initialize serial communication
    // (115200 chosen because it is required for Teapot Demo output, but it's
    // really up to you depending on your project)
    Serial.begin(9600);
    while (!Serial); // wait for Leonardo enumeration, others continue immediately

    // initialize device
    //Serial.println(F("Initializing I2C devices..."));
    mpu.initialize();

    // load and configure the DMP
    //Serial.println(F("Initializing DMP..."));
    devStatus = mpu.dmpInitialize();

    // supply your own gyro offsets here, scaled for min sensitivity
    mpu.setXGyroOffset(220);
    mpu.setYGyroOffset(76);
    mpu.setZGyroOffset(-85);
    mpu.setZAccelOffset(1788); // 1688 factory default for my test chip

    // make sure it worked (returns 0 if so)
    if (devStatus == 0) {
        // turn on the DMP, now that it's ready
        //Serial.println(F("Enabling DMP..."));
        mpu.setDMPEnabled(true);

        // enable Arduino interrupt detection
        //Serial.println(F("Enabling interrupt detection (Arduino external interrupt 0)..."));
        attachInterrupt(0, dmpDataReady, RISING);
        mpuIntStatus = mpu.getIntStatus();

        // set our DMP Ready flag so the main loop() function knows it's okay to use it
        //Serial.println(F("DMP ready! Waiting for first interrupt..."));
        dmpReady = true;

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

    // configure LED for output
    pinMode(LED_PIN, OUTPUT);
}

// ================================================================
// ===                    MAIN PROGRAM LOOP                     ===
// ================================================================

void loop() {
    // if programming failed, don't try to do anything
    if (!dmpReady) return;

    // wait for MPU interrupt or extra packet(s) available
    while (!mpuInterrupt && fifoCount < packetSize) {
  
    }

    // reset interrupt flag and get INT_STATUS byte
    mpuInterrupt = false;
    mpuIntStatus = mpu.getIntStatus();

    // get current FIFO count
    fifoCount = mpu.getFIFOCount();

    // check for overflow (this should never happen unless our code is too inefficient)
    if ((mpuIntStatus & 0x10) || fifoCount == 1024) {
        // reset so we can continue cleanly
        mpu.resetFIFO();
        //Serial.println(F("FIFO overflow!"));
        //resetFunc(); //call reset 
        

    // otherwise, check for DMP data ready interrupt (this should happen frequently)
    } else if (mpuIntStatus & 0x02) {
        // wait for correct available data length, should be a VERY short wait
        while (fifoCount < packetSize) fifoCount = mpu.getFIFOCount();

        // read a packet from FIFO
        mpu.getFIFOBytes(fifoBuffer, packetSize);
        
        // track FIFO count here in case there is > 1 packet available
        // (this lets us immediately read more without waiting for an interrupt)
        fifoCount -= packetSize;
        
    //mic processing loop ----------------------------------------------------------------------------------------------------------
        
  for (int k=0; k<20; k++) { //after 100 sample cycles, re-adjust the levels to more closely reflect the ambient noise

    for (int j=0; j<20; j++){   //sample sound 100 times to get solid reading on 'loudness'
      
      nowLoud = analogRead(soundSensorPin); // this picks up amplitude, not volume
      
      if(j==0){ //the first time through,J will be 0, so initialize these to something useful - nowLoud will do
        lowLoud = nowLoud; 
        highLoud = nowLoud;
      }
      
      if (nowLoud < lowLoud) { //if we've found a new low, keep track
        lowLoud = nowLoud; 
      } 
           
      if (nowLoud > highLoud){ //if we've found a new high, keep track
        highLoud = nowLoud; 
      }
      
    } //END TAKE 100 AUDIO LEVEL SAMPLES to find volume
  
  diffLoud = highLoud - lowLoud;
  
  if(k==0){ //the first time through, initialize these to something within the actual range
    lowDiffLoud = diffLoud; //we use diffLoud instead of nowLoud b/c we set the levels based on diff
    highDiffLoud = diffLoud;
  }
  if (diffLoud < lowDiffLoud) { //if we've found a new low, keep track
    lowDiffLoud = diffLoud; 
  } 
  if (diffLoud > highDiffLoud){ //if we've found a new high, keep track
    highDiffLoud = diffLoud; 
  }
  
 }
 
  //end mic processing loop -------

        #ifdef OUTPUT_READABLE_YAWPITCHROLL
            // display Euler angles in degrees
            mpu.dmpGetQuaternion(&q, fifoBuffer);
            mpu.dmpGetGravity(&gravity, &q);
            mpu.dmpGetYawPitchRoll(ypr, &q, &gravity);
          /*  Serial.print("ypr\t");
            Serial.print(ypr[0] * 180/M_PI);
            Serial.print("\t");
            Serial.print(ypr[1] * 180/M_PI);
            Serial.print("\t");
            Serial.println(ypr[2] * 180/M_PI); */
        #endif
        
        Serial.flush();
        Serial.print(ypr[0] * 180/M_PI);
        Serial.print(",");
        Serial.print(ypr[1] * 180/M_PI);
        Serial.print(",");
        Serial.print(ypr[2] * 180/M_PI);
        Serial.print(",");
        Serial.print(highDiffLoud);
        Serial.println();

        // blink LED to indicate activity
        blinkState = !blinkState;
        digitalWrite(LED_PIN, blinkState);
        
        delay(1);
    }
}