27 April 2026

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)


No comments:

Post a Comment

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