25 April 2026

Team 13 - Final Project - Spooky's Jumpscare Mask

 


Our controller is a spooky Halloween mask for the game Spooky's Jumpscare Mansion, matching the spooky Halloween aesthetic of the game and fitting in with the horror movie references that fill the games roster of monsters. The inside of the mask is lined with dark felt to protect the player from the mask's internal components, as well as supply breathability and comfort. Starting from the top of the forehead is a long piece of dark felt that serves to cover the wires and the box where the circuit is located. This box is located in the middle where the mask's two straps meet. We chose dark colors to keep in with the theme of horror, and the felt that covers the wire was affixed like that to resemble a Mohawk, a hairstyle well linked to 80's Horror movies. The buttons and potentiometer were also chosen as red to match the mask's colors, as well as evoke the color of Blood.


The purpose of the mask is to immerse the player and make them even more susceptible to the games many jumpscares. Left-and-right camera control is handled by the accelerometer in the CPE, simulating looking around as signified by the mask allowing the player to tilt their head left and right as if peeking around corners in the actual mansion. In order to move forwards or backward, the player must lean away or towards their monitor, which is done using an ultrasonic sensor protruding from the forehead. Being closer to the monitor will make the jumpscares that much more impactful (and might make the player involuntarily move backwards!) This ties into the main purpose (and name) of the game. Leaning in even closer will activate the sprint feature as well.


On the temples of the mask are two buttons, the interact button and the pause button. Up-and-down movement is handled by a potentiometer knob near the interact button, so that looking up and down does not interfere with movement via the distance sensing. Finally, the CPE has a microphone input that becomes the attack input which is triggered by a mouse click. In order to click, the player must scream, much like screaming in fright, to trigger it.


Question: What might be a good way to improve visibility when wearing the mask without sacrificing comfort?






//Final Group 13
//Aiden Pain, Victor Farrulla
//Updated 4/24/26

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

#include <Mouse.h>

#include <Keyboard.h>
#include <KeyboardLayout.h>
#include <Keyboard_da_DK.h>
#include <Keyboard_de_DE.h>
#include <Keyboard_es_ES.h>
#include <Keyboard_fr_FR.h>
#include <Keyboard_hu_HU.h>
#include <Keyboard_it_IT.h>
#include <Keyboard_pt_PT.h>
#include <Keyboard_sv_SE.h>


/*
Pinouts:
12 = A0
6 = A1
9 = A2
10 = A3
3 = A4
2 = A5
0 = A6
1 = A7
*/

//declare constants to represent pins
const int DIST_TRIG_PIN = 0; //A6 - trigger
const int DIST_ECHO_PIN = 1; //A7 - echo
#define INTERACT_PIN A0 //A0 for interact button
#define ESCAPE_PIN A1 //A1 for escape button
#define POTY_PIN A4 //A4 for potentiometer

//declare constants to compare to analog read values
const int MOUSE_X_SENS = 200; //how fast the mouse moves left or right when tilted left or right
const int MOUSE_Y_SENS = 30; //how fast the mouse moves up or down when the knob is twisted up or down
const int soundSensitivity = 1; //how much higher than soundBaseLevel the attack activates

//declare variables for analog read values
long distRead;
float tiltX;
int tiltXmapped;
int micRead;
int interactRead;
int interactReadMapped;
int escapeRead;
int escapeReadMapped;

int soundBaseLevel; // initial sound level to compare sound level to, set in setup
int tiltXbase; //variable to compare current X tilt, set in setup
int distBase; //initial distance to compare ultrasonic sensor data to, set in setup
int distSprint; //if distance is less than this, sprint (set to distBase/2 in setup)

//previous checks so events don't fire multiple times per button press
int prevInteractRead = false;
int prevEscapeRead = false;
int prevMainSwitch = false;

