Ethan Printz
Assistive Rocket Adventure
A custom physical controller and associated rocket adventure game created as a novel and fun therapy tool for a five year old boy with cerebral palsy.
Overview
Course
Assistive Technology
Term
Spring 2019
Role
3 Student Group - Part of Design and All of Development

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:

  1. 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.
  2. 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.
  3. 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:
feedbackCards.svg

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:
programmingDiagram2.svg

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.
projectWiring.jpg
finalProductImage.jpg

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;
    }

    
  }