Laser tag S06 – Display and Game Timer

Love Jesus, Love Code

Time: 30 minutes

In this section, we will aim to get the display working. The display will eventually be used to show the IP address for the device, the player name, give stats on health, ammo, clips, game time and display who shot you and how much damage they did. In this tutorial, we will program all of these things into the game, even though they are not all working yet.

For this tutorial, we will be using the 0.96 inch OLED IIC display (2 colour). These displays are quite neat and affordable.

Make an existing copy of the code from the last tutorial, saving it as: NodeMCU_LaserTag_S06

To get the display to work, you will need to install two libraries to your Arduino software:

  1. Adafruit SSD1306 library (2.3.1 works)
  2. Adafruit GFX library (1.10.10 works)

To install the libraries, in the Arduino IDE, go to Tools –> Manage Libraries. Then search and install the two libraries above. I have listed the versions that I know have worked in the past. Newer versions sometimes change things.

1. Call the libraries in the code

Add the following Code after variables have been declared the main program tab between the code for the web page and the //LittleFS library

// Set up OLED device
#include <Wire.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_GFX.h>
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
// Declaration for an SSD1306 display connected to I2C (SDA, SCL pins)
#define OLED_RESET     -1 // Reset pin # (or -1 if sharing Arduino reset pin)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

You will notice the code above calls the wire library which is for the IIC communication to the display, and the two libraries we installed previously. The code above also sets the size of the screen. We have 128 x 64 pixels to display. The SCREEN_WIDTH is 128 pixels and the SCREEN_HEIGHT is 64 pixels

2. Initialise the OLED display in the start routine

  1. Add the following subroutine call to the start routine: setupOLED();
void setup() {
  // Serial communication set up to help with debugging.
  Serial.begin(74880);
  Serial.println("Serial Communication is set up");
  
  setupOTA();
  setupFileSystem();
  setupWebActions();
  setupLEDStrip();
  setupOLED();

  debugInfo += "Setup Complete <br>";
}
  1. Add a tab in the Arduino software called Display.ino and add the following code:
void setupOLED(){
  // Start the OLED display
  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3C for 128x32
    Serial.println(F("SSD1306 allocation failed"));
    debugInfo += "SSD1306 allocation failed for OLED display </br>";
  }
  displayUpdate();
  Serial.println("OLED display is now operational");
  debugInfo += "OLED display is now operational <br>";
}

The above code simply checks to see if the display is working. If it is not, it will print a message to the serial monitor. Since we are likely to be doing troubleshooting through Wi-Fi and a webpage, we will add error messages to a variable called “debugInfo” that can be easily called later on.

The subroutine displayUpdate(); will be explained later.

3. displayUpdate() Subroutine

  1. Add the following code to the Display.ino tab underneath the setup routine.
void displayUpdate(){
  display.clearDisplay();
  display.setTextColor(WHITE);
  display.setCursor(0,0);
  display.setTextSize(2);
  display.print(String("Hello World"));
  display.invertDisplay(true);
  //update display
  display.display();
}
  1. Compile and upload program

The display should now say “Hello World”. The text goes over to the 2nd line. Since this is a 2 colour OLED the top two lines of text are yellow and the bottom lines are blue. The text size is 2 which means it takes up 2 text lines. You will notice that I have inverted the colours using the display.invertDisplay(true); command.

Some commands that we have used includes:
display.clearDisplay(); = all pixels are turned off
display.display(); = updates the display
display.setCursor(x,y); = sets the position for where the text should start writing

4. Display game data

Some important data to display on the screen include:

  • Player Name
  • Time left
  • Ammo
  • Health
  • Who just shot you (This will be programmed in at a later time)

We will need to declare some new variables to store these values.

  1. Go to the main program tab and just after the “// Game variables in order of Cloning Data” section, copy the following code:
// Current Data on Player
int shotsLeft = magSize; // Current ammunition
int health = 99; // (default is 99, will be reset on game start)
int gameState = 1; // 1= in play, 2 = paused, 3 = dead, 4 = game over
int timeLeft = gameLength*60;
int clips = magazines; // current magazines
int burstShots = 0; // burst shots added every 100ms up to total burstShots
int shotDelay = 1000.0 / ((250.0 + (roundsMin * 50.0)) / 60.0); 
int deathCount = 0;
int deathCount = 0;
int damageID [100];// stores the total damage caused by a player ID
int killID [100]; // stores the amount of kills by player ID 
int enemyTeamID [100]; // stores the team against the player ID

We will often start the game with a certain amount of health, or with a certain amount of clips. The original values need to be retained for respawning, so this will often involve two different variables, the respawn value and the current value.

