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
}







 

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.