27 April 2026

Team 6 final contoller - Star Wars Squadrons

picture:
Description: Our controller is a blend with a Yolk and a tie fighter controller to give a driving sense to the player in Star Wars: Squadrons. This is a game that takes the space dog fighting to the absolute limit, giving everything to make you feel like you are in a cockpit. This controller is made to thoughtfully organize all those mechanics into a system not too dissimilar form flying the ships in the game. the exterior was 3D printed to slide and pop in and out its mechanisms to make its wiring easier. the basic translations are simple, steer left and right to spin, and pull and push to go up and down. we figured "yaw" going left and right could be pulled out in light of other mechanics, and to make the player adapt more flying habits form the controller itself. the other buttons are laid out in convenient areas where the player hsodl quickly get an understanding of how to use them, and some of those mechanics are fun to work with based on the buttons. the right light sensor on the handle is for firing, and the left handle is the toggle for targeting. turning it will spin the vehicle and pushing bakc and forth will effect the "pitch" allowing the ship to go up or down. on the right of the main section there is a switch to turn on sub-light, the highest speed to get out of a pinch, and in the left a thumbstick, to regulate the dynamic speed you surf with in your dogfighting the question what we woudl have is what ocudl we do for the physical design to improve the weight or feel of the movement the skimalite:
the code: //Final Group 6 //Marc Walker, Nicholas Smith #include #include #include #include #include #include #include #include #include #include #include #include //declare variables for analog read values const int LHSensorPin = A0; int LHSensorLight;//NEEDS TO BE INITIALIZED const int RHSensorPin = A1; const int LSensorPin = A4; const int RSensorPin = A5; //const int LPotPin = A4; //const int RPotPin = A5; const int throttlePin = A6; const int LSwitchPin = A7; const int distanceTrigPin = A2; const int distanceEchoPin = A3; void setup() { // start CircuitPlayground and Serial Monitor CircuitPlayground.begin(); Keyboard.begin(); pinMode(LSwitchPin, INPUT_PULLUP); pinMode(distanceTrigPin, OUTPUT); pinMode(distanceEchoPin, INPUT); Serial.begin(9600); delay(1000); } void loop() { if(CircuitPlayground.slideSwitch()) { int LHSensorInput = analogRead(LHSensorPin); int RHSensorInput = analogRead(RHSensorPin); int LSensorInput = analogRead(LSensorPin); int RSensorInput = analogRead(RSensorPin); int LSThreshhold = 200;//Threshhold to trigger left light sensor buttons int throttle = analogRead(throttlePin);//0 to 1023 int LSwitchInput = digitalRead(LSwitchPin); digitalWrite(distanceTrigPin, LOW); delayMicroseconds(2); digitalWrite(distanceTrigPin, HIGH); delayMicroseconds(10); digitalWrite(distanceTrigPin, LOW); long duration = pulseIn(distanceEchoPin, HIGH, 30000); static int lastDistance = 0; if (duration > 0) { lastDistance = duration * 0.034 / 2; } int distance = lastDistance; bool LSwitch; //float RPot = analogRead(RPotPin);//Right Potentiometer //float LPot = analogRead(LPotPin);//Left Potentiometer int shields;//Read value of shields mapped between 0 and 100 int speed;//Read value of speed mapped between 0 and 100 int laserStrength;//Read value of laserStrength mapped between 0 and 100 float vertical; float horizontal; //vertical = Get CPE accelerometer Y value; //horizontal = Get CPE accelerometer X value; //shields = LPot; //speed = RPot; //laserStrength = (shields + laserStrength) / 2; //bool LButton = true if Left button is pressed; //bool RButton = true if Right button is pressed; bool LHButton; bool RHButton; bool LButton; bool RButton; //bool LSwitch = true if Left switch is flipped; //bool RSwitch = true if Left switch is flipped; if (LHSensorInput <= LSThreshhold) { LHButton = true; } else { LHButton = false; } if (RHSensorInput <= LSThreshhold) { RHButton = true; } else { RHButton = false; } if (LSensorInput <= LSThreshhold) { LButton = true; } else { LButton = false; } if (RSensorInput <= LSThreshhold) { RButton = true; } else { RButton = false; } if (LSwitchInput == LOW) { LSwitch = true; } else { LSwitch = false; } Serial.print("Distance: "); Serial.println(distance); /*Serial.print("Distance Sensor: "); Serial.println(distance);*/ /*Serial.print("L Sensor: "); Serial.println(LSensorInput); Serial.print("LH Sensor: "); Serial.println(LHSensorInput); Serial.print("RH Sensor: "); Serial.println(RHSensorInput); Serial.print("R Sensor: "); Serial.println(RSensorInput);*/ if (throttle < 250)//INCREASE THRUST { Keyboard.press('W'); } else { Keyboard.release('W'); } if (throttle > 750)//DECREASE THRUST { Keyboard.press('S'); } else { Keyboard.release('S'); } float accelDeadzone = 2; float posX = CircuitPlayground.motionX(); //Serial.println(posX); if(posX > accelDeadzone)//TURN LEFT { Keyboard.press('A'); //Serial.println("WE GO LEFT"); Keyboard.release('D'); } else if(posX < -accelDeadzone)//TURN RIGHT { Keyboard.press('D'); //Serial.println("WE GO RIGHT"); Keyboard.release('A'); } if(RHButton)//Shoot { Keyboard.press('K');//Rebind shoot to K } else { Keyboard.release('K'); } if(LHButton)//Target { Keyboard.press('T'); } else { Keyboard.release('T'); } if(LButton)//Fire Left Aux { Keyboard.press('Q'); } else { Keyboard.release('Q'); } if(RButton)//Fire Right Aux { Keyboard.press('E');//Rebind shoot to K } else { Keyboard.release('E'); } if(LSwitch) { Keyboard.press(' '); } else { Keyboard.release(' '); } //Keyboard.releaseAll(); delay(100); } else { Keyboard.releaseAll(); } } the video:

