ITP Notebook [2020 - 2022]

Project 2: Labyrinth

November 05, 2020

Physical Computing

Overview

Labyrinth is an arcade-style puzzle game where you use a physical controller to roll a virtual marble through a maze. It consists of a table-top device that interfaces via serial communication to an app built with Unity. It also features custom-made sounds and spacial audio (note: the video audio is pretty quiet).

The Controller

The physical controller consists of two knobs and a button connected to an Arduino Nano 33 IoT. View the Bill of Materials.

The circuit diagram shows two potentiometers and a push button each connected to the Arduino and a 10kΩ pulldown resistor

The wiring is relatively straightforward. Two potentiometers and a push button each connect to the input voltage, the Arduino, and a 10kΩ pulldown resistor.

Housing

For the housing I used a small box that happened to fit the breadboard perfectly… on three sides.

The interior The interior

I cut holes the box using a knife and secured the potentiometers in place with their included nuts. I also commandeered two caps from my stereo for the knobs.

Securing the potentiometers Securing the potentiometers

Two knobs pilfered from my stereo Two knobs pilfered from my stereo

The final step was configuring the pushbutton so that pressing the top of the box would press the switch. I did this by taping a stiff piece of cardboard to the button itself and carefully lowering the lid in to place.

The cardboard button extender The cardboard button extender

I wanted the top to remain in place but also be easily openable so I then taped it down.

The finished housing The finished housing

The finished housing looks a bit rough but feels solid, with both knobs securely fastened to the sides.

Pressing the top "Start" button

The knobs feel solidly connected

The Firmware

The hardware code is fairly simple. It reads the values from the sensors and button, maps the analog input to a value between 0 and 100, and prints the results on one line to the serial port. It also establishes a “handshake” — it will wait for serial input before writing output.

const int Z_POT_PIN = A1;
const int X_POT_PIN = A2;
const int BUTTON_PIN = 2;

void setup() {
  Serial.begin(9600);
  pinMode(X_POT_PIN, INPUT);
  pinMode(Z_POT_PIN, INPUT);
  pinMode(BUTTON_PIN, INPUT);
  while (Serial.available() <= 0) {
    Serial.println("waiting...");
    delay(300);
  }
}

int clamp(int input) {
  if (input < 0) {
    return 0;
  }
  if (input > 100) {
    return 100;
  }
  return input;
}

void loop() {
  if (!Serial.available()) return;

  int zVal = analogRead(Z_POT_PIN);
  int mappedZ = clamp(map(zVal, 480, 1024, 0, 100));

  int xVal = analogRead(X_POT_PIN);
  int mappedX = clamp(map(xVal, 480, 1024, 0, 100));

  int buttonState = digitalRead(BUTTON_PIN);

  Serial.print(mappedX);
  Serial.print(',');
  Serial.print(mappedZ);
  Serial.print(',');
  Serial.print(buttonState);
  Serial.println();

  delay(50);
}

The Game

The title screen The title screen

The game is built using Unity and takes advantage of the game engine’s built in physics simulation.

The Unity editor The Unity editor

The game board contains two types of obstacle (three if you include the walls) — holes marked by red lights and a yellow “spring” wall that propels the marble backwards when it makes contact. The goal is to reach the green cube in the bottom right.

Isometric view of the game board Isometric view of the game board

Scripting

The primary scripting occurs in the GameController and the MarbleController. The GameController handles the serial port connection and applies the rotation to the game board based on the input. It also handles logic for starting and resetting the game with the physical button press. The MarbleController handles collisions with the different colliders to determine if the player has won or lost, and also applies force when the marble hits the yellow spring wall.

Audio

I designed several sound effects that correspond to the different events and objects in the game. Some are spacial — the holes and goal produce audio that gets louder as the marble approaches. There are also sounds for starting the game, winning, losing, and hitting the spring wall. The marble itself produces a looping note that increases in pitch in relation to how fast it is moving.

Game Start sound

Loss sound

Sound emanating from the cube

Win sound

Sound of the void

Sound emanating from the holes

Spring/Bounce sound

Marble rolling loop