
Time: Approximately 60 minutes
We have successfully implemented the display functionality; however, we will now enhance it by adding additional game information, such as health, ammo, player name, and the remaining time in the game.
Timers
The game utilises several different timers, implemented using non-blocking code. While a countdown timer could be created using the delay(1000);
function, this would temporarily pause the program for one second, preventing any other processes from running. Instead, we calculate the elapsed time by measuring the difference in system time.
The code below illustrates how a non-blocking timer operates.
The function millis();
returns the number of milliseconds since the program started, and this value continues to increase at a rate of 1000 milliseconds per second.
unsigned long previousMillis = 0;
const long interval = 1000; // 1 seccond delay
void loop() {
unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= interval) {
previousMillis = currentMillis;
Serial.println("1 second has passed");
}
}
New Feature cycle

Save your project as “LaserTag_C09_Timer”
Variables, Setup() and Loop()
Add the following variables and function calls to the main tab

New Tab – Timers
Create a new tab called “Timers”, and add the following code

New Tab – Game
We want to implement a game timer and define various game states. The game timer will count down while the game is active, but it will pause when the game is either paused or has ended.
There will be four game states:
- Game State 1: Normal game state
- Game State 2: Paused
- Game State 3: Dead
- Game State 4: Game Over
The game length 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 represents 12 minutes. We want to change this to 2 minutes, so players do not have to wait 12 minutes to reach the end of the game.
Create a new tab called “Game” and add the following code.
Some code is missing in the following section. Please read the comments to identify where it should be placed.

void 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){
mp3.playFileByIndexNumber(14); // play game over sound
}
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;
mp3.playFileByIndexNumber(8);
debugInfo += "Game State = 1 (Un-Paused) <br>";
}
}
void startGame(){
readPlayerFile();
teamColour();
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
}
void dead() {
displayUpdate();
mp3.playFileByIndexNumber(7); // 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;
}
}
}
void respawn(){
health = respawnHealth;
shotsLeft = magSize;
clips = magazines;
mp3.playFileByIndexNumber(8); //ready to engage
gameState = 1;
canFire = true;
}
Explaining the Code:
In the resetClock() subroutine, the startTime
is set to the current time, and timeLeft
is initialised to the game length in seconds. Additionally, the “ready to engage” sound effect is played.
The gameTimer() subroutine runs within the main loop. As the duration of each loop may vary, the program checks whether one second has elapsed. If it has, the startTime
is incremented by one second, and timeLeft
is decreased by the same amount. Consequently, the startTime
typically remains about one second behind the current time.
millis() represents the current time.
The gameLoop()subroutine determines the current game state and identifies the appropriate game loops that need to be executed.
Modify Code – OLED Tab
The entire code in the tab needs to be replaced to accommodate all the new information that will be displayed on the screen. Please copy and paste the following code:

void setupOLED(){ // Start the OLED display
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println(F("SSD1306 allocation failed"));
debugInfo += "SSD1306 allocation failed for OLED display </br>";
}
Serial.println("OLED display is now operational");
debugInfo += "OLED display is now operational <br>";
displayUpdate();
}
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();
}
Explaining the Code:
The displayUpdate() function is used both before and during the game.
Once the game concludes, displayGameOver() will change the display output to show who inflicted the most kills and damage on you.
For additional information on programming the display, a helpful resource can be found at: https://randomnerdtutorials.com/guide-for-oled-display-with-arduino/
Fix code issues
Search for mp3.playFileByIndexNumber. You will need to remove the duplicate found in the Sounds tab, as it will result in the same sound effect being played twice when the tagger is powered on.
Compile Code
The display should look similar to below:

The screen displays the following information: health is at 36, with 99 shots and 99 clips remaining. There are 12 minutes left in the game. The gun is set to fully automatic mode, dealing 5 damage. The player’s name is Gonzo, and the IP address is 192.168.137.136.
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 contains several placeholders that indicate what is to come. In programming, it is often challenging to get everything working simultaneously. The technical term for this is a stub, which is a piece of code that temporarily stands in for code that has yet to be developed. This allows part of the software solution to be tested without needing the entire project to be completed.
In the Bible, the final solution is not revealed all at once. God establishes temporary solutions until the ultimate resolution is realised. In the Old Testament, God instituted the old priest system. This system functioned temporarily but was never intended to be the final answer. Jesus is the ultimate solution to the priest system. Once Jesus arrived as the final solution, there was no longer any need for the old system. The old system was inferior and did not effectively fulfil its purpose, much like a software stub. Therefore, when Jesus came, the old priest and sacrificial system became obsolete and had to be set aside to make way for a new and better system.
This explains why the Bible is divided into two testaments. At one point, God’s people lived under the Old Testament; however, with the coming of Jesus, they were called to live under the New Testament. The New Testament continues to regard the Old Testament highly, so while the sacrificial system of the Old Testament is now obsolete, it still teaches us valuable lessons about how God has acted throughout history, what God is like, our own nature, and how we can serve both God and others. The Old Testament also provides insight into the old sacrificial system, which aids our understanding of the New Testament. The New Testament presents a better way, with Jesus as the sacrifice for our sins and God giving us a new heart that inspires us to serve him.