Team 16 - Final Controller FNAF Tablet

 For our controller, we built a tablet design controller to mimic the camera display for the game Five Nights at Freddy's. Our original concept included light switches for the lights and slide switches for the doors. Our final project has 11 photoresistors on the top of our controller, each of which individually opens one of the 11 cameras displayed in FNAF. There are two toggle light switches on each side of the “tablet”. There is also a digital button on the front of the “tablet” that allows the player to enable and disable the pop-up monitor, where the player can then switch between cameras. We wanted to have a controller that a clear and easy to understand game to controller connection. Each of the cameras is mapped within the controller to match the in-game display. The controller allows the player to experience FNAF like never before, without the use of a mouse, and with intuitive controls for the left and right side controls. The format of the controller allows for a strong visual-to-physical control scheme. Does the visuals of the tablet support the in-game controls?


Jonathan Tsigas - Designed and sketched the prototype, then designed the final controller in Tinkercad. While working on the prototype and final, Jonathan handled many of the physical tasks of shaping, cutting, and screwing in the hardware to fit inside. Jonathan also did much of the write-up and final assignment delivery. 


Mason Kuhn - Handled the wiring and programming for the prototype and final controller. Mason soldered and wired the breadboard for the prototype and the circuit board for the final. He also wrote and tested the code to work with the Circuit Playground. 


Austin Covert - ?


Board 1 Schematic

Board 2 Schematic






#include <Adafruit_CircuitPlayground.h>
#include <Keyboard.h>

// --- LIGHT SWITCH (digital) ---
const int LIGHT_SWITCH_PIN = A0;

// --- CAMERA PHOTORESISTORS ---
const int PHOTO_PINS[] = {A1, A2, A3, A4, A5, A6};
const int NUM_CAMERAS = 6;
const char CAMERA_KEYS[] = {'1', '2', '3', '4', '5', '6'};
const int CAM_THRESHOLD = 600;

// --- DOOR SLIDER ---
const int PHOTO_DOOR_PIN = A7;
const int DOOR_THRESHOLD = 600;

// --- TIMING ---
const unsigned long DEBOUNCE_MS = 400;
unsigned long lastCamTime[6] = {0};

// --- STATE ---
bool lastCamState[6] = {false, false, false, false, false, false};
bool lastLightLeft = false;

void tapKey(char key) {
Keyboard.press(key);
delay(50);
Keyboard.releaseAll();
}

void setup() {
// Serial.begin(9600);
CircuitPlayground.begin();
Keyboard.begin();

pinMode(LIGHT_SWITCH_PIN, INPUT_PULLUP);

lastLightLeft = digitalRead(LIGHT_SWITCH_PIN) == LOW;

for (int i = 0; i < NUM_CAMERAS; i++) {
lastCamState[i] = analogRead(PHOTO_PINS[i]) > CAM_THRESHOLD;
}

// Serial.println("Board 1 Ready");
// Serial.print("Door raw: "); Serial.println(analogRead(PHOTO_DOOR_PIN));
}

void loop() {
unsigned long now = millis();

// --- DEBUG READINGS ---
static unsigned long lastPrintTime = 0;
if (now - lastPrintTime > 500) {
// Serial.println("--- Analog Readings ---");
// Serial.print("Light (A0): "); Serial.println(digitalRead(LIGHT_SWITCH_PIN));
// Serial.print("Cam1 (A1): "); Serial.println(analogRead(PHOTO_PINS[0]));
// Serial.print("Cam2 (A2): "); Serial.println(analogRead(PHOTO_PINS[1]));
// Serial.print("Cam3 (A3): "); Serial.println(analogRead(PHOTO_PINS[2]));
// Serial.print("Cam4 (A4): "); Serial.println(analogRead(PHOTO_PINS[3]));
// Serial.print("Cam5 (A5): "); Serial.println(analogRead(PHOTO_PINS[4]));
// Serial.print("Cam6 (A6): "); Serial.println(analogRead(PHOTO_PINS[5]));
// Serial.print("Door (A7): "); Serial.println(analogRead(PHOTO_DOOR_PIN));
// Serial.println("-----------------------");
lastPrintTime = now;
}

// --- LEFT LIGHT SWITCH ---
bool lightOn = (digitalRead(LIGHT_SWITCH_PIN) == LOW);
if (lightOn != lastLightLeft) {
tapKey('a');
// Serial.println(lightOn ? "Left light ON" : "Left light OFF");
lastLightLeft = lightOn;
}

// --- CAMERA PHOTORESISTORS ---
for (int i = 0; i < NUM_CAMERAS; i++) {
bool covered = analogRead(PHOTO_PINS[i]) > CAM_THRESHOLD;

if (covered && !lastCamState[i]) {
if (now - lastCamTime[i] > DEBOUNCE_MS) {
tapKey(CAMERA_KEYS[i]);
// Serial.print("Camera: "); Serial.println(CAMERA_KEYS[i]);

lastCamTime[i] = now;
}
}
lastCamState[i] = covered;
}

// --- LEFT DOOR ---
const unsigned long DOOR_COOLDOWN_MS = 2000;
static unsigned long lastDoorTrigger = 0;
static bool doorArmed = true;

int doorReading = analogRead(PHOTO_DOOR_PIN);

if (doorArmed && doorReading > DOOR_THRESHOLD && (now - lastDoorTrigger > DOOR_COOLDOWN_MS)) {
tapKey('q');
// Serial.println("Left door CLOSED");
lastDoorTrigger = now;
doorArmed = false;
}

if (!doorArmed && doorReading < 720) {
doorArmed = true;
}

delay(10);
}
#include <Adafruit_CircuitPlayground.h>
#include <Keyboard.h>

