Project Background
Project Prompt
For this project we were split into teams of three and given the assignment to develop a fully realized physical therapy device/experience to actually be used as a real tool for a five year old boy with Cerebral Palsy.
User Background
Interlude to give some background on the condition of cerebral palsy and what sort of therapy is required for it: cerebral palsy is a rare congenital condition that causes a variety of motor-control problems, including difficulty maintaining balance, severe weakness of muscles, involuntary movements, and poor coordination. While the condition cannot be cured, there have been a variety of methods and medicines developed to lessen symptoms. One of the primary tools is therapy– working with a trainer to improve muscular control and strength.
That training can often be quite difficult and dull for a young child, which holds back the trainer's ability to make progress against the condition. This is where we come in– we're given the task of working with the trainer to create a fun, yet still challenging, new method of working through therapy.
Design
Digital Experience Brainstorm
As a group, we brainstormed what we might do to engage the child in his therapy process. We began from the perspective of a five year old: what would we find entertaining and relevant at that age? We heard from his trainer that he was quite fond of space-themed things, so after a bit of back and forth discussion we arrived on creating some sort of space-based interactive game that could server as a unique medium for training.
Physical Experience Brainstorm
While we had the outlines of the digital experience quickly figured out relatively quickly, it was the physical control that was most important to get right. We had the opportunity of knowing exactly the dimensions and mechanism of the chair that was to be used for his therapy, allowing us to substitute the existing footplate with a new one of the same height but had two pressure sensitive buttons on it that would require him to push down with his feet to make the spaceship go faster.
Considering User Limitations
Next we moved onto considering what limitations his age and condition would have on the mechanisms of the gameplay. It was from this discussion that we decided on many of the game dynamics:
- Have a '1D' movement system with no side to side avoidance or backwards sliding. 2D movement with the need for 2-leg independent movement would probably be too complicated for the child at the current moment, and backwards movement upon a lack of button presses would just be a frustrating experience.
- Make the graphics playful and colorful enough to be engaging to very young children, but not in an overly flashy way that could over-stimulate a child with sensory sensitivity.
- Divide the game into specific themed levels to give a sense of progress and to create reproducible goals for the child's trainer to ask them to reach.
Development
First Digital Prototype
After we wrapped up our initial design discussions, we split the project into three different tasks: the programming of the digital game, the assembly of the physical interface, and the creation of a head-mounted system for reinforcing head-up posture through LED or game feedback. The first prototype presented was a basic tech demo of the digital game connected via arduino to simple buttons, and this is the feedback we received from the child's trainer and our classmates:
Second Digital Prototype
Taking into account these suggestions, I added more HUD elements for time and stage progress. I also overhauled the visual design of these elements to give them a more rounded and playful feel. Planets were added into the background to give a sense of progress and level design. Below is a video of a test run of the game with the internal logic sped up to better show the different levels.
Physical Prototype
Simultaneously to the work being done towards the digital game, there was also work towards a physical interface for the child. We were given an enclosure template which would allow us to make a base that can fit into the child's existing chair design. From this template we drilled out holes for the buttons and added a section to the bottom to house electronics.
The wiring was probably the hardest part of this stage. In order for the switch adapter to work properly, the buttons needed to be wired into a 3.5mm mono aux jack. There wasn't really much information available online for how to wire up the connection to the iPad, so there was plenty of trial and error to get to that stage. This is the wiring configuration that we ended up with:
Final Construction
We decided on layered cardboard and tape for the final project not out of convienience but rather mostly becuase it's freely available around the ITP floor. We had to conform to strict budgetary requirements and wanted to cut down in any way possible to make it easier other families to afford the product should we wish to expand the target user base. Most of the Adapative Design Associations products are made out of the material for the same reason, though they were a bit better in construction quality than we were. Below are images of the in-progress and final construction.
Final Codebase
Server Side (Hardware Interfacing)
//---------------------------------------------
// Node Server Setup Code
//---------------------------------------------
// Module Requirements
var express = require('express');
var path = require('path');
var app = express();
var isLeftButtonHeld = false;
var isRightButtonHeld = false;
// Set public folder for client-side access
app.use(express.static('public'));
// Send index.html at '/'
app.get('/', function(req, res){
res.sendFile(path.join(__dirname + '/views/index.html'));
});
//Send AJAX data stream at '/data'
app.get('/data', function(req,res) {
// Compile individual variables into object
var dataToSendToClient = {
'isLeftButtonHeld': isLeftButtonHeld,
'isRightButtonHeld': isRightButtonHeld
};
// Convert javascript object to JSON
var JSONdata = JSON.stringify(dataToSendToClient);
//Send JSON to client
res.send(JSONdata);
});
//Set app to port 3000
app.listen(3000);
//Log start of app
console.log("App Started");
//---------------------------------------------
// Johnny-Five Code
//---------------------------------------------
var five = require("johnny-five");
board = new five.Board();
board.on("ready", function() {
// Button Initialization
var leftButton = new five.Button("8");
var rightButton = new five.Button("9");
// Left Button Setup
leftButton.on("press", function() {
console.log( "Left Button Pressed" );
isLeftButtonHeld = true;
});
leftButton.on("release", function() {
console.log( "Left Button Released" );
isLeftButtonHeld = false;
});
// Right Button Setup
rightButton.on("press", function() {
console.log( "Right Button Pressed" );
isRightButtonHeld = true;
});
rightButton.on("release", function() {
console.log( "Right Button Released" );
isRightButtonHeld = false;
});
});
Client Side (Game Logic)
//------------------------------------------------------------------------
// Variable Declaration
//------------------------------------------------------------------------
// Flag Declarations
var __ignorePhysicalFlag = true;
// Boolean Declarations
var isInitDone = false;
var isLaunched = false;
var isInSpace = false;
// Time Tracking Declarations
var start = new Date();
// Streal Tracking Declarations
var streakNum = 0;
var streaks = [];
// General Declarations
var thrustSquares = 0;
var timer = new Timer();
var ticks = 0
var currentStage = 0;
var previousStage = 0;
// Stage 1 - Orbit
var stageOneStartingTick;
var stageOneTotalTicks = 2000;
const stageOneLocationStart = '18% + 6.65vh';
const stageOneLocationEnd = '16%';
const stageOneFlyingObjects = ['satellite','spaceShuttle','asteroid','astronaut','sputnik'];
const stageOneFlyingObjectsWidth = [20,30,30,15,20];
const stageOneFlyingObjectsRotation = [100,70,-45,15,45];
const stageOneFlyingObjectsAnimate = [5,8,5,8,7];
var stageOneFlyingObjectsTime = [];
// Stage 2 - Moon
var stageTwoStartingTick;
var stageTwoTotalTicks = 2000;
const stageTwoLocationStart = '43% + 6.65vh';
const stageTwoLocationEnd = '16%';
const stageTwoFlyingObjects = ['comet','comet2','comet3','comet4','lander'];
const stageTwoFlyingObjectsWidth = [20,30,10,40,20];
const stageTwoFlyingObjectsRotation = [-45,-45,-45,-45,0];
const stageTwoFlyingObjectsAnimate = [5,6,4,5,8];
var stageTwoFlyingObjectsTime = [];
// Stage 3 - Saturn
var stageThreeStartingTick;
var stageThreeTotalTicks = 2000;
var stageThreeLocationStart = '68% + 6.65vh';
var stageThreeLocationEnd = '13.75%';
const stageThreeFlyingObjects = ['newHorizons','comet5','asteroid2'];
const stageThreeFlyingObjectsWidth = [35,30,30];
const stageThreeFlyingObjectsRotation = [0,-45,-45];
const stageThreeFlyingObjectsAnimate = [9,4,5];
var stageThreeFlyingObjectsTime = [];
// Stage 4 - Star
var stageFourStartingTick;
var tickPercentage;
//------------------------------------------------------------------------
// AJAX Server Request
//------------------------------------------------------------------------
//On Document Ready
$(document).ready(function(){
//Log to check onload
console.log('JQuery Loaded');
// Send an AJAX JSON GET request to server every 20ms for data
// This interval function runs through the Main Scripting Loop
setInterval(function() {
$.ajax({
// Request Attributes
url : 'http://localhost:3000/data',
type : 'GET',
dataType:'json',
// On Request Success
success : function(data) {
// Loop through attributes of given JSON Object
// to deconstruct object into variables
for (var property in data) {
// Set previous property value as var to track change
window[property + 'Old'] = window[property];
// Set property name as var equal to property
window[property] = data[property];
}
},
// On Request Error
error : function(request,error) {
console.log("Request: "+JSON.stringify(request));
}
});
//------------------------------------------------------------------------
// Main Scripting Loop
//------------------------------------------------------------------------
//---------------------------------------------------
// Store Button States as Variables
// If both buttons are held
if(isLeftButtonHeld && isRightButtonHeld){
thrustSquares = 8;
}
// If only one button is held
else if((isLeftButtonHeld && !isRightButtonHeld) || (!isLeftButtonHeld && isRightButtonHeld)){
thrustSquares = 0;
}
// If neither is held
else{thrustSquares = 0;}
// Test Flag to Ignore Physical Buttons
if(__ignorePhysicalFlag){
thrustSquares = 8;
isLeftButtonHeld = true;
isRightButtonHeld = true;
}
//---------------------------------------------------
// Utilize Thurst Variables
// Things to Apply Only in Space
if(isInSpace){
// Change Streak Heights
$(".streak").css("height",(thrustSquares+1)*25);
// Change Rocket Position
$("#rocket").css("bottom", 4.25 * thrustSquares + 10 + "vh");
}
// Display Thrust Meter
for(i=1;i<=thrustSquares+1;i++){
$("#thrustSquare" + i).css("background-color","lime");
}
for(i=thrustSquares+1;i<=8;i++){
$("#thrustSquare" + i).css("background-color","#343434");
}
//---------------------------------------------------
//If Both Buttons are Held
if(isLeftButtonHeld && isRightButtonHeld){
$("#thrustMeterTag").css("background-image","url('../img/fireIconGreen.png')");
// Rocket Launch Animation
// If Rocket not yet Launched
if(!isLaunched){
timer.start();
isLaunched = true;
// Phase 1
$("#fire").animate({bottom: "-=5vh"}, 1000, function(){
$("#fire").css("animation-name","fireFlicker");
});
$("#rocket").animate({bottom: "+=30vh"}, 4000);
$("#platform").animate({bottom: "-=60vh"},4000);
$("#backgroundOne").animate({opacity: 0},7000);
$("#locationLine").animate({height: "18%"},15000, function(){
currentStage = 1;
});
// Phase 2
$("#backgroundTwo").animate({opacity: 1},9000);
$("#backgroundTwo").animate({opacity: 0},9000);
// Phase 3
$("#rocketContainer").css("filter","brightness(0.8)");
setTimeout(function(){
isInSpace = true;
},3000);
$("#backgroundThree").animate({opacity: 1},10000);
$("#backgroundThree").animate({opacity: 0},10000);
$("#rocket").animate({bottom: "-=10vh"},2500);
}
}else{
$("#thrustMeterTag").css("background-image","url('../img/fireIconGrey.png')");
}
//---------------------------------------------------
// General Scripting
// Format Timer Display Text
var minutes = Math.floor(timer.ticks()/60);
if(minutes.toString().length<2){minutes = "0" + minutes;}
var seconds = timer.ticks()%60;
if(seconds.toString().length<2){seconds = "0" + seconds;}
// Display Time Value
$("#timeMeter").html( minutes + ":" + seconds );
// Change Engine Fire
if(thrustSquares > 0){
$("#fire").css("animation-name","fireFlicker");
$("#fire").css("bottom","-5vh");
} else{
$("#fire").css("animation-name","none");
$("#fire").css("bottom","2vh");
}
// Apply Stage Chages to Location Meter
if(currentStage == 0){
$("#liftoffCircle").css("background-color","#00ff00");
$("#locationMarkerLiftoff").css("background-image","url('../img/liftoffIconGreen.png')");
}
//-------------------------
// All Stages
// Stage 1
if(currentStage == 1){stageOne()}
// STAGE 2
if(currentStage == 2){stageTwo()}
// STAGE 3
if(currentStage == 3){stageThree()}
// STAGE 4
if(currentStage == 4){stageFour()}
//-------------------------
// Increment Logic Tracker
// 2 Ticks if Full Thrust
if(thrustSquares > 4){ticks+=2}
// 1 Tick if Half Thrust
else if(thrustSquares > 0 && thrustSquares <= 4){ticks++}
// End AJAX Call Main Scripting Loop
}, 20);
// Generate and Remove Streaks
setInterval(function() {
if(isInSpace){
if(thrustSquares > 0){
// Generate Streaks
$("body").append("<div class='streak' id='streak"+streakNum+"'></div>");
streaks.push('streak'+streakNum);
$("#"+streaks[streakNum]).css("left",Math.random()*100+"vw");
// Remove Streaks
$("#"+streaks[streakNum-25]).remove();
streakNum++;
}
}
}, 200);
});
//------------------------------------------------------------------------
// Function Declaration
//-----------------------------------------------------------------------
// STAGE ONE
function stageOne(){
// Executed just on first tick
if(previousStage == 0){
// Reset Liftoff Location Meter to Grey
$("#liftoffCircle").css("background-color","#343434");
$("#locationMarkerLiftoff").css("background-image","url('../img/liftoffIconGrey.png')");
// Set Orbit Location Meter to Lime
$("#orbitCircle").css("background-color","#00ff00");
$("#locationMarkerOrbit").css("background-image","url('../img/satelliteIconGreen.png')");
// Set Location Line to Start
$("#locationLine").css("height","calc(" + stageOneLocationStart + ")");
// Flying Object Position
for(flyingObject in stageOneFlyingObjects){
// Get Object Name
let name = stageOneFlyingObjects[flyingObject];
// Generate Object Start Time
stageOneFlyingObjectsTime.push(Math.random()*0.9);
// Generate CSS for Object
$("body").append("<div class='flyingObject' id='" + name + "'></div>")
$("#" + name).css({
"background-image" : "url(../img/" + name + ".svg)",
"right" : Math.random() * 200 + "vh",
"width" : stageOneFlyingObjectsWidth[flyingObject] + "vh",
"height" : stageOneFlyingObjectsWidth[flyingObject] + "vh",
"top" : -stageOneFlyingObjectsWidth[flyingObject] - 2 + "vh",
"transform" : "rotate(" + stageOneFlyingObjectsRotation[flyingObject] + "deg)"
});
}
// Update Variables
previousStage = 1;
stageOneStartingTick = ticks;
tickPercentage = 0;
}
// Re-Executed every tick
// Precent to next stage, from 0 to 1
tickPercentage = ((ticks - stageOneStartingTick)/stageOneTotalTicks);
// Apply Earth Position
$("#earth").css("top", (tickPercentage*210) - 110 + "vh");
// Flying Objects
for(flyingObject in stageOneFlyingObjects){
let name = stageOneFlyingObjects[flyingObject];
if(tickPercentage >= stageOneFlyingObjectsTime[flyingObject] && thrustSquares > 0){
$("#" + name).animate({top: $(document).height() + $("#" + name).height()}, stageOneFlyingObjectsAnimate[flyingObject]*1000);
}
}
// Apply Location Meter Position
$("#locationLine").css("height","calc(((" + stageOneLocationEnd + "-" + stageOneLocationStart + ") * " + tickPercentage + ") + " + stageOneLocationStart + ")");
if(tickPercentage >= 1){
currentStage = 2;
}
}
//----------------------------------------------
// STAGE TWO
function stageTwo(){
// Executed just on first tick
if(previousStage == 1){
// Reset Orbit Location Meter to Grey
$("#orbitCircle").css("background-color","#343434");
$("#locationMarkerOrbit").css("background-image","url('../img/satelliteIconGrey.png')");
// Set Moon Location Meter to Lime
$("#moonCircle").css("background-color","#00ff00");
$("#locationMarkerMoon").css("background-image","url('../img/moonIconGreen.png')");
// Flying Object Position
for(flyingObject in stageTwoFlyingObjects){
// Get Object Name
let name = stageTwoFlyingObjects[flyingObject];
// Generate Object Start Time
stageTwoFlyingObjectsTime.push(Math.random()*0.9);
// Generate CSS for Object
$("body").append("<div class='flyingObject' id='" + name + "'></div>")
$("#" + name).css({
"background-image" : "url(../img/" + name + ".svg)",
"left" : (Math.random() * 200) + "vh",
"width" : stageTwoFlyingObjectsWidth[flyingObject] + "vh",
"height" : stageTwoFlyingObjectsWidth[flyingObject] + "vh",
"top" : -stageTwoFlyingObjectsWidth[flyingObject] - 2 + "vh",
"transform" : "rotate(" + stageTwoFlyingObjectsRotation[flyingObject] + "deg)"
});
}
// Bring up Moon Display
$("#moon").css("display","block");
// Remove Earth Display
$("#earth").css("display","none");
// Update Variables
previousStage = 2;
stageTwoStartingTick = ticks;
tickPercentage = 0;
}
// Re-Executed every tick
// Precent to next stage, from 0 to 1
tickPercentage = ((ticks - stageTwoStartingTick)/stageTwoTotalTicks);
// Apply Location Meter Position
$("#locationLine").css("height","calc(((" + stageTwoLocationEnd + "-" + stageTwoLocationStart + ") * " + tickPercentage + ") + " + stageTwoLocationStart + ")");
// Apply Moon Position
$("#moon").css("top", (tickPercentage*190) -85 + "vh");
// Flying Objects
for(flyingObject in stageTwoFlyingObjects){
let name = stageTwoFlyingObjects[flyingObject];
if(tickPercentage >= stageTwoFlyingObjectsTime[flyingObject] && thrustSquares > 0){
$("#" + name).animate({top: $(document).height() + $("#" + name).height()}, stageTwoFlyingObjectsAnimate[flyingObject]*1000);
}
}
if(tickPercentage >= 1){
currentStage = 3;
}
}
//----------------------------------------------
// STAGE THREE
function stageThree(){
// Executed just on first tick
if(previousStage == 2){
// Reset Orbit Location Meter to Grey
$("#moonCircle").css("background-color","#343434");
$("#locationMarkerMoon").css("background-image","url('../img/moonIconGrey.png')");
// Set Moon Location Meter to Lime
$("#saturnCircle").css("background-color","#00ff00");
$("#locationMarkerSaturn").css("background-image","url('../img/saturnIconGreen.png')");
// Bring up Saturn Display
$("#saturn").css("display","block");
// Remove moon Display
$("#moon").css("display","none");
// Flying Object Position
for(flyingObject in stageThreeFlyingObjects){
// Get Object Name
let name = stageThreeFlyingObjects[flyingObject];
// Generate Object Start Time
stageThreeFlyingObjectsTime.push(Math.random()*0.9);
// Generate CSS for Object
$("body").append("<div class='flyingObject' id='" + name + "'></div>")
$("#" + name).css({
"background-image" : "url(../img/" + name + ".svg)",
"right" : Math.random() * 200 + "vh",
"width" : stageThreeFlyingObjectsWidth[flyingObject] + "vh",
"height" : stageThreeFlyingObjectsWidth[flyingObject] + "vh",
"top" : -stageThreeFlyingObjectsWidth[flyingObject] - 2 + "vh",
"transform" : "rotate(" + stageThreeFlyingObjectsRotation[flyingObject] + "deg)"
});
}
// Update Variables
previousStage = 3;
stageThreeStartingTick = ticks;
tickPercentage = 0;
}
// Re-Executed every tick
// Precent to next stage, from 0 to 1
tickPercentage = ((ticks - stageThreeStartingTick)/stageThreeTotalTicks);
// Apply Location Meter Position
$("#locationLine").css("height","calc(((" + stageThreeLocationEnd + "-" + stageThreeLocationStart + ") * " + tickPercentage + ") + " + stageThreeLocationStart + ")");
// Apply Moon Position
$("#saturn").css("top", (tickPercentage*240) - 130 + "vh");
// Flying Objects
for(flyingObject in stageThreeFlyingObjects){
let name = stageThreeFlyingObjects[flyingObject];
if(tickPercentage >= stageThreeFlyingObjectsTime[flyingObject] && thrustSquares > 0){
$("#" + name).animate({top: $(document).height() + $("#" + name).height()}, stageThreeFlyingObjectsAnimate[flyingObject]*1000);
}
}
if(tickPercentage >= 1){
currentStage = 4;
}
}
//----------------------------------------------
// STAGE FOUR
function stageFour(){
// Executed just on first tick
if(previousStage == 3){
// Reset Orbit Location Meter to Grey
$("#saturnCircle").css("background-color","#343434");
$("#locationMarkerSaturn").css("background-image","url('../img/saturnIconGrey.png')");
// Set Moon Location Meter to Lime
$("#starCircle").css("background-color","#00ff00");
$("#locationMarkerStar").css("background-image","url('../img/starIconGreen.png')");
// Update Variables
previousStage = 4;
stageThreeStartingTick = ticks;
}
}