SparkFun Inventor's Kit for Photon Experiment Guide
Experiment 10: Pong!
Introduction
In this experiment, we'll use the OLED Breakout, potentiometer, and a photo resistor to make a game of Pong, where the paddles are controlled by the analog values of the two sensors.
Parts Needed
You will need the following parts (besides your Photon RedBoard and Breadboard, of course):
- 1x OLED Breakout Board
- 1x Potentiometer
- 1x Photoresistor
- 1x 330 Ohm Resistor
- 15x Jumper Wires
Suggested Reading
- OLED Breakout Hookup Guide - if you're stuck hooking up the OLED breakout, check this lovely guide for more info
- What is an Arduino? -- We'll use an Arduino to send commands and display data to the OLED.
- Serial Peripheral Interface (SPI) -- SPI is the preferred method of communication with the display.
- I2C -- Alternatively, I2C can be used to control the display. It uses less wires, but is quite a bit slower.
- How to Use a Breadboard -- The breadboard ties the Arduino to the OLED breakout.
Hardware Hookup
We'll be connecting three devices to our Photon RedBoard - the OLED breakout, a trim potentiometer, and a photocell. These last two are analog sensors that are going to act as the paddle controllers for each player, while the OLED screen displays the ball, the paddles, and the score.
The photocell is hooked up to pin A0 and GND though a 330 (NOT 10K) Ohm resistor from one pin, while the other pin is connected to +3.3V. The potentiometer's middle pin goes to pin A1, while the side pins are connected to GND and 3.3V.
Here's a handy table for the OLED Breakout connections:
Photon RedBoard Pin | OLED Breakout Pin | |
---|---|---|
A2 | CS | |
A3 | SCK | |
A5 | SDI | |
D5 | D/C | |
D6 | RST | |
3V3 | 3.3V | |
GND | GND |
The table and Fritzing image are much more helpful, but if you wanted to see inside the box, here's a shot of the breadboarded circuit:
Photon Code
Pong is a great programming exercise for getting used to a new language or environment. There are tons of great pong tutorials out there, so we're not going to focus too much on the general code beyond the parts that interact with the hardware.
The first thing we need to do after creating a new app is to add the SparkFun OLED library to our sketch. This let's us draw and write all manner of things to the OLED screen quite easily. First, click on the bookmark icon in the sidebar (it's the fourth one up from the bottom). Under 'Community Libraries', there should be a search box - type in OLED, and you should see the 'SparkFunMicroOLED' Library pop up, like so:
Click on the library name, and then on the big blue button that says "Include in App". That should cause a line like #include "SparkFunMicroOLED/SparkFunMicroOLED.h"
to appear near the top of your sketch. Voila!
Now that you've imported the library, you can copy and paste the rest of the code beneath:
language:c
/* SparkFun Inventor's Kit for Photon
Experiment 10 - Part 1
This sketch was written by SparkFun Electronics
Ben Leduc-Mills
August 31, 2015
https://github.com/sparkfun
This is an example sketch for an analog sensor Pong game.
Development environment specifics:
Particle Build environment (https://www.particle.io/build)
Particle Photon RedBoard
Released under the MIT License(http://opensource.org/licenses/MIT)
*/
//////////////////////////
// MicroOLED Definition //
//////////////////////////
#define PIN_RESET D6 // Connect RST to pin 6
#define PIN_DC D5 // Connect DC to pin 5 (required for SPI)
#define PIN_CS A2 // Connect CS to pin A2 (required for SPI)
//MicroOLED oled(MODE_SPI, PIN_RESET, PIN_DC, PIN_CS);
MicroOLED oled(MODE_SPI, PIN_RESET, PIN_DC, PIN_CS);
//#define SINGLE_PLAYER
const int player1Pin = A1;
#ifndef SINGLE_PLAYER
const int player2Pin = A0;
#endif
/*** Sensor Calibration ****/
int sensor1Calibration = LCDHEIGHT; //photocell w/330
int sensor2Calibration = LCDHEIGHT; //potentiometer
//potentiometer
int sensor1Min = 0;
int sensor1Max = 4096;
//photocell
int sensor2Min = 100;
int sensor2Max = 1000;
/*** Game Settings ***/
const int renderDelay = 16;
const int startDelay = 2000;
const int gameOverDelay = 3000;
const int scoreToWin = 10;
int player1Score = 0;
int player2Score = 0;
const float paddleWidth = LCDWIDTH / 16.0;
const float paddleHeight = LCDHEIGHT / 3.0;
const float halfPaddleWidth = paddleWidth / 2.0;
const float halfPaddleHeight = paddleHeight / 2.0;
float player1PosX = 1.0 + halfPaddleWidth;
float player1PosY = 0.0;
float player2PosX = LCDWIDTH - 1.0 - halfPaddleWidth;
float player2PosY = 0.0;
// This is only used in SINGLE_PLAYER mode:
#ifdef SINGLE_PLAYER
float enemyVelY = 0.5;
#endif
/*** Ball Physics ***/
const float ballRadius = 2.0;
const float ballSpeedX = 1.0;
float ballPosX = LCDWIDTH / 2.0;
float ballPosY = LCDHEIGHT / 2.0;
float ballVelX = -1.0 * ballSpeedX;
float ballVelY = 0;
void setup()
{
// in our setup, we call a few custom functions to get everything ready
initializeGraphics();
initializeInput();
displayGameStart();
Serial.begin(9600);
}
void loop()
{
updateGame(); //custom function to advance the game logic
renderGame(); //custom function to dislay the current game state on the OLED screen
// print out sensor values via serial so we can calibrate
Serial.print(analogRead(A0));
Serial.print(" <-A0 | A1-> ");
Serial.println(analogRead(A1));
//winning conditions
if (player1Score >= scoreToWin)
{
gameOver(true);
}
else if (player2Score >= scoreToWin)
{
gameOver(false);
}
}
//start the OLED and set the font type we want
void initializeGraphics()
{
oled.begin();
oled.setFontType(1);
}
//get either 1 or 2 pins ready for analog input
void initializeInput()
{
pinMode(player1Pin, INPUT);
#ifndef SINGLE_PLAYER
pinMode(player2Pin, INPUT);
#endif
}
//display the start screen
void displayGameStart()
{
oled.clear(PAGE);
renderString(20, 10, "Get");
renderString(10, 30, "Ready!");
oled.display();
delay(startDelay);
}
//update the positions of the player paddles and the ball
void updateGame()
{
updatePlayer1();
updatePlayer2();
updateBall();
}
//reset the score, ball position, and paddle positions for a new game
void resetGame()
{
player2Score = 0;
player1Score = 0;
player2PosY = 0.0;
ballPosX = LCDWIDTH / 2.0;
ballPosY = LCDHEIGHT / 2.0;
ballVelX = -1.0 * ballSpeedX;
ballVelY = 0.0;
}
float clampPaddlePosY(float paddlePosY)
{
float newPaddlePosY = paddlePosY;
if (paddlePosY - halfPaddleHeight < 0)
{
newPaddlePosY = halfPaddleHeight;
}
else if (paddlePosY + halfPaddleHeight > LCDHEIGHT)
{
newPaddlePosY = LCDHEIGHT - halfPaddleHeight;
}
return newPaddlePosY;
}
void updatePlayer1()
{
int potVal = analogRead(player1Pin);
player1PosY = map(potVal, sensor1Min, sensor1Max, 0, sensor1Calibration);
}
void updatePlayer2()
{
// If it's a single player game, update the AI's position
#ifdef SINGLE_PLAYER
// Follow the ball at a set speed
if (player2PosY < ballPosY)
{
player2PosY += enemyVelY;
}
else if (player2PosY > ballPosY)
{
player2PosY -= enemyVelY;
}
player2PosY = clampPaddlePosY(player2PosY);
#else // Else if this is multiplayer, get player 2's position
int lightVal = analogRead(player2Pin);
player2PosY = map(lightVal, sensor2Min, sensor2Max, 0, sensor2Calibration);
//Serial.println(player2PosY);
#endif
}
void updateBall()
{
ballPosY += ballVelY;
ballPosX += ballVelX;
// Top and bottom wall collisions
if (ballPosY < ballRadius)
{
ballPosY = ballRadius;
ballVelY *= -1.0;
}
else if (ballPosY > LCDHEIGHT - ballRadius)
{
ballPosY = LCDHEIGHT - ballRadius;
ballVelY *= -1.0;
}
// Left and right wall collisions
if (ballPosX < ballRadius)
{
ballPosX = ballRadius;
ballVelX = ballSpeedX;
player2Score++;
}
else if (ballPosX > LCDWIDTH - ballRadius)
{
ballPosX = LCDWIDTH - ballRadius;
ballVelX *= -1.0 * ballSpeedX;
player1Score++;
}
// Paddle collisions
if (ballPosX < player1PosX + ballRadius + halfPaddleWidth)
{
if (ballPosY > player1PosY - halfPaddleHeight - ballRadius &&
ballPosY < player1PosY + halfPaddleHeight + ballRadius)
{
ballVelX = ballSpeedX;
ballVelY = 2.0 * (ballPosY - player1PosY) / halfPaddleHeight;
}
}
else if (ballPosX > player2PosX - ballRadius - halfPaddleWidth)
{
if (ballPosY > player2PosY - halfPaddleHeight - ballRadius &&
ballPosY < player2PosY + halfPaddleHeight + ballRadius)
{
ballVelX = -1.0 * ballSpeedX;
ballVelY = 2.0 * (ballPosY - player2PosY) / halfPaddleHeight;
}
}
}
void renderGame()
{
oled.clear(PAGE);
renderScores(player1Score, player2Score);
renderPaddle(player1PosX, player1PosY);
renderPaddle(player2PosX, player2PosY);
renderBall(ballPosX, ballPosY);
oled.display();
delay(renderDelay);
}
void renderString(int x, int y, String string)
{
oled.setCursor(x, y);
oled.print(string);
}
void renderPaddle(int x, int y)
{
oled.rect(
x - halfPaddleWidth,
y - halfPaddleHeight,
paddleWidth,
paddleHeight);
}
void renderBall(int x, int y)
{
oled.circle(x, y, 2);
}
void renderScores(int firstScore, int secondScore)
{
renderString(10, 0, String(firstScore));
renderString(LCDWIDTH - 14, 0, String(secondScore));
}
void gameOver(bool didWin)
{
if (didWin)
{
#ifdef SINGLE_PLAYER
renderString(20, 10, "You");
renderString(20, 30, "Win!");
#else
renderString(0, 10, "Playr 1");
renderString(15, 30, "Wins");
#endif
}
else
{
#ifdef SINGLE_PLAYER
renderString(20, 10, "You");
renderString(15, 30, "Lose!");
#else
renderString(0, 10, "Playr 2");
renderString(15, 30, "Wins");
#endif
}
oled.display();
delay(gameOverDelay);
// Get ready to start the game again.
resetGame();
displayGameStart();
}
What You Should See
After successfully flashing the code onto your Photon RedBoard, you should see the words 'Get Ready!' on the OLED screen, followed soon after by the familiar Pong game screen. If you've calibrated your sensors well, you should be able to move each paddle up and down to defend your side. After a player wins, the game should start all over again.
If you turn the potentiometer or place your hand over the photocell, the paddles should move up and down, allowing you to play a game of Pong:
Code to Note
Whew! That's a lot of code to digest all at once. Let's look at a few of the key sections that make this work.
Defining our pin assignments and declaring our OLED object happens right at the top:
language:c
#define PIN_RESET D6 // Connect RST to pin 6
#define PIN_DC D5 // Connect DC to pin 5 (required for SPI)
#define PIN_CS A2 // Connect CS to pin A2 (required for SPI)
MicroOLED oled(MODE_SPI, PIN_RESET, PIN_DC, PIN_CS);
//#define SINGLE_PLAYER
const int player1Pin = A1; //connected to potentiometer
#ifndef SINGLE_PLAYER
const int player2Pin = A0; //connected to photocell
#endif
This is also where we decide a few important things: which communication mode we're using - SPI in our case means we put MODE_SPI
as that first variable in the MicroOLED object. Below that, we're also deciding if we want a 1 player or 2 player game. To make the game one player, simply uncomment the //#define SINGLE_PLAYER
line.
The next section is crucial - it's where we 'calibrate' our sensors, by telling the program what we expect the minimum and maximum values for each sensor to be. Changes are you'll have to run the program first and look at the serial monitor while testing your sensors to get a good 'sense' of the value ranges.
language:c
/*** Sensor Calibration ****/
int sensor1Calibration = LCDHEIGHT; //photocell w/330
int sensor2Calibration = LCDHEIGHT; //potentiometer
//potentiometer
int sensor1Min = 0;
int sensor1Max = 4096;
//photocell
int sensor2Min = 100;
int sensor2Max = 1000;
For example, if you're in a very brightly lit room, your photocell will get a wider range of values than if you're in a dimly lit place -- but we still want the paddle to move the entire range of the screen, which is why we save the LCDHEIGHT
value to a separate variable to use later on.
After we set a number of global variables that control things like how fast the ball moves and what number each game goes until (the winning number), we're on to our setup()
and loop()
functions.
language:c
void setup()
{
// in our setup, we call a few custom functions to get everything ready
initializeGraphics();
initializeInput();
displayGameStart();
Serial.begin(9600);
}
void loop()
{
updateGame(); //custom function to advance the game logic
renderGame(); //custom function to dislay the current game state on the OLED screen
// print out sensor values via serial so we can calibrate
Serial.print(analogRead(A0));
Serial.print(" <-A0 | A1-> ");
Serial.println(analogRead(A1));
//winning conditions
if (player1Score >= scoreToWin)
{
gameOver(true);
}
else if (player2Score >= scoreToWin)
{
gameOver(false);
}
}
You'll notice that we make use of a lot of custom functions in order to keep our main setup and loop short and readable. In the setup()
, we call a few functions that help start up the OLED screen, initialize our input pins, display the start screen, and open up serial communication. The loop()
is simply updating the game state, displaying that state, checking to see if anyone won, and ending the game if so. Dig into all of these functions to get a better sense of how the game works!
Troubleshooting
The main challenge (besides being good at Pong) is making sure your sensor inputs are calibrated to the point where each paddle covers the entire height of the OLED screen. Checking the serial monitor while testing your sensors is the best way to get an idea of what the range for each sensor is.
Part 2: Scoreboard!
What does every serious Pong competition need? A scoreboard, that's what! In this half of the exercise, we're going to use Particle.variable() with the Particle Cloud API to create a web page that stores our Pong scores locally (i.e., without a database) in HTML 5 localData, displays them in a table, and updates when a new score in ready.
Photon Code
Here's the new code that goes on the Photon RedBoard - in reality, there are just a few changes. We'll go over them in the next section, but this is the complete sketch on the Photon side.
language:c
/* SparkFun Inventor's Kit for Photon
Experiment 10 - Part 2
This sketch was written by SparkFun Electronics
Ben Leduc-Mills
August 31, 2015
https://github.com/sparkfun
This is an example sketch for an analog sensor Pong game, with
an html/javascript scoreboard.
Development environment specifics:
Particle Build environment (https://www.particle.io/build)
Particle Photon RedBoard
Released under the MIT License(http://opensource.org/licenses/MIT)
*/
//////////////////////////
// MicroOLED Definition //
//////////////////////////
#define PIN_RESET D6 // Connect RST to pin 6
#define PIN_DC D5 // Connect DC to pin 5 (required for SPI)
#define PIN_CS A2 // Connect CS to pin A2 (required for SPI)
//MicroOLED oled(MODE_SPI, PIN_RESET, PIN_DC, PIN_CS);
MicroOLED oled(MODE_SPI, PIN_RESET, PIN_DC, PIN_CS);
//#define SINGLE_PLAYER
const int player1Pin = A1;
#ifndef SINGLE_PLAYER
const int player2Pin = A0;
#endif
/*** Sensor Calibration ****/
int sensor1Calibration = LCDHEIGHT; //photocell w/330
int sensor2Calibration = LCDHEIGHT; //potentiometer
//potentiometer
int sensor1Min = 0;
int sensor1Max = 4096;
//photocell
int sensor2Min = 100;
int sensor2Max = 1000;
/*** Game Settings ***/
const int renderDelay = 16;
const int startDelay = 2000;
const int gameOverDelay = 3000;
const int scoreToWin = 10;
int player1Score = 0;
int player2Score = 0;
const float paddleWidth = LCDWIDTH / 16.0;
const float paddleHeight = LCDHEIGHT / 3.0;
const float halfPaddleWidth = paddleWidth / 2.0;
const float halfPaddleHeight = paddleHeight / 2.0;
float player1PosX = 1.0 + halfPaddleWidth;
float player1PosY = 0.0;
float player2PosX = LCDWIDTH - 1.0 - halfPaddleWidth;
float player2PosY = 0.0;
// This is only used in SINGLE_PLAYER mode:
#ifdef SINGLE_PLAYER
float enemyVelY = 0.5;
#endif
/*** Ball Physics ***/
const float ballRadius = 2.0;
const float ballSpeedX = 1.0;
float ballPosX = LCDWIDTH / 2.0;
float ballPosY = LCDHEIGHT / 2.0;
float ballVelX = -1.0 * ballSpeedX;
float ballVelY = 0;
//score keeper
char postScore[64];
//game ID
int gameID = 0;
void setup()
{
// in our setup, we call a few custom functions to get everything ready
//here's our call to Particle.variable - we're just sending a string called 'data' out to the cloud
Particle.variable("data", &postScore, STRING);
initializeGraphics();
initializeInput();
displayGameStart();
Serial.begin(9600);
}
void loop()
{
updateGame(); //custom function to advance the game logic
renderGame(); //custom function to dislay the current game state on the OLED screen
//winning conditions
if (player1Score >= scoreToWin)
{
gameOver(true);
}
else if (player2Score >= scoreToWin)
{
gameOver(false);
}
}
//start the OLED and set the font type we want
void initializeGraphics()
{
oled.begin();
oled.setFontType(1);
}
//get either 1 or 2 pins ready for analog input
void initializeInput()
{
pinMode(player1Pin, INPUT);
#ifndef SINGLE_PLAYER
pinMode(player2Pin, INPUT);
#endif
}
//display the start screen
void displayGameStart()
{
oled.clear(PAGE);
renderString(20, 10, "Get");
renderString(10, 30, "Ready!");
oled.display();
delay(startDelay);
}
//update the positions of the player paddles and the ball
void updateGame()
{
updatePlayer1();
updatePlayer2();
updateBall();
}
//reset the score, ball position, and paddle positions for a new game
void resetGame()
{
player2Score = 0;
player1Score = 0;
player2PosY = 0.0;
ballPosX = LCDWIDTH / 2.0;
ballPosY = LCDHEIGHT / 2.0;
ballVelX = -1.0 * ballSpeedX;
ballVelY = 0.0;
}
float clampPaddlePosY(float paddlePosY)
{
float newPaddlePosY = paddlePosY;
if (paddlePosY - halfPaddleHeight < 0)
{
newPaddlePosY = halfPaddleHeight;
}
else if (paddlePosY + halfPaddleHeight > LCDHEIGHT)
{
newPaddlePosY = LCDHEIGHT - halfPaddleHeight;
}
return newPaddlePosY;
}
void updatePlayer1()
{
int potVal = analogRead(player1Pin);
player1PosY = map(potVal, sensor1Min, sensor1Max, 0, sensor1Calibration);
}
void updatePlayer2()
{
// If it's a single player game, update the AI's position
#ifdef SINGLE_PLAYER
// Follow the ball at a set speed
if (player2PosY < ballPosY)
{
player2PosY += enemyVelY;
}
else if (player2PosY > ballPosY)
{
player2PosY -= enemyVelY;
}
player2PosY = clampPaddlePosY(player2PosY);
#else // Else if this is multiplayer, get player 2's position
int lightVal = analogRead(player2Pin);
player2PosY = map(lightVal, sensor2Min, sensor2Max, 0, sensor2Calibration);
//Serial.println(player2PosY);
#endif
}
void updateBall()
{
ballPosY += ballVelY;
ballPosX += ballVelX;
// Top and bottom wall collisions
if (ballPosY < ballRadius)
{
ballPosY = ballRadius;
ballVelY *= -1.0;
}
else if (ballPosY > LCDHEIGHT - ballRadius)
{
ballPosY = LCDHEIGHT - ballRadius;
ballVelY *= -1.0;
}
// Left and right wall collisions
if (ballPosX < ballRadius)
{
ballPosX = ballRadius;
ballVelX = ballSpeedX;
player2Score++;
}
else if (ballPosX > LCDWIDTH - ballRadius)
{
ballPosX = LCDWIDTH - ballRadius;
ballVelX *= -1.0 * ballSpeedX;
player1Score++;
}
// Paddle collisions
if (ballPosX < player1PosX + ballRadius + halfPaddleWidth)
{
if (ballPosY > player1PosY - halfPaddleHeight - ballRadius &&
ballPosY < player1PosY + halfPaddleHeight + ballRadius)
{
ballVelX = ballSpeedX;
ballVelY = 2.0 * (ballPosY - player1PosY) / halfPaddleHeight;
}
}
else if (ballPosX > player2PosX - ballRadius - halfPaddleWidth)
{
if (ballPosY > player2PosY - halfPaddleHeight - ballRadius &&
ballPosY < player2PosY + halfPaddleHeight + ballRadius)
{
ballVelX = -1.0 * ballSpeedX;
ballVelY = 2.0 * (ballPosY - player2PosY) / halfPaddleHeight;
}
}
}
void renderGame()
{
oled.clear(PAGE);
renderScores(player1Score, player2Score);
renderPaddle(player1PosX, player1PosY);
renderPaddle(player2PosX, player2PosY);
renderBall(ballPosX, ballPosY);
oled.display();
delay(renderDelay);
}
void renderString(int x, int y, String string)
{
oled.setCursor(x, y);
oled.print(string);
}
void renderPaddle(int x, int y)
{
oled.rect(
x - halfPaddleWidth,
y - halfPaddleHeight,
paddleWidth,
paddleHeight);
}
void renderBall(int x, int y)
{
oled.circle(x, y, 2);
}
void renderScores(int firstScore, int secondScore)
{
renderString(10, 0, String(firstScore));
renderString(LCDWIDTH - 14, 0, String(secondScore));
}
// OK - here's the new bit
void gameOver(bool didWin)
{
//at the end of a game, increment the gameID by 1
gameID += 1;
//now, send out the player 1 score, player 2 score, and the game ID
//we do this by using 'sprintf', which takes a bunch of random data types (in our case, integers),
//and puts them all into a nicely formatted string - which is exactly what our Particle.variable is supposed to be
sprintf(postScore, "{\"player1Score\":%d, \"player2Score\":%d, \"gameID\":%d}", player1Score, player2Score, gameID);
//give us a sec, and start a new game
delay(1000);
if (didWin)
{
#ifdef SINGLE_PLAYER
renderString(20, 10, "You");
renderString(20, 30, "Win!");
#else
renderString(0, 10, "Playr 1");
renderString(15, 30, "Wins");
#endif
}
else
{
#ifdef SINGLE_PLAYER
renderString(20, 10, "You");
renderString(15, 30, "Lose!");
#else
renderString(0, 10, "Playr 2");
renderString(15, 30, "Wins");
#endif
}
oled.display();
delay(gameOverDelay);
// Get ready to start the game again.
resetGame();
displayGameStart();
}
HTML Code
This is where the real action is happening for this exercise - on the web. We'll be using a combination of good ol' HTML, the HTML 5 localStorage ability, and some basic javaScript to create a page that reads the values of our Particle.variable, stores them (without using a database), displays them in a table, and checks every 15 seconds for a new score to come in.
Create a new html file in your favorite text editor, save it as 'experiment10.html' and paste in the code below (doesn't matter where you save it, just remember where it is):
language:javascript
<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8 />
<META HTTP-EQUIV="refresh" CONTENT="15"><!-- refresh our page every 15 seconds -->
<!--
SparkFun Inventor's Kit for Photon
Experiment 10 - Part 2
This sketch was written by SparkFun Electronics
Ben Leduc-Mills
August 31, 2015
https://github.com/sparkfun
This is an example sketch for an analog sensor Pong game, with
an html/javascript scoreboard.
Development environment specifics:
Particle Build environment (https://www.particle.io/build)
Particle Photon RedBoard
Released under the MIT License(http://opensource.org/licenses/MIT)
-->
<title>SparkFun Photon SIK Experiment 10 - Part 2</title>
</head>
<body>
<div style="height:300px;overflow:auto;"><!-- make a div set to overflow so our table scrolls -->
<table id="scorekeeper"> <!-- table id - important soon -->
</table>
</div>
<FORM> <!-- form button to clear data if we want -->
<INPUT TYPE="button" onClick="handlers.clearAppData()" VALUE="Clear Scores">
</FORM>
<!-- javascript starts here -->
<script type="text/javascript">
//object to contain our data
var app = {
scores: [{
}]
};
//to keep track of game number - don't want to record the same game more than once
var gameNum =0;
//set of functions to handle getting and setting the local storage data from our api call
//thanks to stackoverflow user bundleofjoy(http://stackoverflow.com/users/1217785/bundleofjoy) for the inspiration
var handlers = {
//make an api call and save new data from our Particle sketch into our local storage object
saveData: function () {
var xmlhttp = new XMLHttpRequest();
// IMPORTANT: replace this with your device name, Particle.variable name, and your access token!
var url = "https://api.particle.io/v1/devices/{your device name}/{your Particle.variable name}?access_token={your access_token}";
xmlhttp.open("GET", url, true);
xmlhttp.send();
xmlhttp.onreadystatechange = function(){
if(xmlhttp.readyState == 4 && xmlhttp.status == 200) {
var data = JSON.parse(xmlhttp.responseText);
var result = decodeURI(data.result);
//parse our json object
var p = JSON.parse(result);
//check if the latest game number from Particle is greater than what we last stored locally
//if so, add a new score to our scores array
if(p.gameID > gameNum){
console.log("saved new");
app.scores.push({player1Score: p.player1Score, player2Score: p.player2Score, gameID: p.gameID});
localStorage.setItem("app", JSON.stringify(app));
gameNum = p.gameID;
}
}
}
},
//get the data currently stored in our HTML 5 localStorage object
getData: function () {
// Retrieves the object app from localStorage
var retrievedObject = localStorage.getItem("app");
var savedData = JSON.parse(retrievedObject);
//if there's something in saved data, let's grab it!
if(savedData !== null){
//and put the data into something we can see on the page (our table)
handlers.displayTable();
//let's also update our local gameID variable with the latest gameID
for(scores in savedData) {
var current = savedData.scores.length-1;
gameNum = savedData.scores[current].gameID;
}
}
return savedData;
},
//deletes all our local data
clearAppData: function (event) {
localStorage.clear();
return false;
},
//displays our data in an html table
displayTable: function() {
var retrievedObject = localStorage.getItem("app");
var savedData = JSON.parse(retrievedObject);
var out = "<th>Game Number</th><th>Player 1 Score</th><th>Player 2 Score</th>";
//iterate through all the scores in our saved data and put them into one long string
for(var i = 1; i < savedData.scores.length-1; i++) {
out += "<tr>" + "<td>";
out += savedData.scores[i].gameID+ "</td>";
out += "<td>" + savedData.scores[i].player1Score+ "</td>";
out += "<td>" + savedData.scores[i].player2Score + "</td>";
out += "</tr>"
}
console.log(out);
//put that long string into our table using out table's id property
document.getElementById('scorekeeper').innerHTML = out;
}
};
//when the window loads (like on refresh), this gets called
window.onload = function () {
//get data from localStorage
var saved = handlers.getData();
//check if its null / undefined
if ([null, undefined].indexOf(saved) === -1) {
//it's not null/undefined - data exists!
console.log("Data exists. Here's the data : ", saved);
//set your "app" to data from localStorage
app = saved;
handlers.saveData();
} else {
//localStorage is empty
console.log("Data does not exist, save to localStorage");
//so, save incoming data in localStorage
handlers.saveData();
}
};
</script>
</body>
</html>
What You Should See
Plug in your Photon RedBoard, and make sure it's running Pong and posting a JSON object to the URL you specified. Now, if you open the HTML file in your browser of choice (the URL might be something like 'file://localhost/Users/User1/Desktop/Experiment10.html' on a mac), you should see something like this:
If the page is blank, give it a minute or two - the updates are sometimes a little out of sync with what the Pong game is currently doing (and remember, it only posts new data after a game has been won or lost).
The page will refresh every 15 seconds, check for a new score, add it if there is one, and show it to you. You can push the 'clear scores' button to empty the scores table, though to reset the gameID you'll have to restart your Photon.
Sweet! Now, as long as your Photon is online you can point your friends to a live scoreboard of your match scores.
Code to Note: Photon Part 2
Some important bits have changed, so pay attention.
//score keeper
char postScore[64];
//game ID
int gameID = 0;
We add to variable at the top of our code to contain postScore[]
a character array that we will register as our Particle.variable(), and gameID
, which we will use as a unique identifier for each game played.
In setup()
, we register our postScore[]
variable so we can access from the cloud:
Particle.variable("data", &postScore, STRING);
That first argument ("data" in our case) is what we will use in the Particle api to retrieve our info - but it could be called anything.
Now for the fun part - formatting our data to a string to be accessed by the Particle cloud API.
// OK - here's the new bit
void gameOver(bool didWin)
{
//at the end of a game, increment the gameID by 1
gameID += 1;
//now, send out the player 1 score, player 2 score, and the game ID
//we do this by using 'sprintf', which takes a bunch of random data types (in our case, integers),
//and puts them all into a nicely formatted string - which is exactly what our Particle.variable is supposed to be
sprintf(postScore, "{\"player1Score\":%d, \"player2Score\":%d, \"gameID\":%d}", player1Score, player2Score, gameID);
Read the comments for a better idea of what's going on, or if you're feeling very adventurous, check out the c++ reference page for sprintf
.
Code to Note: HTML
Ok, so it looks like a mess of gibberish right now - that's fine. The most important thing is that you find the URL variable and change it to match your credentials. It looks like this (should be around line 39):
var url = "https://api.particle.io/v1/devices/{your device name}/{your Particle.variable name}?access_token={your access_token}";
The parts in curly brackets {} are the parts you need to replace. If even one thing is off, none of this will work. Computers are pretty finicky that way, trust me. A great way to check is to paste your URL in a browser (make sure your Photon code is uploaded and your Photon RedBoard is plugged in first) - you should see that URL return a JSON object (it's a nice way of organizing data for the web). It might look something like this (I took bits out for privacy):
{
"cmd": "VarReturn",
"name": "data",
"result": "{\"player1Score\":10, \"player2Score\":2, \"gameID\":1}",
"coreInfo": {
"last_app": "",
"last_heard": "2015-08-26T21:35:28.021Z",
"connected": true
}
If you see something like, you're all set - if not, check your credentials and try again.
Troubleshooting
Granted, this is a rather complicated experiment (I'm not even sure that anyone had done this with a Photon yet when we wrote this). Being comfortable with HTML and javaScript is obviously a big help here. Here are a few links that might help you out (they helped me out, anyway) while looking through the code and comments:
- Restoring an array from HTML5 localStorage
- Iterating over a JSON object
- Parsing JSON for an HTML table
- Using InnerHTML and javaScript
Going Further
There is SO much potential to go further with this project. Display the data with the last game played at the top of the table. Add some CSS and make it look pretty. Keep track of player 1 wins vs. player 2 wins over time. Calculate the average margin of victory. Visualize the scores in a graph over time. So. Many. Possibilities. Please share with us if you make some improvements!