// --- LIGHT SWITCH (digital) ---
const int LIGHT_SWITCH_PIN = A0;

// --- SPACEBAR BUTTON (digital) ---
const int SPACE_BUTTON_PIN = A1;

// --- CAMERA PHOTORESISTORS ---
const int PHOTO_PINS[] = {A2, A3, A4, A5, A6};
const int NUM_CAMERAS = 5;
const char CAMERA_KEYS[] = {'7', '8', '9', 'i', 'o'};
const int CAM_THRESHOLD = 600;

// --- DOOR SLIDER ---
const int PHOTO_DOOR_PIN = A7;
const int DOOR_THRESHOLD = 600;

// --- TIMING ---
const unsigned long DEBOUNCE_MS = 400;
unsigned long lastCamTime[5] = {0};
unsigned long lastSpaceTime = 0;

// --- STATE ---
bool lastCamState[5] = {false, false, false, false, false};
bool lastLightRight = false;
bool lastSpaceState = HIGH;

void tapKey(char key) {
Keyboard.press(key);
delay(50);
Keyboard.releaseAll();
}

void setup() {
// Serial.begin(9600);
CircuitPlayground.begin();
Keyboard.begin();

pinMode(LIGHT_SWITCH_PIN, INPUT_PULLUP);
pinMode(SPACE_BUTTON_PIN, INPUT_PULLUP);

lastLightRight = digitalRead(LIGHT_SWITCH_PIN) == LOW;

for (int i = 0; i < NUM_CAMERAS; i++) {
lastCamState[i] = analogRead(PHOTO_PINS[i]) > CAM_THRESHOLD;
}

// Serial.println("Board 2 Ready");
// Serial.print("Door raw: "); Serial.println(analogRead(PHOTO_DOOR_PIN));
}

void loop() {
unsigned long now = millis();

// --- DEBUG READINGS ---
static unsigned long lastPrintTime = 0;
if (now - lastPrintTime > 500) {
// Serial.println("--- Analog Readings ---");
// Serial.print("Light (A0): "); Serial.println(digitalRead(LIGHT_SWITCH_PIN));
// Serial.print("Space (A1): "); Serial.println(digitalRead(SPACE_BUTTON_PIN));
// Serial.print("Cam1 (A2): "); Serial.println(analogRead(PHOTO_PINS[0]));
// Serial.print("Cam2 (A3): "); Serial.println(analogRead(PHOTO_PINS[1]));
// Serial.print("Cam3 (A4): "); Serial.println(analogRead(PHOTO_PINS[2]));
// Serial.print("Cam4 (A5): "); Serial.println(analogRead(PHOTO_PINS[3]));
// Serial.print("Cam5 (A6): "); Serial.println(analogRead(PHOTO_PINS[4]));
// Serial.print("Door (A7): "); Serial.println(analogRead(PHOTO_DOOR_PIN));
// Serial.println("-----------------------");
lastPrintTime = now;
}

// --- RIGHT LIGHT SWITCH ---
bool lightOn = (digitalRead(LIGHT_SWITCH_PIN) == LOW);
if (lightOn != lastLightRight) {
tapKey('l');
// Serial.println(lightOn ? "Right light ON" : "Right light OFF");
lastLightRight = lightOn;
}

// --- SPACEBAR BUTTON ---
bool spacePressed = (digitalRead(SPACE_BUTTON_PIN) == LOW);
if (spacePressed && lastSpaceState == HIGH) {
if (now - lastSpaceTime > 500) {
tapKey(' ');
// Serial.println("Space pressed");
lastSpaceTime = now;
}
}
lastSpaceState = spacePressed ? LOW : HIGH;

// --- CAMERA PHOTORESISTORS ---
for (int i = 0; i < NUM_CAMERAS; i++) {
bool covered = analogRead(PHOTO_PINS[i]) > CAM_THRESHOLD;

if (covered && !lastCamState[i]) {
if (now - lastCamTime[i] > DEBOUNCE_MS) {
tapKey(CAMERA_KEYS[i]);
// Serial.print("Camera: "); Serial.println(CAMERA_KEYS[i]);

lastCamTime[i] = now;
}
}
lastCamState[i] = covered;
}

// --- RIGHT DOOR ---
const unsigned long DOOR_COOLDOWN_MS = 2000;
static unsigned long lastDoorTrigger = 0;
static bool doorArmed = true;

int doorReading = analogRead(PHOTO_DOOR_PIN);

if (doorArmed && doorReading > DOOR_THRESHOLD && (now - lastDoorTrigger > DOOR_COOLDOWN_MS)) {
tapKey('p');
// Serial.println("Right door CLOSED");
lastDoorTrigger = now;
doorArmed = false;
}

if (!doorArmed && doorReading < 720) {
doorArmed = true;
}

delay(10);
}



