20 April 2025

Vampires Survivors Controller

 VAMPIRE SURVIVORS CONTROLLER




DESCRIPTION

Our project is a custom game controller designed around the conceptual metaphor of piloting a creature or vehicle—specifically, one inspired by gothic horror. The controller resembles a coffin, aligning with the theme and asthetics of Vampire Survivors, the game it’s built for. The design aims to put a spin on the game’s simple controls by making you indirectly drive the character instead of simply moving them.

The core inputs consist of a slide potentiometer functioning as a throttle and a rotary encoder acting as the steering mechanism. The potentiometer allows players to modulate their movement speed, while the rotary encoder provides directional control. Together, they translate into in-game character movement, offering a tactile, immersive way to "drive" the vampire protagonist.

Our conceptual model treats the player’s character as a kind of haunted vehicle, with the coffin-shaped controller reinforcing that metaphor. This model informs the physical affordances of the design—the coffin shape signals the horror theme; the rotary encoder, placed like a steering wheel inside a coffin, encourages users to turn it as they would in a car or tank.

Signifiers like the shape of the coffin and the tactile feedback from the rotary encoder guide the player in understanding how to use the controller without written instructions. Feedback is provided both visually (the character’s movement in-game responding to input) and physically (the resistance or feel of the controls as they are turned or pressed), reinforcing the loop between action and result.

While the items you use to steer the player are faithful to the game, does it make sense what they do just by looking at them or do you need to see it in action to get how it works? Was there a more intuitive way to control the menus in menu mode? If you know what Vampire Survivors is, would you be able to tell that this controller is made for it from a glance?

 

TEAM CONTRIBUTIONS

Both team members contributed to the intial idea of a controller that operated like driving some sort of big vehicle, character, or tank. Luke handled most of the physical components. He made the initial sketch documentation, breadboard, prototype, and handled physically building the controller (3d printing, woodwork, soldering, and painting). Stanley was in charge of much of the guts behind the controller. Specifically, how it functioned. Stanley wrote the code for the controller’s functionality.


SCHEMATIC


CODE

                #include "HID-Project.h"

#include "Rotary.h"

 

#define ROTARY_PIN1 0

#define ROTARY_PIN2 1

#define SWITCH_PIN 10

#define POT_PIN A5

 

Rotary r;

 

const int maxRotationSpeed = 8; // Max rotation in degrees each clock cycle

int rotationSpeed = 0;     // Current rotation speed.

const int angleStep = 15;  // Degrees per notch

int angle = 0;             // Rotation from encoder (0-359)

int throttle = 0;          // Throttle value from potentiometer

 

// Stores if the left or right input was recently used (for using the menu)

bool rightOptionSelectThisFrame = false;

bool leftOptionSelectThisFrame = false;

 

// Tracks if the game is in the menu or not

bool inMovementMode = false;

 

// Timer and debounce

unsigned long lastRotationTime = 0;

const unsigned long debounceDelay = 50;

 

void setup() {

  Serial.begin(9600);

  // Allows CPE to be seen as a USB controller

  Gamepad.begin();

  // Sets up rotary encoder library

  r.begin(ROTARY_PIN1, ROTARY_PIN2);

  r.setLeftRotationHandler(rotateLeft);

  r.setRightRotationHandler(rotateRight);

  // State of the switch that lets you change modes

  pinMode(SWITCH_PIN, INPUT_PULLDOWN);

}

 

void loop() {

  r.loop();

 

  // If the switch is closed, set controller to movement mode

  if (digitalRead(SWITCH_PIN) == 1) {

    checkIfJustSwitched(); // Presses A button leaving menu mode

    movementMode();

  }

  // If the switch is open, set controller to menu mode

  else {

    menuMode();

  }

 

 

}

 

// Allows you to select options using the sword as up and down

// and the garlic as left and right.

// Leaving this mode by using the switch confirms your selction

void menuMode() {

  // Prevents A button from being pressed.

  Gamepad.release(1);

  inMovementMode = false;

 

  // Lets the menu read the left input and resets bool that tracks if

  // left has been used recently.

  bool isLeft = false;

  if (leftOptionSelectThisFrame) {

    isLeft = true;

    leftOptionSelectThisFrame = false;

    Serial.print("Left on D-Pad | ");

  }

 

  // Same as above

  bool isRight = false;

  if (rightOptionSelectThisFrame) {

    isRight = true;

    rightOptionSelectThisFrame = false;

    Serial.print("Right on D-Pad | ");

  }

 

  // Read and map potentiometer to 0-255 throttle

  int potValue = analogRead(POT_PIN);

  throttle = map(potValue, 0, 1023, 0, 255);

 

  // Converts mapped input to up and down input

  bool isUp = false;

  bool isDown = false;

  if (throttle < 31) {isDown = true; Serial.print("Down on D-Pad | ");}

  else if (throttle > 220) {isUp = true; Serial.print("Up on D-Pad | ");}

 

  // (manually) converts all tracked inputs into x and y coords

  int x = 0;

  int y = 0;

  if (isRight && isLeft) {x=0; y=0;}

  else if (isUp && isRight) {x = -32767; y = -32767;}

  else if (isDown && isRight) {x = -32767; y = 32767;}

  else if (isDown && isLeft) {x = 32767; y = 32767;}

  else if (isUp && isLeft) {x = 32767; y = -32767;}

  else if (isUp) {y = -32767;}

  else if (isRight) {x = -32767;}

  else if (isDown) {y = 32767;}

  else if (isLeft) {x = 32767;}

 

  // Sends x and y coords to controller

  Gamepad.xAxis(x);

  Gamepad.yAxis(y);

  Gamepad.write();

 

  delay(50);

}

 

// Presses A button if controller has just switched from

// menu mode to movement mode

void checkIfJustSwitched() {

  if (!inMovementMode) {

    uint8_t  aButton = 1;

    Gamepad.press(aButton);

    inMovementMode = true;

  }

}

 

// Lets you control speed with the sword and speed of rotation

// with the garlic.

void movementMode() {

  // Read and map potentiometer to 0-255 throttle

  int potValue = analogRead(POT_PIN);

  throttle = map(potValue, 0, 1023, 0, 255);

 

  // Calculate new angle each cycle adjusted for the current

  // rotation speed.

  angle = (angle + rotationSpeed + 360) % 360;

 

  // Convert polar (angle, throttle) to joystick X/Y

  float rad = radians(angle);

  int x = cos(rad) * throttle;

  int y = sin(rad) * throttle;

 

  // Map to full Gamepad range (0 to 65535)

  int joyX = map(x, -255, 255, -32768, 32767);

  int joyY = map(y, -255, 255, -32768, 32767);

 

  // Sends x and y to controller

  Gamepad.xAxis(joyX);

  Gamepad.yAxis(joyY);

  Gamepad.write();

 

  // Debug output

  Serial.print("Throttle: ");

  Serial.print(throttle);

  Serial.print(" | Angle: ");

  Serial.print(angle);

  Serial.print(" | X: ");

  Serial.print(joyX);

  Serial.print(" | Y: ");

  Serial.println(joyY);

 

  delay(30);

}

 

void rotateLeft(Rotary& r) {

  // Debounce

  if (millis() - lastRotationTime < debounceDelay) return;

  lastRotationTime = millis();

 

  // Adjusts rotation speed in positive direction

  rotationSpeed = rotationSpeed + 1 ;

  if(rotationSpeed > maxRotationSpeed) rotationSpeed = maxRotationSpeed;

  Serial.print("Rotated Left. Rotation Speed: ");

  Serial.println(rotationSpeed);

 

  // Lets menu mode know if left has been pressed

  leftOptionSelectThisFrame = true;

 

}

 

// Same as above

void rotateRight(Rotary& r) {

  if (millis() - lastRotationTime < debounceDelay) return;

  lastRotationTime = millis();

 

  rotationSpeed = rotationSpeed - 1;

  if (rotationSpeed < -maxRotationSpeed) rotationSpeed = -maxRotationSpeed;

  Serial.print("Rotated Right. Rotation Speed: ");

  Serial.println(rotationSpeed);

 

  rightOptionSelectThisFrame = true;

 

}


VIDEO


 






No comments:

Post a Comment

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