// ----------------- //
// ----- SETUP ----- //
// ----------------- //
void setup() {
  // start CircuitPlayground and Serial Monitor
  CircuitPlayground.begin();
  Serial.begin(9600);

  //set pin modes for pins attached to sensor and buttons
  pinMode(DIST_TRIG_PIN, OUTPUT);
  pinMode(DIST_ECHO_PIN, INPUT);
  pinMode(INTERACT_PIN, INPUT_PULLUP);
  pinMode(ESCAPE_PIN, INPUT_PULLUP);

  delay(2000); //wait for setup

  //read current environment sound and set a variable to act as the baseline for microphone input
  int sum = 0;
  for (int i = 0; i < 10; i++){
    sum += CircuitPlayground.mic.soundPressureLevel(10);
  }
  soundBaseLevel = sum / 10;

  //set up ultrasonic sensor to get the controller's initial distance from the screen
  digitalWrite(DIST_TRIG_PIN, LOW);
  delayMicroseconds(2);
  digitalWrite(DIST_TRIG_PIN, HIGH);
  delayMicroseconds(10);
  digitalWrite(DIST_TRIG_PIN, LOW);
  distBase = pulseIn(DIST_ECHO_PIN, HIGH);
  distBase = (distBase/2)/29.1; //distance in centimeters
  distSprint = distBase/2; //half the distance from the screen for sprint

  tiltXbase =  CircuitPlayground.motionX(); //get initial accelerometer X tilt
}

// ---------------- //
// ----- LOOP ----- //
// ---------------- //
void loop() {
  //only run code if the switch is active
  if(CircuitPlayground.slideSwitch() == true){
    //user-defined functions to handle distance sensing, x-tilt sensing, microphone sensing, button inputs, and potentiometer inputs
    DistanceSense();
    TiltSense();
    MicSense();
    ButtonsCheck();
    PotYCheck();
    //sets prevMainSwitch to true as the CPX switch is switched on
    prevMainSwitch = true;
  }
  //if the CPX switch was just switched off, release all keys
  else if(prevMainSwitch){
    Keyboard.releaseAll();
    prevMainSwitch = false; //sets prevMainSwitch to false as the CPX switch is switched off
  }

  Serial.println("---");
  delay(100); //debounce
}

// --------------------- //
// ----- FUNCTIONS ----- //
// --------------------- //
//function that handles ultrasonic sensor input in loop()
void DistanceSense(){
  //set distRead to the value of the ultrasonic sensor's reported distance
  digitalWrite(DIST_TRIG_PIN, LOW);
  delayMicroseconds(2);
  digitalWrite(DIST_TRIG_PIN, HIGH);
  delayMicroseconds(10);
  digitalWrite(DIST_TRIG_PIN, LOW);
  distRead = pulseIn(DIST_ECHO_PIN, HIGH);
  Serial.print("distRead: ");
  Serial.println(distRead);
  distRead = (distRead/2)/29.1; //distance in centimeters
  Serial.print("distBase: ");
  Serial.println(distBase);


  //map it so it's negative when leaning backward, positive when leaning forward, and zero for staying still (in relation to distBase)
  //if distBase-distRead is negative, map to -1
  //if distBase-distRead is around 0 (i.e. they're about equal), map to 0
  //else, check distRead for sprint
  int distMapped = map(distBase-distRead, -distBase, distBase, -1, 2);
  //use if-else statements with that variable to press S if negative, W if positive, or neither if zero
  //if mapped distance is negative, move backwards by pressing S (and release W too)
  if(distMapped < 0){
    Keyboard.press('s');
    Keyboard.release('w');
  }
  //if mapped distance is zero, stop moving by releasing S and W
  else if(distMapped == 0){
    Keyboard.release('s');
    Keyboard.release('w');
  }
  //else, if mapped distance is positive, release S and move forwards by pressing W
  else{
    Keyboard.release('s');
    //if mapped distance is less than the sprint threshold, sprint by pressing shift
    if(distRead < distSprint){
      Keyboard.press('W');
    } else {
      Keyboard.press('w');
    }
  }
  //regardless of direction, if farther away than distSprint, release shift
  if(distRead > distSprint){
    Keyboard.release(KEY_LEFT_SHIFT);
  }
}

void TiltSense(){
  //set tiltX to correspond to accelerometer x-axis input, and map to mouse move-friendly values
  tiltX =  CircuitPlayground.motionX();
  tiltXmapped = map(tiltXbase - tiltX, -3, 3, -2, 2);
  //send the mouse move signals, inverting the up/down axis represented by the X axis tilt
  Mouse.move(-tiltXmapped*MOUSE_X_SENS, 0, 0);
  Serial.print("tiltX: ");
  Serial.println(tiltX);
  Serial.print("tiltXmapped: ");
  Serial.println(tiltXmapped);
}