Team 21 - Final Controller: Getting Over it

 Image:


Video:


Description:

Our controller is a physical sledgehammer built to match the tool at the center of Getting Over It with Bennett Foddy, a game about perseverance, frustration, and the act of struggling with a simple object. The hammer is constructed from a PVC pipe handle and a 3D printed head, giving it the weight, shape, and feel of the real thing. The Circuit Playground Express is housed inside the head, keeping the exterior clean.

The conceptual model is simple: you are holding a hammer, and the hammer controls the game. When you tilt it, the cursor moves. When you click the button, it clicks. There is no hidden logic to learn or mental translation required. The idea is that anyone who picks it up should immediately understand what to do, because it behaves the way a physical tool in your hand should behave. A button mounted on the side of the handle acts as a left click for navigating menus. Its placement on the handle keeps it accessible during natural grip without interrupting play. A potentiometer, also mounted on the handle, controls pause and unpause: turn it one direction to pause, the other to resume.

The signifiers here are largely physical. The hammer's form tells you to grip it, tilt it, and interact with it as you would any real tool. The side mounted controls are within thumb reach, signaling that they are supplementary to the primary tilt input rather than competing with it. Feedback is delivered through on screen response: the cursor moves as you tilt, reinforcing the tilt to motion mapping immediately. Together, the design argues that the controller should feel like an extension of the game's theme: deliberate, physical, and a little absurd.

Peer review: Through trial and error, we were able to adjust the sensitivity of the hammer for the mouse movement, but there is no way to adjust this set up outside of the code. How would you go about adjusting the sensitivity/calibrating the hammer while playing?




Schematic:



Code:

#include <Keyboard.h>
#include <KeyboardLayout.h>
#include <Adafruit_CircuitPlayground.h>
#include <Mouse.h>
#include <Wire.h>
#include <SPI.h>

int buttonPin = A3; // button for left click
//int shakeThresh = 40; // shake for pause, shake removed

int potPin = A2; // potentiometer pin
int threshold = 800; // threshold for pause on pot
bool wasAbove = false; // state for potentiometer so esc is inputted both ways

// mouse settings
// x movement settings
#define X_MIN 0.1
#define X_MAX 8.0
#define X_SPEED 25.0
#define X_SCALE 1.5

// y movement settings
#define Y_MIN X_MIN
#define Y_MAX X_MAX
#define Y_SPEED X_SPEED
#define Y_SCALE 1.5

// Flip X and Y if needed
#define FLIP false // false makes up up, true is right up

// map a value from one range to another
float mapValue(float val, float inMin, float inMax, float outMin, float outMax) {
  if (val <= inMin) return outMin;
  if (val >= inMax) return outMax;
  return outMin + (outMax - outMin) * ((val - inMin) / (inMax - inMin));
}

void setup() {
  CircuitPlayground.begin();
  pinMode(buttonPin, INPUT_PULLUP); // start button
  Mouse.begin();             // start mouse control
  Serial.begin(9600);
  delay(1000);
}

void loop() {


  if (!CircuitPlayground.slideSwitch()) { // if switch is off controller is off
    return;
  }
  //button state removed with new button and new logic
  //bool button1 = digitalRead(buttonPin);
 
  float xTilt = CircuitPlayground.motionX(); // get values for which way its tilted
  float yTilt = CircuitPlayground.motionY();
 
  float xMove = mapValue(abs(xTilt), X_MIN, X_MAX, 0.0, X_SPEED); // turn tilt into speed
  float yMove = mapValue(abs(yTilt), Y_MIN, Y_MAX, 0.0, Y_SPEED);

  if (xTilt < 0) xMove *= -1; // direction of tilt
  if (yTilt < 0) yMove *= -1;
  xMove *= -1; // make sure right is right left is left

  // scale
  xMove = floor(xMove * X_SCALE);
  yMove = floor(yMove * Y_SCALE);

  // move mouse
  if (!FLIP) {
    Mouse.move((int)xMove, (int)yMove, 0);
  } else {
    Mouse.move((int)yMove, (int)xMove, 0);
  }

  delay(10); // lil delay

  // get button state later|removed with new button
  // bool button2 = digitalRead(buttonPin);
 
  // left click
  if (digitalRead(buttonPin) == LOW) {
  Mouse.press(MOUSE_LEFT);
} else {
  Mouse.release(MOUSE_LEFT);
}

 // escape button
  static bool wasPaused = false;
      int potValue = analogRead(potPin);

    bool isAbove = potValue > threshold;

   
    if (isAbove != wasAbove) { // if crossed the threshold (either direction)
      Keyboard.press(KEY_ESC);
      delay(50);
      Keyboard.release(KEY_ESC);
    }

    wasAbove = isAbove; // update da state
}




