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
#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.