void MicSense(){
  //set micRead to the recorded microphone input
  micRead = CircuitPlayground.mic.soundPressureLevel(10);
  //if the microphone input is significantly higher than the environmental baseline, press left-click (to attack)
  if(micRead > soundBaseLevel + soundSensitivity){
    Mouse.click(MOUSE_LEFT);
  }
}

void ButtonsCheck(){
  //set interactRead to read whether the interact button is pressed
  interactRead = analogRead(INTERACT_PIN);
  interactReadMapped = map(interactRead, 0, 1023, 0, 1);
  //if the voltage reads high and wasn't previously high, press the E key (to interact)
  if(interactReadMapped && !prevInteractRead){
    Keyboard.press('e');
  }
  else{
    Keyboard.release('e');
  }
  //set the chin button's previous state (to compare to the next read)
  prevInteractRead = interactReadMapped;

  //set escapeRead to read whether the escape button is pressed
  escapeRead = analogRead(ESCAPE_PIN);
  escapeReadMapped = map(escapeRead, 0, 1023, 0, 1);
  //if the voltage reads high and wasn't previously high, press the escape key (to get to the pause menu)
  if(escapeReadMapped && !prevInteractRead){
    Keyboard.press(KEY_ESC);
  }
  else{
    Keyboard.release(KEY_ESC);
  }
  //set the chin button's previous state (to compare to the next read)
  prevEscapeRead = escapeReadMapped;
}

//check potentiometer value for looking up and down
void PotYCheck(){
  //read in potentiometer value and map to usable integers
  int potYreading = analogRead(POTY_PIN);
  int potYreadingMapped = map(potYreading, 0, 1023, -1, 2);

  //if potentiometer is turned downward, look down
  if(potYreadingMapped < 0){
    Mouse.move(0, MOUSE_Y_SENS, 0);
  }
  //if potentiometer is turned upward, look up
  else if(potYreadingMapped > 0){
    Mouse.move(0, -MOUSE_Y_SENS, 0);
  }
  //if potentiometer is more or less in the middle, do not move the mouse up or down

  Serial.println(potYreadingMapped); //debug print to make sure the controller is on and working
}







 

Final Team 4: Fireboy and Watergirl Game Controller

 


Description:
Do you remember the good old days of sitting down with a friend of a sibling and playing a 2 player game on a single keyboard, having to share the space and getting in each other's way? Our controller seeks to ease that experience by giving each player their own controller. The game we decided to make our custom controller for is Fireboy and Watergirl. Taking inspiration from the game's themes, each player uses the gem-like controller to pilot each of the 2 characters.
The technical aspects of the controller consist of 2 character controllers and 1 home console used for mouse movements. The home console uses 2 potentiometers mapped to the X and Y axis of a mouse. A photoresistor is used for left mouse clicking by checking the light level to be below a certain threshold. The Fireboy controller contains our CPE which uses its built in accelerometer for jumping by sending a rapid shift in motion and triggering a jump when shaken and a potentiometer mapped for right and left movement. Our water girl controller uses the same potentiometer mechanic for right and left movement, but uses a microphone for jumping by measuring the noise levels to be above a certain level to trigger the jump action. While building the casing we made sure to use plastic that would partially isolate outside noise to avoid accidental triggers of the jump button.
The controller thematically looks like the gemstones in the game to represent each character. The red gemstone is for Fireboy, and the blue gemstone is for Watergirl. While the central console is modeled after the yellowish stone of the jungle temple.

Our question for the class: 
If you had to improve on this design do you think having the mouse controller being a part of the individual character controllers would be better, or is having the 3 modules set up better?


Specific Contributions:
Mateo Rios: Made 3D models of casings and printed them, created initial schematic, built physical prototype, wrote blog description, aided in final controller assembly.
Brody Townsend: Designed the initial idea of the game controller, wrote the code, tested code, and made a breadboard prototype. Soldered the components together and aided in the final assembly of the controller. Took photos of the controller, recorded the video, and made the blog post.


Schematic:

Code:
// Brody Townsend
// Mateo Rios

#include <Mouse.h>

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