Team 17 - Resident Evil Remastered Final Controller

    Our project is a controller modeled after a stun gun inspired by Resident Evil. The main idea is to make it feel like you’re actually holding and using something from the game instead of a normal controller. It’s supposed to feel more like an extension of the character instead of something separate, which matches the slower, more tense pacing of survival horror. The whole design is about making actions feel more intentional by using more physical inputs instead of just quickly pressing buttons.

    The input to output mapping uses a mix of buttons, switches, and light sensors. Movement is controlled with buttons for forward and backward, and turning is handled with a potentiometer. The light sensors are along the front of the controller, where the top one is for shooting, the middle one readies the weapon, and the bottom one is for interacting. So instead of just pressing a button, you have to actually move or position the controller to do those things. There’s also a switch for running and a tilt input for quick turning. Opening the inventory and using menus works through combinations of those same inputs, so everything stays consistent but still feels a little different from a normal controller.

    The signifiers mostly come from the shape and layout since it looks like a stun gun and kind of shows how you’re supposed to hold and use it. Feedback comes from how the game reacts to what you do, like movement, attacking, or menu navigation, so you can tell if something worked or not. All of this ties back to the theme by making you more aware of your actions, which helps keep that tense, controlled Resident Evil feeling. 

What are some ways we could make the controls feel more natural without losing the slower, more intentional pacing of the game?


(Final controller)

(Schematic)

#include <Adafruit_CircuitPlayground.h>
#include <Keyboard.h>
#include <Mouse.h>

// =====================================================
// Pin assignments
// =====================================================
const int INTERACT_LIGHT_PIN = A0;   // interact
const int BUTTON1_PIN        = A1;   // forward
const int BUTTON2_PIN        = A2;   // backward
const int TOGGLE_PIN         = A3;   // sprint
const int POT_PIN            = A4;   // left / middle / right
const int TILT_PIN           = A5;   // quick turn
const int AIM_LIGHT_PIN      = A6;   // aim/ready weapon
const int SHOOT_LIGHT_PIN    = A7;   // shoot

// =====================================================
// Keyboard bindings
// =====================================================
const char KEY_FORWARD    = 'w';
const char KEY_BACKWARD   = 's';
const char KEY_TURN_LEFT  = 'a';
const char KEY_TURN_RIGHT = 'd';
const char KEY_QUICK_TURN = 'q';
const char KEY_INTERACT   = 'f';
const char KEY_SELECT     = 'e';

const uint8_t KEY_SCROLL_UP   = KEY_UP_ARROW;
const uint8_t KEY_SCROLL_DOWN = KEY_DOWN_ARROW;
const uint8_t KEY_MENU_LEFT   = KEY_LEFT_ARROW;
const uint8_t KEY_MENU_RIGHT  = KEY_RIGHT_ARROW;
const uint8_t KEY_SPRINT      = KEY_LEFT_SHIFT;
const uint8_t KEY_MENU_ESC    = KEY_ESC;

// =====================================================
// Sensor baselines
// =====================================================
int aimLightBaseline = 0;
int shootLightBaseline = 0;
int interactLightBaseline = 0;

// =====================================================
// Thresholds
// =====================================================
const int POT_LEFT_MAX         = 400;
const int POT_RIGHT_MIN        = 700;

const int AIM_DARK_THRESH      = -80;
const int SHOOT_DARK_THRESH    = -80;
const int INTERACT_DARK_THRESH = -80;

// =====================================================
// Timing / debounce
// =====================================================
const unsigned long MENU_HOLD_TIME       = 3000;
const unsigned long MENU_REARM_TIME      = 1500;

const unsigned long SHOOT_COOLDOWN       = 150;
const unsigned long INTERACT_COOLDOWN    = 1200;
const unsigned long SELECT_COOLDOWN      = 300;

const unsigned long TILT_HOLD_TIME       = 150;
const unsigned long TILT_COOLDOWN        = 700;

// =====================================================
// Menu state
// =====================================================
bool menuOpen = false;

// =====================================================
// Hold / cooldown tracking
// =====================================================
unsigned long bothButtonsStartTime = 0;
bool bothButtonsTiming = false;
unsigned long lastMenuToggleTime = 0;

unsigned long lastShootTime = 0;
unsigned long lastInteractTime = 0;
unsigned long lastSelectTime = 0;

unsigned long tiltStartTime = 0;
unsigned long lastTiltTriggerTime = 0;
bool tiltTiming = false;
bool tiltUsed = false;

bool interactTriggeredAlready = false;

// =====================================================
// Keyboard state tracking
// =====================================================
bool forwardHeld   = false;
bool backwardHeld  = false;
bool leftHeld      = false;
bool rightHeld     = false;
bool sprintHeld    = false;

bool scrollUpHeld   = false;
bool scrollDownHeld = false;
bool menuLeftHeld   = false;
bool menuRightHeld  = false;

// =====================================================
// Mouse state tracking
// =====================================================
bool aimMouseHeld = false;

// =====================================================
// Helper key/mouse press functions
// =====================================================
void holdKey(char key, bool &stateVar) {
  if (!stateVar) {
    Keyboard.press(key);
    stateVar = true;
  }
}

