Philips Hue for Games: Beyond the Screen

Video games are framed by the computer screen. The world of the game exists within that rectangle, and the ‘real’ world exists around it. What if we use connected lighting systems to extend the environment of the game beyond the screen?

For this project I have created a simple game of pong in which how well you are doing in the game is reflected by the world around you. Using a Philips Hue Bloom (or any “hue”, “sat”, and/or “bri” capable light), your progress in the game affects the brightness and colour of the room you are playing in.

Very simply, the longer you play, the more pleasantly cool and desaturated the light around you becomes. Whenever you ‘lose’ the light progresses some increment back towards a glaring, saturated red.

Hue Control Setup Function
// Setup function modified from client-control example
function setupHueControl() {
  var addressLabel, usernameLabel, connectButton;

  // set up the address and username fields:
  addressLabel = createSpan("IP address:");
  addressLabel.position(10,10);
  addressField = createInput('text');
  addressField.position(100, 10);
  addressField.value(url);

  usernameField = createInput('text');
  usernameField.position(100, 40);
  usernameField.value(username);
  usernameLabel = createSpan("Username:");
  usernameLabel.position(10,40);

  lightNumberField = createInput('text');
  lightNumberField.position(100, 70);
  lightNumberField.value(4);
  lightNumberLabel = createSpan("Light No.");
  lightNumberLabel.position(10,70);

  targetColourField = createInput('text');
  targetColourField.position(100, 100);
  targetColourField.value(44000);   //A nice winning green
  targetColourLabel = createSpan("Target Hue");
  targetColourLabel.position(10,100);

  connectButton = createButton("connect");
  connectButton.position(100, 130);
  connectButton.mouseClicked(connect);

  resetButton = createButton("reset");
  resetButton.position(180, 130);
  resetButton.mouseClicked(resetLight);
}

At the moment this code works with one light, which the player can select.

Hue Connect
//Connect function directly from client-control
function connect() {

  this.html("refresh");             // change the connect button to 'refresh'
  for (control in controlArray) {
    controlArray[control].remove(); // clear the lights div on reconnect
  }
  controlArray = [];                // clear the control array
  url = "http://" + addressField.value() + '/api/' + usernameField.value() + '/lights/';
  httpDo(url, 'GET', getLights);

}

Once we have all the lights on the hub, we find the one the player has selected:

Get Selected Light
function getLights(result) {
  var lights = JSON.parse(result);    // parse the HTTP response
  let flag = false;
  for (thisLight in lights) {         // iterate over each light in the response
    if(thisLight == lightNumberField.value()){
      // This is the selected light
      setupLightController(lights[thisLight]);
      flag = true
    }
  }

if(flag!=true){
    alert("The light you entered is not available! Try use http://<hub.ip>/debug/clip.html to find a valid light");
  }
  resetLight();
  setupGame();
}

Now that we have the required light, use Tom’s technique to extract the useful properties. In my case I’m interested in whether the light has brightness control, saturation control, and/or hue control.

Setup Light Controller Object
function setupLightController(thisLight){
  var state = thisLight.state;  // state of this light

  for (property in state) {     // iterate over  properties in state object
    switch (property) {         // handle the cases you care about
      case 'on':
      customLightControl.on = state.on;
        break;
      case 'bri':
         customLightControl.bri = state.bri;
        break;
      case 'hue':
        customLightControl.hue = state.hue;
        break;
      case 'sat':
        customLightControl.sat = state.sat;
        break;
      case 'ct':
        customLightControl.ct = state.ct;
        break;

      //   break;
    }

    
  }   // end of for-loop to create controls
  //console.log(customLightControl);
}

My code has some helper functions to increase/decrease property values incrementally. Using a linear interpolation, we can slowly ease towards a target value for brightness, saturation, hue:

Property Value Change Functions
function increaseSaturation(target, amount){  //amount = lerp amount (1 max)
  for (property in customLightControl) {     
    switch (property) {         
      case 'sat':
         customLightControl.sat = lerp(customLightControl.sat, target, amount);
        break;
      }
    }
}
function increaseBrightness(target, amount){  //amount = lerp amount (1 max)
  for (property in customLightControl) {     
    switch (property) {         
      case 'bri':
         customLightControl.bri = lerp(customLightControl.bri, target, amount);
        break;
      }
    }
}
function increaseHue(target, amount){  //amount = lerp amount (1 max)

  for (property in customLightControl) {     
    switch (property) {         
      case 'hue':
         customLightControl.hue = lerp(customLightControl.hue, target, amount);
        break;
      }
    }
}

function fullBrightness(){
  for (property in customLightControl) {     
    switch (property) {        
      case 'bri':
         customLightControl.bri = 254;
        break;
      }
    }
}

function resetLight(){
  // Turns off, desaturates lights
  for (property in customLightControl) {     
    switch (property) {         
      case 'bri':
         customLightControl.bri = 150;
        break;
      case 'hue':
        customLightControl.hue = 0;
        break;
      case 'sat':
        customLightControl.sat = 0;
        break;
    }
  }
  updateLight(); //Always update light when resetting.
}

//https://codepen.io/ma77os/pen/KGIEh
function lerp (start, end, amt){
  return (1-amt)*start+amt*end
}

Finally on the Hue Control side, send the request to change the Hue light:

Update Lights and Send Request
function updateLight(){
  console.log(customLightControl);

  var payload = {};
  // put the value for the given control into the payload:
  for (property in customLightControl) {     
    switch (property) {         
      case 'bri':
         payload['bri'] = Math.floor(customLightControl.bri); 
        break;
      case 'hue':
        payload['hue'] =  Math.floor(customLightControl.hue); 
        break;
      case 'sat':
        payload['sat'] =  Math.floor(customLightControl.sat); 
        break;
      case 'on':
        payload['on'] = customLightControl.on;
        break;
    }
  }

  setLight(lightNumberField.value(), payload, 'state');
}

function setLight(lightNumber, data, command) {
  var path = url + lightNumber + '/' + command;    // assemble the full URL
  var content = JSON.stringify(data);         // convert JSON obj to string
  httpDo( path, 'PUT', content, 'text', getResponse); //HTTP PUT the change
}

function getResponse(response) {
  // show response:
  console.log("res:");
  console.log(response);
}

I won’t go into detail on the pong side of this project, since it’s not too relevant to the subject. The important part of the pong code is where the lighting control functions are called.

There are two cases where a lighting control function is called. The first is when the ball hits the side or top of the screen. This marks successful progress in the game:

Handle Progress
function onEdgeHit(){
  ball.vel.mult(ball.speedMultiplier);  // Increase difficulty over time
  increaseSaturation(minSat,0.1);  // Decrease satureation over time
  increaseBrightness(255,0.05);  // Increase brightness over time
  increaseHue(targetColourField.value(),0.1);  //Progress towards users chosen colour temperature 
  updateLight();
}

The other case is when the ball reaches the bottom. This is a negative progression:

Handle Setback
function onLose(){
  increaseSaturation(maxSat,0.5);  // Greatly increase saturation
  increaseBrightness(minBri,0.5);  // Greatly decrease brightness
  increaseHue(0,0.5); //Greatly increment saturation towards red
  updateLight();
}

Leave a Reply

Your email address will not be published. Required fields are marked *