Go back to the Display.ino tab and add the following code for the screen. You will also notice that this code also changes direction of text, and draws triangles and squares.

void setupOLED(){
  // Start the OLED display
  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3C for 128x32
    Serial.println(F("SSD1306 allocation failed"));
    debugInfo += "SSD1306 allocation failed for OLED display </br>";
  }
  displayUpdate();
  Serial.println("OLED display is now operational");
  debugInfo += "OLED display is now operational <br>";
}

void displayUpdate(){
  // Clear the buffer.
  display.setRotation(0);
  display.clearDisplay();
  //display data to screen
  display.setTextColor(WHITE);
  display.setCursor(0,0);
  display.setTextSize(2);
  display.print(String(health));
  display.write(3);
  display.print(String(magazines));
  display.write(15);
  display.print(String(shotsLeft));
  display.setCursor(0,16);
  display.setTextSize(1);
  display.println(WiFi.localIP());
  //Code to out zero in for seconds less than ten on display
  display.setTextSize(2);
  display.setCursor(12,48);
  if ((timeLeft % 60) < 10) display.println(""+ String(timeLeft / 60) +":0"+ timeLeft % 60);
  else display.println(""+ String(timeLeft / 60) +":"+ timeLeft % 60);
    
  display.drawTriangle(0, 48, 10, 48, 5, 55, WHITE);
  display.drawTriangle(0, 62, 10, 62, 5, 55, WHITE);
  display.setTextSize(1);
  display.fillRect(114,48,128,56,BLACK);
  display.setCursor(80,48);
  display.println(weaponName(fireSelect));
  display.setCursor(80,56);
  display.println(damageMapped(myGunDamage));
  display.setCursor(16,0);
  display.setRotation(1);
  display.println(playerName(playerID));
  display.display();
}

void displayGameOver(){
  Serial.println("display Game Over screen");
  display.setRotation(0);
  display.clearDisplay();
  display.setCursor(0,0);
  display.setTextSize(1);
  display.setTextSize(1);
  display.println("DAMAGE REPORT:");

   for (int i=0; i<5; i++){
      display.println (playerName(3+i) + " " + String(teamName(1)) + " " + String("50") + "d " + String("3") + " K");
  } 
  display.display();
}

We put two subroutines above, displayUpdate() is used before the game and during the game. displayGameOver() will change what the display does once the game is over. It should eventually show who killed you the most. For more information on how to program the display a good website is: https://randomnerdtutorials.com/guide-for-oled-display-with-arduino/

Compile and upload the binary

The display should look similar to below:

The screen shows that the health is 36, there are 99 shots and 99 clips. There are 12 minutes left in the game. The gun mode is fully automatic with a damage of 5. The player name is Gonzo. The IP address is 192.168.137.136.

Many of the values are simply placeholders that we will fix at a later time. The IP address is correct and will make it easier to find the IP address on your device once it connects to the network.

You will also notice that you can fit quite a lot of data on the screen. We have used big text for the important data.

Add in timer, game state

We want to add in a game timer and game states. The game timer will count down when the game is active. It will not count down when the game is paused or if the game is over.

There will be 4 game states:

  • Game State 1 = normal game state
  • Game State 2 = pause
  • Game State 3 = dead
  • Game State 4 = game over

The gameLength is already declared in the main program where the game variables are listed in order of Cloning Data. It was originally set to 0x0C which is 12 minutes. We want to change it to 2 minutes so we don’t have to wait 12 minutes to get to the end of the game.

  1. Find and change gameLength to 2 minutes
int gameLength = 0x02; // 2 Minutes
  1. We still need to declare Declare variables gameTime, oneSecond, halfSecond, and if the deadLight is on or off. This will be placed under the //current Data on Player section
// timers
unsigned long oneSecond = 1000UL;
unsigned long halfSecond = 500UL;
unsigned long gameTime;
unsigned long deadTime;

// Code Variables
bool deadLight = false;
  1. Create a new tab called Timers.ino and coping the following code into that tab:
void resetClock(){
  gameTime = millis();
  timeLeft = gameLength*60;
}

void gameTimer(){
  if (millis() - gameTime >= oneSecond)
  {
  gameTime += oneSecond;
  //countdown timer for game
  if (timeLeft > 0){
    timeLeft--;
    displayUpdate();
  }
    else {
      gameOver();
    }
  }
}

Explaining the code:
In the resetClock() subroutine, the startTime is set to the current time. The timeLeft is set to the gameLength in seconds. The “ready to engage” sound effect is also played.
The gameTimer() subroutine is to be run in the main loop. Because it can take an unknown time to complete a loop, the program checks to see if one second has passed. If one second has passed, it then increases the start time by one second, and also reduces the time left by one second. The start time will generally stay about 1 second from the current time.