#include <Keyboard.h>
#include <KeyboardLayout.h>
#include <Keyboard_da_DK.h>
#include <Keyboard_de_DE.h>
#include <Keyboard_es_ES.h>
#include <Keyboard_fr_FR.h>
#include <Keyboard_hu_HU.h>
#include <Keyboard_it_IT.h>
#include <Keyboard_pt_PT.h>
#include <Keyboard_sv_SE.h>

//Normal Game Controls
//Watergirl---------------------------
//(W) = Jump
//(A) = Move Left
//(D) = Move Right
//Fireboy---------------------------
//(Up Arrow) = Jump
//(Left Arrow) = Move Left
//(Right Arrow) = Move Right

//-------------------------------------------------------------------------------
int WaterPot = A0; ////Potentiometer for Watergirl movement. Set to A0.

int FirePot = A1; //Potentiometer for Fireboy movement. Set to A1.

int potXPin = A2; //Potentiometer for X mouse movement. Set to A2.

int potYPin = A3; //Potentiometer for Y mouse movement. Set to A3.

int micPin = A4; //Sound Sensor for watergirl jump. Set to A4.
int micValue = 0;
int soundThreshold = 600;
int jumpcooldown = 300;
unsigned long lastJumpTime = 0;

unsigned long lastFireJump = 0;
int fireJumpCooldown = 300;  // milliseconds between jumps
float shakeThreshold = 15.0; // adjust for sensitivity

// For reading accelerometer
float accelX, accelY, accelZ;
float accelTotal;

const int photoPin = A5; //Photoresistor for left mouse click. Set to A5.
int photoValue = 0;
int photoThreshold = 600;


String WaterDirection = "";
String WaterPrevious = "";
String FireDirection = "";
String FirePrevious = "";




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

void loop() {

if (!CircuitPlayground.slideSwitch()) {
    Keyboard.releaseAll();  // make sure no keys are stuck
    return;                 // skip the rest of the code
  }

  int waterValue = analogRead(WaterPot);
  int fireValue = analogRead(FirePot);
  int xValue = analogRead(potXPin);
  int yValue = analogRead(potYPin);
  micValue = analogRead(micPin);
  if (micValue > 520){
  Serial.println(micValue);
  }


//Watergirl Controls
//Jumping with Sound Sensor
  if (micValue > soundThreshold && millis() - lastJumpTime > jumpcooldown) {
    Keyboard.press('W');
    delay(50); // short tap
    Keyboard.release('W');

    lastJumpTime = millis();
  }
 
//Moving with Potentiometer
  if (waterValue < 400){
     WaterDirection = "Left";}
else if (waterValue > 600){
   WaterDirection = "Right";}
else {WaterDirection = "Middle";}

if (WaterDirection != WaterPrevious) {
  Keyboard.release('A');
  Keyboard.release('D');

  if (WaterDirection == "Left") Keyboard.press('A');
  else if (WaterDirection == "Right") Keyboard.press('D');

  WaterPrevious = WaterDirection;
}


//FireBoy Controls
//Jumping
// Read accelerometer
accelX = CircuitPlayground.motionX();
accelY = CircuitPlayground.motionY();
accelZ = CircuitPlayground.motionZ();

// Calculate total movement force
accelTotal = sqrt(accelX * accelX + accelY * accelY + accelZ * accelZ);

// Shake to Jump
if (accelTotal > shakeThreshold && millis() - lastFireJump > fireJumpCooldown){
  Keyboard.press(KEY_UP_ARROW);
  delay(50);                // short tap
  Keyboard.release(KEY_UP_ARROW);

  lastFireJump = millis();  // reset cooldown
}




//Moving with Potentiometer
if (fireValue < 400){
   FireDirection = "Left";}
else if (fireValue > 600){
   FireDirection = "Right";}
else {FireDirection = "Middle";}

if (FireDirection != FirePrevious) {
  Keyboard.release(KEY_LEFT_ARROW);
  Keyboard.release(KEY_RIGHT_ARROW);

  if (FireDirection == "Left") Keyboard.press(KEY_LEFT_ARROW);
  else if (FireDirection == "Right") Keyboard.press(KEY_RIGHT_ARROW);

  FirePrevious = FireDirection;
}



  // Map the analog value to mouse movement speed (-10 to 10)
  int moveX = map(xValue, 0, 1023, -10, 10);
  int moveY = map(yValue, 0, 1023, -10, 10);

  // Ccreate deadzone in center
  if (abs(moveX) < 2) moveX = 0;
  if (abs(moveY) < 2) moveY = 0;

  // Move the mouse
  Mouse.move(moveX, moveY);

  delay(20); // adjust speed, lower is faster




// Read photoresistor
    photoValue = analogRead(photoPin);

    // If the photo value crosses the threshold, then left click
    if(photoValue > photoThreshold){
        Mouse.press(MOUSE_LEFT);
    } else {
        Mouse.release(MOUSE_LEFT);
    }



}

