Developing your first game using patch bridging (2/2)

This video features the following 3D object under CC-BY-4.0: Fantasy_Island by Omar Faruq Tawsif and Finding titles is nightmare by Duhan.

In part one, we fetched assets and set up the signals required to run our games scoring. This time, we're going to be:

  • Updating the UI text dynamically.
  • Saving our users' high scores between sessions using the Persistence Module.
  • Ending and resetting the game when all lives are lost.

Download the example project to follow along.

Setting the UI text dynamically

At the moment, the script is controlling the number of lives (livesText) and scores (scoreText) that appear on screen.

We’ll amend the script so the values change dynamically as the player scores points and loses lives. We’ll change the text property that specifies the text displayed inside each function.

Add these two lines of code below score points and lose life functions we've already built:

  function scorePoints() {
    score++;
    Diagnostics.log('Score: ' + score)

    // *NEW* set text display score dynamically
    scoreText.text = 'Score: ' + score.toString();
  }

  function loseLives() {
    lives--;
    Diagnostics.log('Lives: ' + lives)

    // *NEW* set text displaying number of lives dynamically
    livesText.text = 'Lives: ' + lives.toString();
  }

Your game now works like this:

The score and lose life functions can be seen working in the console.

Setting the UI text dynamically

We’re going to use the Persistence Module to save a high score and display it on the screen at the end of the game.

Learn more about the Persistence Module

The Persistence Module allows arbitrary game data to be stored between sessions. It exposes a key-value store where data is stored and retrieved with a particular key. Later, when the user returns to the effect, the effect can restore this data from its JS script.

In other types of games, you could decide to get the position of objects from the previous session and carry them over to a new session. If you’re making a chess game, for example, you'll need to store the positions of your chess pieces. For our effect, we are storing the high score.

To use the Persistence Module we first need to whitelist the data we are saving in the project properties. To do this:

  1. Click the gear icon in the toolbar.
  2. Select Change Project Properties. You’ll see Persistence is already listed under capabilities. This is because we added it as a module at the start of our tutorial.
  3. Select Persistence and enter highScore in the Whitelisted keys box.
The capability properties.

You can now use the Persistence module in your code. Add this line of code inside the scorePoints function:

Persistence.userScope.set('highScore', {value: score});

This code accesses the userScope property from the Persistence Module and sets the current score as the highScore value.

When add this line, Spark AR extension for VS Code will prompt you that the set method expects a string. This string contains:

  • The name of the data we’re saving, i.e. highScore.
  • The object, i.e. the score value we’re saving.
The capability properties.

Now the current score is being saved as the highScore we can fetch it and display it in the UI.

Next, add this code above the scorePoints( ) function:

 let highScore = 0;

 try {
   // get user scope data stored as 'highScore'
   highScore = await Persitence.userScope.get("highScore");

   // check if there is a highScore data to be displayed in the console
   if (highScore) {
      Diagnostics.log("Score from previous session: " + highScore.value);
    }
  } catch (error) {
    // display errors in the console
    Diagnostics.log("Error: " + error);
  }

The above code:

  • Creates a variable called highScore and set it to zero.
  • Adds the Persistence module to access the useScope property and get the highScore. Note that in VS Code a promise was returned. That’s why we need the keyword await.
  • Checks to see if there is any highScore saved and, if there is, logs this variable in the console. Note that if it’s the first time the player is playing, there won’t be a highScore saved. Without this check, we could get a null/undefined error in the console.

Updating the UI text

When the player ends the game, we want to update the UI with the current high score and hide the life and score texts. To do this:

Create a new variable to update the score text and set it to an empty string updatedScoreText = "".

Add the following code inside the loseLife function:

// check if game is over
    if (lives === 0) {
      // hide the lives and score texts
      livesText.hidden = scoreText.hidden = true;

      // set the score message based on the comparison 
      // between the high score and current score
      if ( highScore && highScore.value > score) {
        updatedScoreText = `Your score was higher in the previous session:  ${highScore.value}`
      } else {
        updatedScoreText = `This is your new high score: ${score}`
      } 
      
      // display the updated score message
      highScoreText.text = updatedScoreText;
    }

