Slipperhop

Slipperhop is an arcade style game played with slippers. Inside the slippers, there are pressure sensors made from velostat and copper tape. These DIY force sensitive resistors work because velostat’s conductivity is dependent on the pressure applied to it. Velostat is very flexible and thus works great with soft materials (such as slippers).

Making Slipperhop

Materials

Slippers (ideally ones that can be opened as the design includes insoles)

Velostat (e.g. https://www.adafruit.com/product/1361)

Copper foil tape (or other conductive flat material)

2 insoles per slipper (the velostat and the tape will be glued on them)

Resistors, wire, arduino etc. (the basics for putting it all together)

Learning how to make my own pressure sensors

I started with trying out an existing pressure sensor and seeing what kind of data it gives. Detail image To make the first version of pressure sensors with velostat, I only used wires on each side of the patch. Detail image With copper tape, the setup is similar. The tape can be placed on both sides of the the velostat or next to each other on one side (they should not touch each others). Detail image For better reading, here is a sketch of a circuit for 4 pressure sensors. I ended up using 100 ohm resistors. The bigger the resistor, more sensitive the sensors become. Detail image

Reading multiple sensors from Arduino:

int w1;
int w2;
int w3;
int w4;

void setup() {
  Serial.begin(9600);

}

void loop() {

  // read the values
  w1 = analogRead(A0);
  w2 = analogRead(A1);
  w3 = analogRead(A2);
  w4 = analogRead(A3);

  // print the values 
  //Serial.println("Min:0 Max:1023");

  Serial.print(w1);
  Serial.print(",");
  Serial.print(w2);
  Serial.print(",");
  Serial.print(w3);
  Serial.print(",");
  Serial.println(w4);

  delay(10);

}

Connecting to Processing

I used serial connection to read the data inside Processing.

Note: You have to close serial in Arduino in order for Processing to read the values.

A simple sketch to check the connection:

Processing code:

import processing.serial.*;

Serial myPort;
int w1;
int w2;
int w3;
int w4;

void setup() {
  fullScreen();
  // Change the port name to match your Arduino port
  String portName = Serial.list()[0];
  myPort = new Serial(this, portName, 9600);
}

void draw() {
  if (myPort.available() > 0) {
    // Read the values as strings
    String[] values = myPort.readStringUntil('\n').trim().split(",");

    // Check if there are 4 values
    if (values.length == 4) {
      // Parse the values as integers
      w1 = int(values[0]);
      w2 = int(values[1]);
      w3 = int(values[2]);
      w4 = int(values[3]);
    }
    
    background(255);

    // Map the sensor data into what works with your visual
    int mappedw1 = int(map(w1, 800, 1023, 0, 400));
    int mappedw2 = int(map(w2, 800, 1023, 0, 400));
    int mappedw3 = int(map(w3, 800, 1023, 0, 400));
    int mappedw4 = int(map(w4, 800, 1023, 300, 800));
    
    // The visuals
    fill(150,100,200);
    ellipse(width/2,height/2,mappedw4,mappedw3);
    fill(150,200,200);
    circle(width/2,mappedw4,mappedw1);
    rect(mappedw4,height/2,mappedw2,mappedw2);
    
    // Have values on screen 
    textSize(20);
    text(mappedw1,50,50);
    text(mappedw2,50,70);
    text(mappedw3,50,90);
    text(mappedw4,50,110);
  }
}

Making the slippers

The slippers have two insoles inside. The copper tape and the velostat are glued on each insole.
Detail image Detail image

When the slippers are used, the wires are constantly moving, which makes the breadboard very unreliable. To make the design more durable, I designed the circuit on a SparkFun Qwiic Shield. Detail image

Coding the game

I won’t go into too much detail about how the code works, but here is the full game with some comments.

Font: https://www.1001freefonts.com/pixel.font

Sound: https://freesound.org/people/MATRIXXX_/sounds/402767/

import java.util.ArrayList;
import java.util.Iterator;
import processing.serial.*;
import ddf.minim.*;

Minim minim;
AudioPlayer coin;

// Load font
PFont pixel;

// Serial communication setup
Serial myPort;
float left, right;

// Enemy class definition (the enemies are the the falling boxes
class Enemy {
  float x, y, speed;
  color colorLeft = color(#C5EADB);
  color colorRight = color(#C5C5EA);
  color colorBoth = color(#F9FAD1);
  int type; //If the enemy is left, right or both 

  // Constructor for Enemy
  Enemy(float x_, float y_) {
    x = x_;
    y = y_;
    speed = initialSpeed;
    type = int(random(3)); //If the enemy is left, right or both 
    slippery = int(random(5)); //How often the enemies shake
  }

  // Get color based on enemy type
  color getColor() {
    if (type == 0) {
      return colorLeft;
    } else if (type == 1) {
      return colorRight;
    } else {
      return colorBoth;
    }
  }

  // Move the enemy down the screen
  void fall() {
    y += speed;
    if (slippery == 1) { //shake
      int randomize = int(random(3));
      if (randomize == 0) {
        x=x+7;
      }
      else {
        x = x-7;
      }
    }
  }

  // Update enemy speed
  void updateSpeed(float newSpeed) {
    speed = newSpeed;
  }

  // Display the enemy on the screen
  void display() {
    fill(getColor());
    //noFill();
    stroke(0);
    strokeWeight(2);
    if (type == 2) { //both feet
      rect(x-enemySize/2, y, enemySize, enemySize, 20);
      rect(x+enemySize/2, y, enemySize, enemySize, 20);
    } else { //left or right 
      rect(x, y, enemySize, enemySize, 10);
      if (type == 1) {
        triangle(x-50, y-50, x-50, y+50, x+50, y);
      } else {
        triangle(x+50, y-50, x+50, y+50, x-50, y);
      }
    }
  }
}

// List to store enemy objects
ArrayList<Enemy> enemies = new ArrayList<Enemy>();

// Game state and score variables
boolean start; //to know if start was pressed
int state = 0; //to know if left, right or both feet
int points = 0;
int highscore = 0;
float initialSpeed = 4;
float enemySpeedIncrement = 0.3;  // Increment speed by this value
int slippery; //shake
float enemySize = 250;

// Setup function, runs once at the beginning
void setup() {
  fullScreen();
  myPort = new Serial(this, "COM4", 9600);
  left = 0.0; 
  right = 0.0;
  rectMode(CENTER); // Enemies always fall from the center of the screen

  // Initialize Minim
  minim = new Minim(this);

  // Load the WAV file
  coin = minim.loadFile("coin.wav");

  pixel = createFont("Pixel NES.otf", 128);
}

// Draw function, continuously executed
void draw() {

  // Read data from serial port if available
  readSerialData();

  // Start the game
  if (mousePressed) // User has clicked the button
  {
    if (mouseX > width/2 - 300 && mouseX < width/2 + 300 && mouseY > height/2 - 150 && mouseY < height/2 + 150)
    {
      start = true;
    }
  }

  // Start screen 
  if (start == false) { 
    // Draw the background and game information
    background(255);

    fill(255);
    strokeWeight(2);
    if (mouseX > width/2 - 300 && mouseX < width/2 + 300 && mouseY > height/2 - 150 && mouseY < height/2 + 150) {
      strokeWeight(10);
      fill(#C5EADB);
    }
    rect(width/2, height/2, 600, 300, 20);
    
    fill(50);
    textFont(pixel);
    text("slipperhop", width/2, 150); 
    textSize(30);
    text("by Jonna Eloranta", width/2, 250); 
    textSize(80);
    textAlign(CENTER, CENTER);
    fill(50);
    text("start", width/2, height/2);
  }

  // Run game 
  if (start == true) {

    background(255);

    // Update game state based on sensor values
    updateGameState();

    // Add enemies periodically
    addEnemiesPeriodically();

    // Iterate through and display enemies
    updateAndDisplayEnemies();

    drawGameInfo();

    // Draw a line indicating the danger zone
    drawDangerZone();
  }
}

// Function to read data from the serial port
void readSerialData() {
  if (myPort.available() > 0) {
    String data = myPort.readStringUntil('\n');
    if (data != null) {
      // Trim leading and trailing whitespace
      data = data.trim();
      String[] values = split(data, ',');
      if (values.length >= 2) {
        left = float(values[0]);
        right = float(values[1]);
      }
    }
  }
}

// Function to draw game information on the screen
void drawGameInfo() {
  textFont(pixel);
  fill(50);
  textSize(65);
  textAlign(LEFT);
  text("Slipperhop", 20, 80);
  text(points, width-400, 80);
  textSize(30);
  text("Highscore: " + highscore, width-400, 150);

  if (state == 0) {
    text("Right foot", 20, height-150);
  }
  if (state == 1) {
    text("Left foot", 20, height-150);
  }
  if (state == 2) {
    text("Both feet", 20, height-150);
  }
  textSize(24);
  text("Left: " + left, 20, height-100);
  text("Right: " + right, 20, height-80);
}

// Function to check which foot was pressed
void updateGameState() { // These values should be udjusted to fit the sensor values
  if (left <= 150 && right >= 150) {
    state = 0;
  } else if (left >= 350 && right <= 700) {
    state = 1;
  } else if (left >= 350 && right >= 700) {
    state = 2;
  }
}

// Function to add enemies periodically
void addEnemiesPeriodically() {
  if (frameCount == 100 || (enemies.size() > 0 && enemies.get(enemies.size() - 1).y > enemySize)) {
    // Increase the speed gradually
    initialSpeed += enemySpeedIncrement;
    for (Enemy enemy : enemies) {
      enemy.updateSpeed(initialSpeed);
    }
    Enemy enemy = new Enemy(width / 2, 0);
    enemies.add(enemy);
  }
}

// Function to iterate through and display enemies
void updateAndDisplayEnemies() {
  Iterator<Enemy> it = enemies.iterator();
  while (it.hasNext()) {
    Enemy enemy = it.next();
    enemy.fall();
    enemy.display();
    color enemyColor = enemy.getColor();

    // Check if an enemy is in the scoring zone
    if (enemy.y >= height - 150 && enemy.y < height - 70) {
      if ((state == 0 && enemyColor == enemy.colorRight) || (state == 1 && enemyColor == enemy.colorLeft) || (state == 2 && enemyColor == enemy.colorBoth)) {
        it.remove();
        coin.rewind();
        coin.play();
        points++;
        if (points > highscore) {
          highscore = points;
        }
      }
    }

    // Check if an enemy has reached the bottom of the screen, and loses
    if (enemy.y > height) {
      points = 0;
      initialSpeed = 4;
      it.remove();
    }
  }
}

// Function to draw a line indicating the danger zone
void drawDangerZone() {
  strokeWeight(2);
  line(0, height - 150, width, height - 150);
}

Remaining issues / suggestions for future development

Slippers

The biggest issue with the slippers currently is unreliable data. The scale of values is more or less the same, no matter how long you play but both the lowest and highest values increase in numbers. This makes setting up the correct values hard for the game. On top of this, as all people have different weight, feet size, and playing style, the scale of values differs slightly for each player. To start fixing this, I thought of implementing calibration, at least to the beginning of the game. In this way, the game could check what are the min and max values for each player. Now, the values that trigger the game to recognise which foot was pressed, need to be adjusted manually (can be found inside the function “void updateGameState()” in the code).

Another issue is the insole material. The insoles I used are made of foam, which is not the best for glueing. Now, in order to make it more durable, I also added tape to hold the copper in place but ideally, the insoled should be of different material. I think that having different material insoles could also help with the consistancy of the sensor values.

Processing

Currently, there is an issue with the start screen and enemies. The game has a start screen but since the enemies were coded to start falling at a certain frame, unless the player presses “start” before that frame, the enemies won’t start falling.

I would also like to add a highscore system that updates between runs. Now, every time the game is run, the highscore is at a 0. I would try to do this by updating the highscore to a txt file.

Final words

This project has been a lot of fun! Going from not knowing almost anything about arduino or how to work with electronics in general to making a functioning prototype and game was rewarding. Even though there is a lot left to develop with this prototype, I’m happy with the practical knowledge I’ve gained during this course.

The original idea was to make a dance mat for VJing. As you can see, the end result ended up being quite different. The biggest reason for this being the lack of sensor data, as there are currently only two sensors inside the slippers. Making interesting visuals with only two different values was difficult and an arcade game seemed like a better direction for the slippers. In the end, making the game also taught a lot about Processing as it recuired problem solving for specific challenges that I would have not encountered if I had focused on purely visual output. However, I would still like to explore the original idea in the future and keep experimenting with the possibilities of sensoring movement and touch to create visuals. I would also like to try embedding similar sensors into other pieces of clothing / the rest of the body. This could give interesting possibilitites for dance performances.

The first idea

Alternative controller / custom midi for live visuals

I want to create something that lets me control the visual output of my Processing sketches in a fun way.

Dancing VJ

A dance mat that can be used for creating live visuals through force sensing resistors. Could be small (hands) or big (full body). I would also be interested in making it two-player.

Detail image

I would make a game that lets the player choose between a few songs and then have the visuals react to the dancing. Would also be fun if the player could save the playthroughs as music videos but not sure how to do this yet.