Video:


23 April 2026

Team 2: Temple Run

Our Controller:



Description:

Our controller is a stylish way to play the game Temple Run. The design of the controller mimics the look of the pathways that you run through in the game, and the guy that our controller uses is designed after the guy in the game. We wanted our controller to be highly themed to create a unique experience for each person that uses it. 

The technical aspects of the controller consist of 3 photoresistors, 1 range sensor, and 1 button. The 3 photoresistors are mapped to the inputs for left, right, and slide, while the range sensor is mapped to the jump key. The button is simply used to start the game and activate your power ups when you unlock those. The code for the game simply detects in the range or light level is within a certain threshold and presses the assigned button if it is. You simply hover the guy figure over the photoresistor or range sensor to simulate this input. 

Since you are actively moving the figure around the controller, it feels as though you are physically controlling the character in the game, creating a unique controller experience.

There were certain design intentions that were not able to be translated and/or did not come out as intended. These include: the size of the controller came out smaller than anticipated and the range sensor was intended to detect if the figure was lifted off of the sensor rather than the other way around.

Do you feel that the controller would be better with the ability to lift the figure as it was originally intended, or does it not make too much of a difference?

Schematic:



Project Code:

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

//Initializing Variables
#define startButton 1

#define trigPin 9
#define echoPin 6
int disThreshold = 60;

const int photoLeft = A3;
const int photoRight = A4;
const int photoSlide = A6;
const int debounce = 200;

int lumoLeft;
int lumoRight;
int lumoSlide;
int lumoThreshold = 200;

void setup() {
  //Basic setup for code
  Serial.begin(9600);
  CircuitPlayground.begin();
  Keyboard.begin();
  delay(1000);

  //Initializing pin modes
  pinMode(trigPin, OUTPUT);
  pinMode(echoPin, INPUT);
  pinMode(startButton, INPUT);
}

void loop() {
  //Calling functions that store the code
  startButtonInputCheck();
  jumpInputCheck();
  leftInputCheck();
  rightInputCheck();
  slideInputCheck();

  //Delay between checks
  delay(150);
}

//Start button function
void startButtonInputCheck() {
  if(digitalRead(startButton) == HIGH) {
    Keyboard.write(' ');
    Serial.println("START"); //For debug/testing
    delay(debounce);
  }
}

//Left input function
void leftInputCheck() {
  lumoLeft = analogRead(photoLeft);
 
  if(lumoLeft < lumoThreshold) {
    Keyboard.write('A');
    Serial.println("LEFT"); //For debug/testing
    delay(debounce);
  }
}

//Right input function
void rightInputCheck() {
  lumoRight = analogRead(photoRight);
 
  if(lumoRight < lumoThreshold) {
    Keyboard.write('D');
    Serial.println("RIGHT"); //For debug/testing
    delay(debounce);
  }
}

//Slide input function
void slideInputCheck() {
  lumoSlide = analogRead(photoSlide);
 
  if(lumoSlide < lumoThreshold) {
    Keyboard.write('S');
    Serial.println("SLIDE"); //For debug/testing
    delay(debounce);
  }
}

//Jump input function
void jumpInputCheck() {
  //Code for range sensor
  long duration, distance;
  digitalWrite(trigPin, LOW);
  delayMicroseconds(2);
  digitalWrite(trigPin, HIGH);
  delayMicroseconds(10);
  digitalWrite(trigPin, LOW);
  duration = pulseIn(echoPin, HIGH);
  distance = duration * 0.1715;

  //Printing distance in serial monitor for tracking and debugging
  Serial.println(distance);

  //Distance check for input
  if(distance < disThreshold) {
    Keyboard.write('W');
    Serial.println("JUMP"); //For debug/testing
    delay(debounce);
  }
}

Video Demonstration: