16 April 2021

David Perez Game Controller


 Five Nights at Freddy's 2 controller

David Perez

My controller is a plug and play inspired device for controlling Five Nights at Freddy's 2 (PC version). The controller features one dial (potentiometer) and three toggle switches. The intent of the design is to bring the player to the 80s (when the game takes place) with it's large, boxy design and shiny switches (the print out I used for the actual controller is black and white. A colored version of the image is included for viewing).

Five Nights at Freddy's 2 features three controls; mouse movement, left click and left CTRL (for the flashlight). During play, the player navigates to different on screen prompts to open and close the camera feed, equip and unequip the mask and look around the security room. The controls are mapped to the controller as follows:

  1. The dial primarily controls the mouse moving on the X axis for use in the security room. When the camera feeds are opened, the mouse instead cycles between different camera feeds and the music box wind up prompt.
  2. One toggle switch puts on the mask, used by the player to protect themselves. The switch does nothing when toggled to OFF and must be flipped to ON again to unequip the mask.
  3. One toggle switch activates the flashlight. The flashlight remains on while the switch is on and turns off when the switch is turned off.
  4. One toggle switch is mapped to accessing and leaving the camera feeds. When the switch is ON, the dial controls change to cycling between the camera feed prompts. When the switch is off, it returns to X axis movement. The switch does nothing when toggled to OFF and must be flipped to ON again to close the camera feed and return to the security room.

The layout of the controller is intended to be friendly for ambidextrous users, meaning it's usable whether you play left or right handed. The dial sits front and center while the three toggle switches rest up near the end of the controller. Around the dial is a series of numbers and lines which indicate camera feed will be opened when the player twists the dial on them. The flashlight toggle sits in the middle and features a flashlight icon beneath the switch. The mask toggle is on the left, same as the on-screen prompt for wearing the mask which is reflected on the controller. The camera toggle is on the right, same as the on-screen prompt for opening the camera feed which is reflected on the controller.

The game controller as it was built


Side view of the game controller with the micro USB cable extending from the back end

A JEPG of the printout as originally intended in color.


 

Code

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

// CONDITIONS
bool center = false;
bool room = false; // A bool to determine if the camera is at the origin point or not.
int cam = 0;
int mask = 0;

// PIN AND DATA FOR POTENTIOMETER INPUT
int potpin = A3; // potentiometer
int mousePos; // Input data for potentiometer
int x, y, s, senseAve, prevX, prevY, prevZ; // Integers for data points
int z = 1;
int numReads = 10; // number of reads to smooth out potentiometer for average
int senseSum = 0;

// TOGGLE SWITCH PINS
int fLightPin = A6; // Input pin on CPE for the flashlight
int cameraPin = A1; // Input pin on CPE for the camera controller
int maskPin = A2; // Input pin on CPE for the mask controller

// ARRAYS
int xAxis[13] = {-2000, -4500, -2000, -4500, -2000, -4500, -4500, -2000, 4450, 4430, -4500, -10, -4500}; // X values for mouse movement during camera view
int yAxis[13] = {-6666666, -6666666, 1101111, 1101111, -2222222, -2222222, 4444444, 4444444, 270, -200, -210, -2222222, -180}; // y values for mouse movement during camera view

void setup() {
CircuitPlayground.begin();
pinMode(fLightPin, INPUT);
pinMode(maskPin, INPUT);
pinMode(cameraPin, INPUT);
Mouse.begin();
Keyboard.begin();
}

void loop() {

// SECURITY ROOM DIAL SETTINGS

if (!digitalRead(cameraPin)) { // moves mouse into proper position for security room view. Runs once and only after returning from camera view.
  prevX = 0;
  prevY = 0;
  if (room == false){
    Recenter();
    Mouse.move(0, 30);
    delay(50);
    room = true;
  }
  // Dial controls in security room
  Mouse.press(); // mouse always presses and holds
 for (int k = 0; k < numReads; k++) {
    mousePos += analogRead(potpin); // add up all readings together
  }
  senseAve = mousePos / numReads; // divide total value of readings by number of readings
  mousePos = 0; // reset reading value for next set of readings

  s = map(senseAve, 0, 1023, 0, 12); // map average
if (s <= 2) Mouse.move(7, 0); // mouse moves right if less than or equal to 2
if (s > 2 > 10) Mouse.move(0,0); // mouse does not move if between 2 and 10
if (s >= 10) Mouse.move(-7,0); // mouse moves left if greater than or equal to 10
}

// DIAL SETTINGS IN CAMERA VIEW
else { // "else if (digitalRead(cameraPin))" implied
  room = false; // once leaving security room view, room is set to false

   if (z == 11) Mouse.press(); // if potentiometer value is mapped to 11, mouse click is held for the music box
   else Mouse.release();
 
  if(center == false) // call Recenter function only if just entered camera view.
    {
    Recenter();
    center = true; // prevents Recenter from being called on every loop.
    }

// CAMERA DIAL SETTINGS
z = Dial(z); // Call Dial function

x = xAxis[z];
y = yAxis[z];
if (z != prevZ) { // if previous Dial input is not the same as new input, run this code.
 
  if (prevZ >= 8) { // Any value greater 7 must run additional "mouse.move"s to return from the far edges of the screen.
  Mouse.move((prevX * -1), 0);
  delay(50);
  }
  prevZ = z; // establish current Dial Input as new previous input.
 
  Mouse.move((prevX * -1), (prevY * -1)); // undo previous mouse movement.
  delay(50);
 
  if (prevZ >= 8) { // if value is greater than 7, must run additional "mouse.move"s to reach the far edges of the screen.
    Mouse.move(x,0);
    delay(50);
      }
 
  Mouse.move(x,y); // using values from arrayX and arrayY, move the mouse to the indicated point on the screen.
  delay(50);
  Mouse.click(); // click on security cam.
  prevX = x; // set previoux X value to equal current X
  prevY = y; // set previoux Y value to equal current Y
  }

}

// TOGGLE PIN FOR THE FLASHLIGHT CONTROL
if (digitalRead(fLightPin)) Keyboard.press(128);
else Keyboard.release(128);

// TOGGLE PIN FOR ENTERING AND EXITING CAMERA VIEW
if (digitalRead(cameraPin)) {
  if (cam == 0) { // if Cam is not equal to zero, then this code will not run.
    for (int i = 0; i < 4; i++) {
    Mouse.move(9999999, 9999999); // moves the mouse to on-screen Camera access panel, moves 4 times to reach it.
    delay (50);
      }
    Recenter(); // Call Recenter function
    }
    cam = 1;
    }
  else cam = 0;
// TOGGLE PIN FOR WEARING AND REMOVING THE MASK
if (digitalRead(maskPin) && (!digitalRead(cameraPin))) {
  if (mask == 0) { // if Mask is not equal to zero, then this code will not run.
    for (int i = 0; i < 3; i++) {
    Mouse.move(-10000, -6666666); // moves mouse to the on-screen Mask panel, moves 3 times to reach it.
    delay (50);
      }
    room = false;
    }
    mask = 1;
    }
  else mask = 0;
}
int Dial (int g) // This function captures potentiometer input, averages it and maps it for smooth movement. Then it assigns it from 0 to 12 and returns it.
{
  for (int k = 0; k < numReads; k++) {
    mousePos += analogRead(potpin); // add up all readings together
  }
  senseAve = mousePos / numReads; // divide total value of readings by number of readings
  mousePos = 0; // reset reading value for next set of readings
  g = map(senseAve, 0, 1023, 0, 12); // map average
  return g; // return mapped value
}

void Recenter() // This function is used to return the mouse to an origin point for all other mouse movements to operate from.
{
      for (int p = 0; p < 5; p++) {
    Mouse.move(-9999999,-9999999); // moves mouse into top left corner of the screen
    delay(50);
      }
      for (int p = 0; p < 3; p++){
       Mouse.move(-6666666,-3333333); // moves mouse to origin point
       delay(50);
      }
}

Reflection

When I started this assignment, I was very hopeful that the controller would be on par if not a better way to play the game than mouse and keyboard. I expected that I would be able to just map on screen points to integer values so switching camera feeds would be instantaneous. That proved to not be the case; I wasn't able to uncover anything that would allow me to define coordinates on the computer screen, so I had to create an "origin" point for the mouse to return to after each command to ensure it can navigate to the next game element.

What resulted is a functional, but clunky and somewhat slow method of control. I wouldn't say I'm disappointed but I am rather surprised I was able to get it to function at all considering all the variables that need to play out in the right sequence in order to ensure the mouse doesn't stray off course.

With my understanding of the game as someone who has completed it before, I believe it would be much harder to play the game given the lightning fast reflexes the player is expected to hav.

As a side note, someone suggested I have a slanted surface to put the controls on which I would've done had I created a custom casing for the controller as opposed to picking up a box from Hobby Lobby. It was a good suggestion though!


No comments:

Post a Comment

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