20 April 2025

The Stanley Parable Marionette Controller

 



Our controller was designed for critically acclaimed walking simulator, The Stanley Parable. In The Stanley Parable, the player is frequently treated as a separate being to the player character Stanley. The player merely controls Stanley actions and movements, just like a puppeteer controls a puppet. That comparison inspired the conceptual model we used. Instead of just metaphorically puppeteering Stanley through the game, you are now literally puppeteering Stanley.  

Our controller is meant to have a very neutral color scheme, using the natural color of the wood. We chose this, both because we thought it looked nice, but also because it fit in with the general aesthetic of The Stanley Parable. The wires being visible are meant to represent the strings normally attached to puppet controllers.

Using a gyroscope housed inside each controller, the player can move around and control the camera, as well as click on objects and press buttons. The right controller controls player movement, while the left controller controls the camera. Tilting the right controller forward moves the player forward, tilting left moves left, tilting right moves right, tilting back moves backwards. The same general principle follows for the right controller, only it moves the camera instead. By quickly moving the right controller up, the player can make a generic keyboard input. By quickly moving the left controller up, the player can click on an object in-game.

In other words, we mapped tilting a controller in a direction to moving whatever thing said controller controls in the same direction.

The signifier as to what input a player is inputting is the direction they are tilting the controllers, and the feedback is Stanley or the camera moving in that direction. The signifier for inputting a keyboard input or a click input is the player moving the controller up, which has the feedback of the game performing that input.

The affordances of our design makes it so that the player can control both the camera and Stanley simultaneously, as well as perform clicks and button inputs. The limitations are that, functionally speaking, the controller can only make two non-movement/non-camera inputs.

Our Question: What other games do you believe this controller would be well-suited for, either mechanically or thematically?


Video: https://drive.google.com/file/d/1pfHmym14S9zgqTD6UF5qc1okjSb_ZxnX/view


Schematic:

Code:

#include <Keyboard.h>
#include <Adafruit_MPU6050.h>
#include <Adafruit_Sensor.h>
#include <Wire.h>
#include <Mouse.h>

Adafruit_MPU6050 mpu; // Address 0x68 - for movement/keyboard
Adafruit_MPU6050 mpu2; // Address 0x69 - for mouse

// Variables
float moveX = 0;
float moveY = 0;
float mouseX = 0;
float mouseY = 0;
float sensitivity = 40.0; // Mouse Sensitivity

void setup(void) {
Serial.begin(115200);
while (!Serial) delay(10);

// Initialize first MPU and check for testing purposes
if (!mpu.begin(0x68)) {
Serial.println("Failed to find MPU6050 at 0x68");
while (1) delay(10);
}
Serial.println("MPU6050 #1 Found at 0x68!");

// Initialize second MPU (ADO high) and check for testing purposes
if (!mpu2.begin(0x69)) {
Serial.println("Failed to find MPU6050 at 0x69");
while (1) delay(10);
}
Serial.println("MPU6050 #2 Found at 0x69!");

// Configure both sensors
mpu.setAccelerometerRange(MPU6050_RANGE_8_G);
mpu.setGyroRange(MPU6050_RANGE_500_DEG);
mpu.setFilterBandwidth(MPU6050_BAND_21_HZ);

mpu2.setAccelerometerRange(MPU6050_RANGE_8_G);
mpu2.setGyroRange(MPU6050_RANGE_500_DEG);
mpu2.setFilterBandwidth(MPU6050_BAND_21_HZ);

delay(100);

// Initialize keyboard and mosue libraries
Keyboard.begin();
Mouse.begin();
}

void loop() {
// Get events from first MPU (movement control)
sensors_event_t a, g, temp;
mpu.getEvent(&a, &g, &temp);

// Get events from second MPU (mouse control)
sensors_event_t a2, g2, temp2;
mpu2.getEvent(&a2, &g2, &temp2);

float deltaTime = 0.01;

// Use MPU g for movement
moveX += g.gyro.y * deltaTime - 0.0005; // Adjust for drift
moveY += g.gyro.x * deltaTime;

// Use MPU2 g2 for mouse
mouseX -= g2.gyro.y * deltaTime * sensitivity;
mouseY -= -1 * (g2.gyro.x * deltaTime * sensitivity + 0.03); // Adjust for drift

// Move mouse directily to
Mouse.move(mouseX, mouseY);

// Thresholds for inputs
float threshold = 0.1;

// FORWARD / BACKWARD (W/S)
if (moveY > threshold) {
Keyboard.release('s');
Keyboard.press('w');
Serial.println("W");
} else if (moveY < -threshold) {
Keyboard.release('w');
Keyboard.press('s');
Serial.println("S");
} else {
Keyboard.release('w');
Keyboard.release('s');
}

// LEFT / RIGHT (A/D)
if (moveX > threshold) {
  Keyboard.release('d');
  Keyboard.press('a');
  Serial.println("A");
} else if (moveX < -threshold) {
  Keyboard.release('a');
  Keyboard.press('d');
  Serial.println("D");
} else {
  Keyboard.release('a');
  Keyboard.release('d');
}


// JUMP (Z-axis acceleration spike on movement MPU)
if (a.acceleration.z > 15.0) {
Keyboard.press(' ');
Serial.println("Jump");
} else {
Keyboard.release(' ');
}
// All other possible prompts (Z-axis acceleration spike on mouse MPU)
if (a2.acceleration.z > 15.0)
{
  Keyboard.press('e');
  Keyboard.press('r');
  Keyboard.press('q');
  Keyboard.press('t');
  Keyboard.press('y');
  Keyboard.press('u');
  Keyboard.press('i');
  Serial.println("all buttons");
} else {
  Keyboard.release('e');
  Keyboard.release('r');
  Keyboard.release('q');
  Keyboard.release('t');
  Keyboard.release('y');
  Keyboard.release('u');
  Keyboard.release('i');
}

// Gentle decay to prevent imbalance and adjust for different resting positions
moveX *= 0.995;
moveY *= 0.995;
mouseX *= 0.995;
mouseY *= 0.995;

delay(10);
}



No comments:

Post a Comment

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