void holdSpecialKey(uint8_t key, bool &stateVar) {
  if (!stateVar) {
    Keyboard.press(key);
    stateVar = true;
  }
}

void releaseKey(char key, bool &stateVar) {
  if (stateVar) {
    Keyboard.release(key);
    stateVar = false;
  }
}

void releaseSpecialKey(uint8_t key, bool &stateVar) {
  if (stateVar) {
    Keyboard.release(key);
    stateVar = false;
  }
}

void tapKey(char key) {
  Keyboard.press(key);
  delay(40);
  Keyboard.release(key);
}

void tapSpecialKey(uint8_t key) {
  Keyboard.press(key);
  delay(40);
  Keyboard.release(key);
}

void holdRightMouse() {
  if (!aimMouseHeld) {
    Mouse.press(MOUSE_RIGHT);
    aimMouseHeld = true;
  }
}

void releaseRightMouse() {
  if (aimMouseHeld) {
    Mouse.release(MOUSE_RIGHT);
    aimMouseHeld = false;
  }
}

void clickLeftMouse() {
  Mouse.press(MOUSE_LEFT);
  delay(100);
  Mouse.release(MOUSE_LEFT);
}

void releaseGameplayInputs() {
  releaseKey(KEY_FORWARD, forwardHeld);
  releaseKey(KEY_BACKWARD, backwardHeld);
  releaseKey(KEY_TURN_LEFT, leftHeld);
  releaseKey(KEY_TURN_RIGHT, rightHeld);
  releaseSpecialKey(KEY_SPRINT, sprintHeld);
  releaseRightMouse();
}

void releaseMenuInputs() {
  releaseSpecialKey(KEY_SCROLL_UP, scrollUpHeld);
  releaseSpecialKey(KEY_SCROLL_DOWN, scrollDownHeld);
  releaseSpecialKey(KEY_MENU_LEFT, menuLeftHeld);
  releaseSpecialKey(KEY_MENU_RIGHT, menuRightHeld);
}

void releaseAllInputs() {
  releaseGameplayInputs();
  releaseMenuInputs();
  Keyboard.releaseAll();
  Mouse.release(MOUSE_LEFT);
  Mouse.release(MOUSE_RIGHT);
}

// Extra helper functions for reading sensors
int averageAnalogRead(int pin, int samples = 10) {
  long total = 0;
  for (int i = 0; i < samples; i++) {
    total += analogRead(pin);
    delay(2);
  }
  return total / samples;
}

void calibrateBaselines() {
  aimLightBaseline      = averageAnalogRead(AIM_LIGHT_PIN, 20);
  shootLightBaseline    = averageAnalogRead(SHOOT_LIGHT_PIN, 20);
  interactLightBaseline = averageAnalogRead(INTERACT_LIGHT_PIN, 20);
}

// =====================================================
// Setup
// =====================================================
void setup() {
  CircuitPlayground.begin();
  Keyboard.begin();
  Mouse.begin();

  pinMode(BUTTON1_PIN, INPUT_PULLUP);
  pinMode(BUTTON2_PIN, INPUT_PULLUP);
  pinMode(TOGGLE_PIN, INPUT_PULLUP);
  pinMode(TILT_PIN, INPUT_PULLUP);

  delay(1500);
  calibrateBaselines();
}