In the above code:

  • First we check if the user has exhausted their lives — if (lives === 0) {
  • If so, we hide both the life and score texts at the same time — livesText.hidden = scoreText.hidden = true;
  • Then, we display the highest score text depending on a condition — highScoreText.text = updatedScoreText;
  • If there is highScore data available and it is bigger than the current score, we update the score text with the statement ‘Your score was higher in the previous session’’ followed by the saved highScore data:
      if ( highScore && highScore.value > score) {
        updatedScoreText = `Your score was higher in the previous session:  ${highScore.value}`
      } ...
  • Otherwise we update the text to say "This is your new high score" followed by the current score value:
  ...   else {
        updatedScoreText = `This is your new high score: ${score}`
      } 
      
      highScoreText.text = updatedScoreText;
    }

Ending the game

At the end of the game, falling particles signal that the game is over. We achieved this by sending a pulse from the script to the Patch Editor to trigger particles to appear.

So far in the tutorial, we’ve been receiving variables from the Patch Editor. To send a variable from the script to the Patch Editor, we’ll use the set methods provided by the PatchesInput API.

First, select the script and add a From Script pulse variable. Name it GameOver.

Go back to your script. Staying inside the if statement in the loseLives function, add this last final line of code:

    // send pulse to patch editor to indicate game is over
      Patches.inputs.setPulse('GameOver',Reactive.once());

Here, the Reactive.once() method is used to return an eventSource (a pulse) as soon as possible. When the game is over, this pulse will be sent immediately.

In Spark AR Studio:

  1. Select the script.js file.
  2. Click Create , under Interactions in the Inspector.

A purple Variables From Script patch will appear in the Patch Editor:

Next, add the following patches to the Patch Editor:

  • Switch.
  • If Then Else.
  • And a yellow consumer patch representing emitter0's Birthrate (click the yellow arrow in the Properties panel).

Finally:

  • Set the Birthrate in emitter0 to 40.
  • Connect the graph as below:

And with that, you’ve created your first prototype game in Spark AR Studio — congratulations! Here's what the finished effect will look like:

And here's the finished script:

// How to load in modules
const Scene = require('Scene');
const Diagnostics = require('Diagnostics');
const Patches = require('Patches');
const Persitence = require('Persistence');
const Reactive = require('Reactive');
 
(async function () {  // Enables async/await in JS [part 1]
 
 // To access scene objects
 const [livesText, scoreText, highScoreText, blueAlien1, blueAlien2, pinkAlien1] = await Promise.all([
   Scene.root.findFirst('livesText'),
   Scene.root.findFirst('scoreText'),
   Scene.root.findFirst('highScoreText'),
 
   // get signals from patch editor
   Patches.outputs.getPulse('BlueAlien1'),
   Patches.outputs.getPulse('BlueAlien2'),
   Patches.outputs.getPulse('PinkAlien1')
 ]);
 
 // Set initial UI texts
 livesText.text = "Lives: 3";
 scoreText.text = "Score: 0";
 
 // Set initial variables
 let score = 0;
 let lives = 3;
 let highScore = 0;
 let updatedScoreText = '';
 
 try {
   // get user scope data stored as 'highScore'
   highScore = await Persitence.userScope.get("highScore");

   // check if there is a highScore data to be displayed in the console
   if (highScore) {
     Diagnostics.log("Score from previous session: " + highScore.value);
   }
 } catch (error) {
   // display errors in the console
   Diagnostics.log("Error: " + error);
 }
 
 // score points incrementally
 function scorePoints() {
   score++;
 
   Diagnostics.log('Score: ' + score);
 
   // set text display score dynamically
   scoreText.text = 'Score: ' + score.toString();
  
   // save current score as high score
   Persitence.userScope.set('highScore', {value: score});
 }
 
 // lose life decrementaly
 function loseLives() {
   lives--;
 
   Diagnostics.log('Lives: ' + lives);
  
   // set text displaying number of lives dynamically
   livesText.text = 'Lives: ' + lives.toString();
 
   // check if game is over
   if (lives === 0) {
     // hide the lives and score texts
     livesText.hidden = scoreText.hidden = true;
 
     // set the score message based on the comparison 
     // between the high score and current score
     if (highScore && highScore.value > score) {
       updatedScoreText = `Your score was higher in the previous session: ${highScore.value}`
     } else {
       updatedScoreText = `This is your new high score: ${score}`
     }
 
     // display the updated score message
     highScoreText.text = updatedScoreText;
 
     // send pulse to patch editor to indicate game is over
     Patches.inputs.setPulse('GameOver', Reactive.once());
   }
 
 }
 
 // execute function every time a signal
 // is sent from the patch editor to the script
 blueAlien1.subscribe(scorePoints);
 blueAlien2.subscribe(scorePoints);
 pinkAlien1.subscribe(loseLives);
 
 
})(); // Enables async/await in JS [part 2]