millis() is the current time

  1. Create a new tab called Game.ino In this tab we will control what happens in the different game states and create the gameOver subroutine. Copy the code below in the Game.ino tab:
gameLoop(){
  if (gameState == 1){ //normal game play
    gameTimer();
  }
  // game state 2 is paused
  if (gameState == 3){ //dead - cannot use trigger
    deadCycle(); 
    gameTimer();
  }
  if (gameState == 4){ //game over
    deadCycle();
  }
}

void gameOver(){
  if (gameState != 4){
     // place game over sound here later
  } 
  gameState = 4; // change game state to game over mode
  displayGameOver();
  timeLeft = 0;
  debugInfo += "Game State = 4 (Game Over) <br>";
}

void pauseGame(){
  if (gameState == 1){
    gameState = 2;
    debugInfo += "Game State = 2 (Paused) <br>";
  }
  else if (gameState == 2){
    gameState = 1;
    // place "ready to engage" sound here
    debugInfo += "Game State = 1 (Un-Paused) <br>";
  }
}

void startGame(){
  readPlayerFile();
  shotsLeft = magSize; // Current ammunition
  health = healthMapped(respawnHealth); // Current health
  gameState = 1; // 1= in play, 2 = paused, 3 = dead, 4 = game over
  timeLeft = gameLength*60;
  clips = magazines; // current magazines
  burstShots = 0; // burst shots added every 100ms up to total burstShots
  shotDelay = 1000.0 / ((250.0 + (roundsMin * 50.0)) / 60.0); //  calculate shot delay
  resetClock();
  deathCount = 0;
  mp3.playFileByIndexNumber(8); //ready to engage
  
}

();
  resetClock();
  mp3.playFileByIndexNumber(8); //ready to engage
  
}

void dead() { 
  displayUpdate();
  // Put code in here to play dead sound
  gameState = 3;
  deadTime = millis(); //start the dead time
  Serial.println ("game state = 3");
  debugInfo += "Game State = 3 (Dead) <br>";
}

// the deadCycle flashes the lights on and off
void deadCycle(){
  if (millis() - deadTime >= halfSecond){
    deadTime += halfSecond;
    if (deadLight){
      teamColour(); 
      deadLight = false ;
      } 
      else {
        flashOff();
        deadLight = true;
        }
  }
}

The gameLoop() subroutine determines what game state the program is in, and then what game loops need to be applied.

  1. gameLoop needs to be placed in the main loop of the program as shown below:
void loop() {
   AsyncElegantOTA.loop();
   gameLoop();
}
  1. in the main program tab, at the end of the setup() loop, add the following code:
startGame();

Christian Content

Hebrews 8
3 Every high priest is appointed to offer both gifts and sacrifices, and so it was necessary for this one also to have something to offer.4 If he were on earth, he would not be a priest, for there are already priests who offer the gifts prescribed by the law. 5 They serve at a sanctuary that is a copy and shadow of what is in heaven. This is why Moses was warned when he was about to build the tabernacle: “See to it that you make everything according to the pattern shown you on the mountain.” 6 But in fact the ministry Jesus has received is as superior to theirs as the covenant of which he is mediator is superior to the old one, since the new covenant is established on better promises.

The screen we just created has a number of placeholders to show us what is to come. When programming, it is difficult to get everything working all at once. The technical word for this in programming is a stub. It is a piece of code that is a temporary substitute for yet-to-be-developed code. It allows part of the software solution to be tested without the whole project needing to be completed.

In the Bible, the final solution is not revealed immediately. God puts in place temporary solutions until the final solution is realised. In the Old Testament, God created the old priest system. The priest system works temporarily, but was never intended to be the final solution. Jesus was the final solution for the priest system. Once Jesus came as the final solution, there was no need to keep the old system. The old system was inferior, and did not actually do the job properly to start with, just like a software stub. So when Jesus came, the old priest and sacrificial system became obsolete and needed to be put aside to make way for the new and better system.

This is why the Bible is divided up into two testaments. At one time, God’s people lived under the Old Testament, but once Jesus came, God’s people needed to start living under the New Testament. The New Testament still speaks highly of the Old Testament, so while the sacrificial system of the Old Testament is obsolete, it still teaches us about how God has worked through history, what God is like, and what we are like, and how we can serve God and others. The Old Testament also teaches us about the old sacrificial system, which helps us better understand the New Testament. The New Testament contains a better way, with Jesus being the sacrifice for our sins, and God giving us a new heart that makes us want to serve him.

Leave a Reply