// =====================================================
// Main loop
// =====================================================
void loop() {
  unsigned long now = millis();

  // Built-in slide switch = arming switch
  bool controllerArmed = CircuitPlayground.slideSwitch();

  if (!controllerArmed) {
    releaseAllInputs();
    bothButtonsTiming = false;
    bothButtonsStartTime = 0;
    tiltTiming = false;
    tiltUsed = false;
    interactTriggeredAlready = false;
    delay(10);
    return;
  }

  // -------------------------------------------------
  // Read digital inputs
  // -------------------------------------------------
  bool button1Pressed = (digitalRead(BUTTON1_PIN) == LOW);
  bool button2Pressed = (digitalRead(BUTTON2_PIN) == LOW);
  bool toggleOn       = (digitalRead(TOGGLE_PIN) == LOW);
  bool tiltTriggered  = (digitalRead(TILT_PIN) == HIGH);

  // -------------------------------------------------
  // Read analog inputs
  // -------------------------------------------------
  int potValue            = analogRead(POT_PIN);
  int aimLightValue       = analogRead(AIM_LIGHT_PIN);
  int shootLightValue     = analogRead(SHOOT_LIGHT_PIN);
  int interactLightValue  = analogRead(INTERACT_LIGHT_PIN);

  int aimLightDiff        = aimLightValue - aimLightBaseline;
  int shootLightDiff      = shootLightValue - shootLightBaseline;
  int interactLightDiff   = interactLightValue - interactLightBaseline;

  bool potLeft  = (potValue < POT_LEFT_MAX);
  bool potRight = (potValue > POT_RIGHT_MIN);

  // -------------------------------------------------
  // Both buttons held = menu toggle
  // -------------------------------------------------
  if ((now - lastMenuToggleTime) > MENU_REARM_TIME) {
    if (button1Pressed && button2Pressed) {
      if (!bothButtonsTiming) {
        bothButtonsTiming = true;
        bothButtonsStartTime = now;
      } else if ((now - bothButtonsStartTime) >= MENU_HOLD_TIME) {
        tapSpecialKey(KEY_MENU_ESC);
        menuOpen = !menuOpen;

        bothButtonsTiming = false;
        bothButtonsStartTime = 0;
        lastMenuToggleTime = now;
      }
    } else {
      bothButtonsTiming = false;
      bothButtonsStartTime = 0;
    }
  } else {
    bothButtonsTiming = false;
    bothButtonsStartTime = 0;
  }

  // =================================================
  // GAMEPLAY MODE
  // =================================================
  if (!menuOpen) {
    // Refreshes inputs and switches over to gameplay inputs
    releaseMenuInputs();

    // Forward press
    if (button1Pressed) {
      holdKey(KEY_FORWARD, forwardHeld);
    } else {
      releaseKey(KEY_FORWARD, forwardHeld);
    }

    // Backward press
    if (button2Pressed) {
      holdKey(KEY_BACKWARD, backwardHeld);
    } else {
      releaseKey(KEY_BACKWARD, backwardHeld);
    }

    // Potentiometer turn left/right
    if (potLeft) {
      holdKey(KEY_TURN_RIGHT, rightHeld);
      releaseKey(KEY_TURN_LEFT, leftHeld);
    } else if (potRight) {
      holdKey(KEY_TURN_LEFT, leftHeld);
      releaseKey(KEY_TURN_RIGHT, rightHeld);
    } else {
      releaseKey(KEY_TURN_LEFT, leftHeld);
      releaseKey(KEY_TURN_RIGHT, rightHeld);
    }

    // Sprint toggle
    if (toggleOn) {
      holdSpecialKey(KEY_SPRINT, sprintHeld);
    } else {
      releaseSpecialKey(KEY_SPRINT, sprintHeld);
    }

    // Tilt switch quick turn with hold + cooldown to prevent multiple triggers back to back
    if (tiltTriggered) {
      if (!tiltTiming) {
        tiltTiming = true;
        tiltStartTime = millis();
      } else {
        if (!tiltUsed &&
            (millis() - tiltStartTime >= TILT_HOLD_TIME) &&
            (millis() - lastTiltTriggerTime >= TILT_COOLDOWN)) {
          tapKey(KEY_QUICK_TURN);
          tiltUsed = true;
          lastTiltTriggerTime = millis();
        }
      }
    } else {
      tiltTiming = false;
      tiltUsed = false;
    }

    // Aim light sensor -> hold right mouse
    if (aimLightDiff < AIM_DARK_THRESH) {
      holdRightMouse();
    } else {
      releaseRightMouse();
    }

    // Shoot light sensor -> left click
    if ((shootLightDiff < SHOOT_DARK_THRESH) &&
        (now - lastShootTime > SHOOT_COOLDOWN)) {
      clickLeftMouse();
      lastShootTime = now;
    }

    // Interact light sensor -> tap interact
    if ((interactLightDiff < INTERACT_DARK_THRESH) &&
        !interactTriggeredAlready &&
        (now - lastInteractTime > INTERACT_COOLDOWN)) {
      tapKey(KEY_INTERACT);
      lastInteractTime = now;
      interactTriggeredAlready = true;
    } else if (interactLightDiff >= INTERACT_DARK_THRESH) {
      interactTriggeredAlready = false;
    }
  }

  // =================================================
  // MENU MODE
  // =================================================
  else {
    //Refreshes inputs and switches over to menu inputs
    releaseGameplayInputs();

    // Forward button = scroll up
    if (button1Pressed) {
      holdSpecialKey(KEY_SCROLL_UP, scrollUpHeld);
    } else {
      releaseSpecialKey(KEY_SCROLL_UP, scrollUpHeld);
    }

    // Backward button = scroll down
    if (button2Pressed) {
      holdSpecialKey(KEY_SCROLL_DOWN, scrollDownHeld);
    } else {
      releaseSpecialKey(KEY_SCROLL_DOWN, scrollDownHeld);
    }

    // Left / right = scroll left / right
    if (potLeft) {
      holdSpecialKey(KEY_MENU_LEFT, menuLeftHeld);
      releaseSpecialKey(KEY_MENU_RIGHT, menuRightHeld);
    } else if (potRight) {
      holdSpecialKey(KEY_MENU_RIGHT, menuRightHeld);
      releaseSpecialKey(KEY_MENU_LEFT, menuLeftHeld);
    } else {
      releaseSpecialKey(KEY_MENU_LEFT, menuLeftHeld);
      releaseSpecialKey(KEY_MENU_RIGHT, menuRightHeld);
    }

    // Aim / Ready weapon = select in menu
    if ((aimLightDiff < AIM_DARK_THRESH) &&
        (now - lastSelectTime > SELECT_COOLDOWN)) {
      tapKey(KEY_SELECT);
      lastSelectTime = now;
    }
  }

  delay(10);
}
(Code)


Team 11 - SRB2Kart Final Controller

 

Sonic Robo Blast 2 Kart Controller

The final controller we built is for the game Sonic Robo Blast 2 Kart. It is designed as a car radio. The idea is built on the irony that the user is ‘playing with the radio’ while driving. It is a way of symbolizing the havoc and chaos in karting games like this.

