Developing your first game using patch bridging (1/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 this tutorial, you’ll build a simple game. Starting with 3 lives, the user scores points when they tap a blue alien and loses lives when they tap a pink alien. You'll learn:

  • How to pass data between the Patch Editor and a script using Patch Bridging and Spark AR Reactive JavaScript.
  • The basics of game design and gameplay interactions, including using the Persistence Module to save a high score and display a scoreboard message.

Download the example project to follow along.

The unfinished project

Inside the unfinished project we’ve already positioned the game assets in the scene, including a 2D stack containing empty text. We'll populate these later using a script.

The unfinished project is shown in Studio.

The project includes a customized plane-tracker block. This block lets the player manipulate the position, rotation and scale of the World object in their scene before triggering a 3 second countdown. After the countdown, the World object stays in position even if the player taps the screen.

Start scripting

Now we’ve set up our scene, we can focus on scripting. First, create a new JavaScript file and open it in Visual Studio Code.

Loading the modules

We want to load in:

Add this code to your script:

const Scene = require("Scene");
const Patches = require("Patches");
const Persistence = require("Persistence");
const Reactive = require("Reactive");
const Diagnostics = require('Diagnostics');

Accessing and populating the 2D text

The text in the unfinished project is empty. Next, we’ll use a script to access and populate the livesText and scoreText.

The 2D text objects can be seen in the Scene panel.

In the previous lessons, we fetched assets one at a time using the async/await keywords. This time, we need to access several assets and signals, so we’ll wrap the operation in a Promise.all() method.

This method takes an iterable of all the objects to be found as an input, and returns a single promise that resolves to list the results of the input promises.

(async function () {  // Enables async/await in JS [part 1]

  // To access scene objects
  const [livesText, scoreText, highScoreText] = await Promise.all([
    Scene.root.findFirst('livesText'),
    Scene.root.findFirst('scoreText'),
    Scene.root.findFirst('highScoreText'),
  ]);


})(); // Enables async/await in JS [part 2]

Now we’re ready to populate the text objects. We’ll use the text property to set the livesText to 3 and the scoreText to 0.

Add this code to your script, below the promise:

  livesText.text = "Lives: 3",
  scoreText.text = "Score: 0"

All together, your script will now look like this:

(async function () {  // Enables async/await in JS [part 1]

  // To access scene objects
  const [livesText, scoreText, highScoreText] = await Promise.all([
    Scene.root.findFirst('livesText'),
    Scene.root.findFirst('scoreText'),
    Scene.root.findFirst('highScoreText'),
  ]);

  livesText.text = "Lives: 3",
  scoreText.text = "Score: 0"
  
})(); // Enables async/await in JS [part 2]

Your script now controls the LIVES and SCORE value that appears on screen:

The 'score' and 'lives' value can now be seen in the Simulator.

Setting up the gameplay

We’re now ready to set up the interactive gameplay using Script to Patch Bridging. This lets us pass information between the script and the Patch Editor using shared variables and the methods provided by the Patches Module API.

Adding interactions

In our gameplay, once the player positions the World object and the countdown is over, we want them to tap the blue aliens and score points.

To set this up:

  • Select the blueAlien1 in the Scene panel.
  • Create an Object Tap patch in the Inspector.
The Interactions section of the Inspector is highlighted.

Now, every time the player taps blueAlien1, a pulse signal is emitted. We want to send this signal to the script.

Sending signals

To send the pulse variable from the Patch Editor:

  • Select the script file in the Assets panel.
  • In the Inspector, to the right of To Script, click + to add a Pulse script variable.
  • Rename the variable BlueAlien1.
  • Click the arrow to the left of the variable to create a yellow consumer patch representing BlueAlien1.
  • Connect the patch to the output of the Object Tap patch.
  • Repeat steps 1-5 for BlueAlien2.

Your patch graph will look like this:

A patch graph with 4 patches.

Accessing signals

To access the pulse signals sent from the Patch Editor in a script, use the get methods provided by the PatchesOutputs Module.

Inside the Promise.all method, add a variable called blueAlien1 and blueAlien2 and two additional lines of code that fetch those signals:

  // Added new variables
  const [livesText, scoreText, highScoreText, blueAlien1, blueAlien2] = 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'),
  ]);