The left knob (which is typically associated with volume control on a car radio) is used for the speed controls. As the ‘volume’ ramps up, so does the kart velocity. The potentiometer inputs for it are mapped to a range of 1 to 100. The lowest part of this range is used for reverse, the mostly low section is used for idle (no gas), the section above this uses a PMW system to allow for speed ramping, and the highest section is equivalent to holding the accelerate button.

The right button (which is typically the tune button) is used to control the steering, as the player is making an ‘adjustment’ to their kart. It is mapped to a range of -100 to 100 and also uses a form of PMW to make it so gradual turning is possible in the intermediate zones, with maximum turn at the farther ends on each side.

Both knobs have notches in the 3d design to provide feedback on the current position and allow for the user to normalize their frame of reference while controlling.

The final input device is the slider in the center. The slider is an indicator of the radio's channel, and in this case, it is an indicator of the kart's special state. The input is mapped to create 3 zones; The left is used for drifting, the middle is neutral, and the right activates/uses items.

One question we have about our controller design is: Which input device is the weakest, and if you had to replace it with another type of input device, what would it be?

 

(Final Controller)


(Schematic)

#include <Keyboard.h>
#include <Adafruit_CircuitPlayground.h>
#include <Adafruit_Circuit_Playground.h>


float stopwatch = 0;

bool isWPressed = false;
bool isSpacePressed = false;

int speed;
int steer;
int slider;

void setup() {
  // put your setup code here, to run once:
  CircuitPlayground.begin();
  Serial.begin(9600);
  pinMode(A1, INPUT);
  pinMode(A2, INPUT);
  pinMode(A3, INPUT);
  delay(1000);
}

void loop() {

  ReadSensors();

  HandleSpeed();

  HandleSteering();

  HandleSlider();
 
}

void ReadSensors() {
  //  Read Left Knob (Speed): Map the 0-1023 range to a 1 to 100 range
  speed = map(analogRead(A1), 0, 1023, 1, 100);
  //  Read Right Knob (Steering): Map the 0-1023 range to a -100 to 100
  steer = map(analogRead(A2), 0, 1023, -100, 100);
  //  Read Slider: Map the 0-1023 range to a 1-30 range
  slider = map(analogRead(A3), 0, 1023, 1, 30);
}


void HandleSpeed() {

  if (speed <= 10) {
    // 1 Reverse if dial is at the very bottom
    Keyboard.release('a');
    Keyboard.press('d');

  } else if (speed >=11 && speed <= 30) {
    // 2 No movement (gas off, idling)
    Keyboard.release('d');
    Keyboard.release('a');

  } else if (speed >= 85) {
    // 3 Full Speed
    Keyboard.release('d');
    Keyboard.press('a');

  } else {

    // 4 PWM Speed (variable speed ramps when player is between idling and max speed zones)
    Keyboard.release('d');
    Keyboard.press('a');
    delay(speed*4);
    Keyboard.release('a');
    delay(100);
}

}

void HandleSteering() {
    // 1 Neutral (Deadzone) (-15 to 15)
  if (steer >= -15 && steer <= 15) {
    Keyboard.release(KEY_LEFT_ARROW);
    Keyboard.release(KEY_RIGHT_ARROW);

  } else if (steer <= -50) {
    // 2 Full Left (Hard steer) (-50 or less)
    Keyboard.release(KEY_RIGHT_ARROW);
    Keyboard.press(KEY_LEFT_ARROW);

  } else if (steer >= 50) {
    // 3 Full Right (Hard steer) (50 or more)
    Keyboard.release(KEY_LEFT_ARROW);
    Keyboard.press(KEY_RIGHT_ARROW);

  } else if (steer < -15) {
    // PWM Left (Partial steer)
    Keyboard.release(KEY_RIGHT_ARROW);
    Keyboard.press(KEY_LEFT_ARROW);
   
    // Hold then Release
    delay(180);
    Keyboard.release(KEY_LEFT_ARROW);
   
    // Wait a delay based on strength of steer
    int restTime = map(abs(steer), 15, 50, 80, 2);
    delay(restTime);

  } else if (steer > 15) {
    // PWM Right (Partial steer)
    Keyboard.release(KEY_LEFT_ARROW);
    Keyboard.press(KEY_RIGHT_ARROW);
   
    // Hold then Release
    delay(180);
    Keyboard.release(KEY_RIGHT_ARROW);
   
    // Wait a delay based on strength of steer
    int restTime = map(steer, 15, 50, 80, 2);
    delay(restTime);
  }
}

void HandleSlider() {
  // 1 Lowest Zone (Drift)
  if (slider <= 7) {
    Keyboard.release(' ');
    Keyboard.press('s');
    isSpacePressed = false;

  } else if (slider >= 8 && slider <= 17) {
  // 2 Middle Zone (Neutral)
    Keyboard.release('s');
    Keyboard.release(' ');
   
    // Prevents jitter at the 17/18 border and spamming fire of space (item) button
    if (slider <= 15) {
      isSpacePressed = false;
    }
   
  }   else if (slider >= 18) {
    // 3 Highest Zone (Collect/Use Item)
    Keyboard.release('s');

    if (isSpacePressed == false) {
      Keyboard.press(' ');
      delay(50);
      Keyboard.release(' ');
      isSpacePressed = true; // Set lock so player so slider only uses space ONCE per time it enters the zone (prevents item spam)
    }
  }
}

(Code)