The Patches.outputs.getPulse signal emits an eventSource that can be subscribed to and that invokes a function every time the event (the pulse) is emitted.

The points system

In our game, we want to count the number of times the player taps the target and increment the score accordingly. To do this, we’ll add a reusable block of code, called a function.

Add the score points function to your script, just below the promise method. This one piece of code can be used multiple times by multiple targets:

  let score = 0;

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

  blueAlien1.subscribe(scorePoints),
  blueAlien2.subscribe(scorePoints)

Here, we have:

  • Created a score variable and set it to zero — let score = 0.
  • Made this variable increment in value each time the target is hit — score++.
  • Used the subscription method to subscribe to the Patch Editor signals indicating that a target has been tapped. This method invokes a scorePoints callback function every time the blueAlien1.subscribe(scorePoints) and blueAlien2.subscribe(scorePoints) pulse is emitted.

So far, your complete script will look like this:

const Scene = require("Scene");
const Patches = require("Patches");
const Persistence = require("Persistence");
const Reactive = require("Reactive");
const Diagnostics = require('Diagnostics');

(async function () {  // Enables async/await in JS [part 1]

  // To access scene objects
  const [livesText, scoreText, highScoreText, blueAlien1, blueAlien2] = 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')
  ]);

  // Set initial UI texts
  livesText.text = "Lives: 3",
  scoreText.text = "Score: 0"

  // Set initial variables
  let score = 0;

  // score points incrementally
  function scorePoints() {
    score++;
    Diagnostics.log('Score: ' + score)
  }

  // execute function every time a signal 
  // is sent from the patch editor to the script
  blueAlien1.subscribe(scorePoints)
  blueAlien2.subscribe(scorePoints)

})(); // Enables async/await in JS [part 2]

By using the Diagnostics Module to print the variable score in the console — Diagnostics.log('Score: ' + score) — we see it increasing on each object tap:

The value can be seen printed into the console.

Losing lives

Similarly, we want the player to lose a life every time they tap the wrong target, the pink alien. In other games, this might happen when the character hits an obstacle or collides with a projectile from an enemy.

Select pinkAlien1 in the Scene panel, then:

  1. Create an Object Tap patch using the Inspector.
  2. Select the script and add a Pulse script variable under To Script, named PinkAlien1.
  3. Click the arrow to the left of the variable in the Inspector to create a yellow consumer patch representing PinkAlien1.
  4. Connect it to the Object Tap patch.

Your patch graph looks like this:

A patch graph and the Inspector.

Now, we're ready to make some changes to our script:

  // 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 our initial numbers of life to 3
  let lives = 3;

  // lose lives decrementally
  function loseLives() {
    lives--;
    Diagnostics.log('Lives : ' + lives)
  }

  // execute loseLives( ) every time a pinkAlien1 signal 
  // is sent from the patch editor to the script
  pinkAlien1.subscribe(loseLives)

You'll see that we have added pinkAlien1 to the promise and signals section of our script. We also:

  • Set our initial lives to 3: let lives = 3;
  • Added a loseLives() function that changes life decrementally: lives--
  • Invokes this function every time a pinkAlien1 pulse is detected: pinkAlien1.subscribe(loseLives)

We also use Diagnostics.log('Lives: ' + lives) to log this event in the console:

Lives decreasing as the pink alien is tapped.

If the player continues to tap the pink alien, the lives will continue to decrease into minus numbers. You don’t need to worry about this, as our game will end and UI text will be hidden when the number of lives reaches 0.

So far, our script looks like this:

(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;

  // score points incrementally
  function scorePoints() {
    score++;
    Diagnostics.log('Score: ' + score)
  }

  // lose lives decrementally
  function loseLives() {
    lives--;
    Diagnostics.log('Lives: ' + lives)
  }

  // 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]

Part two

In part two of this patch bridging and AR games tutorial, you'll update your project to:

  • Update the UI text dynamically.
  • Save high scores between sessions using the Persistence Module.
  • End and reset the game when all lives are lost.