Scripting
Creating Turn-Based Experiences

Creating Turn-Based Experiences

The ParticipantsModule API provides the ability to query and monitor the status of each call Participant in a video calling effect.

As each individual participant is assigned a unique ID that persists for the duration of the video call, even after a dropout, you can design experiences that maintain a stable global state, even if participants join or leave while the effect is actively running.

The example project covered in this article expands upon the effect shown in the Scripting Your First Multipeer Effect article, by converting it into a turn-based experience in which call participants can only change the global video feed background when it's their turn.

Additionally, the project will be structured to allow call participants to drop in and out of the call, while maintaining their place in the turn order.

Turn order logic

As mentioned, we'll be implementing a turn-based mechanic with a persistent turn order, meaning the order doesn't change even if a participant leaves or joins the call while the effect is running.

At a high level, our effect will follow the logic below:

  1. Create a master list of all call participants.
  2. Determine the turn order.
  3. Pass control to the first participant.
  4. When their turn ends, pass control to the next participant.
  5. Repeat step 4 for as long as required.

To build a master list of the call participants, we can use the array returned by Participants.getAllOtherParticipants() and then add the Participant object returned by Participants.self to the array.

The second step is required because getAllOtherParticipants() does not include the current user in the returned array.

To simplify the turn-passing logic, we'll use the master list to populate a second list containing just the participants who are currently active in the effect.

We can achieve this by monitoring the isActiveInSameEffect property of each Participant in the master array and updating the active participants array accordingly whenever a change is detected.

The second array makes it easier to determine which participant control should be passed to next as we can ensure that control is never passed to an inactive participant.


Maintaining a synchronized turn order

With turn-based experiences it's important to ensure that each participant is using an identical turn order, otherwise you can run into issues when trying to determine who control should be passed to.

While this could theoretically be achieved in a number of ways, by sending multiple messages back and forth between participants to reach a consensus on a final turn order, we would need to be weary of not exceeding the message rate limit imposed by the MultipeerModule API backend.

Alternatively, by sorting the active participant array by the participants' unique IDs we can guarantee that each instance of the effect is using an identical turn order, without needing to send any messages between peers.

Once a synchronized turn order has been determined each effect instance can keep track of whose turn it is next by maintaining a turnIndex variable. The effect instance with control can increment the index value and broadcast the new value to other peers when control is ready to be passed to the next participant.


Handling call participants joining or leaving

Another consideration when designing the turn order logic is making sure that the dynamic active participant list is correctly handled as users join or leave the call.

To do this we can monitor each participants' isActiveInSameEffect property and create a callback function which fires whenever the value of the property changes.

Within the callback function we can use the master list to find and reference the participant that has just joined or left and update our active participant list accordingly by adding or removing the array member with a matching ID, making sure to also update the turn index.

Creating the participant lists

To create a master participant list we can use a couple of asynchronous method calls provided by the Participants API to retrieve the current user and all other participants on the call.

// Import the Participants module so that we can use the API
const Participants = require('Participants');

(async function() { // Enable async/await in JS [part 1]
          
  // Retrieve the current participant
  const self = await Participants.self;
          
  // Retrieve all other participants          
  const participants = await Participants.getAllOtherParticipants();

  // Add self to the participants array, as it only fetches other participants
  participants.push(self);
          
})(); // Enable async/await in JS [part 2]

As well as the master list, we need a list to keep track of the participants that are currently active.

The first time a group effect is started in a video call it's automatically applied to all call participants, meaning that initially the active participants list will be the same as the master list.

However, as participants leave and join throughout the call, the second list will need to be updated accordingly. To do this we can monitor whether each participant in the master list is active and add or remove members from the active participants array as needed.

In the code below, we iterate through the master list and populate the active participant list based on the value of each Participant object's isActiveInSameEffect property.

// Create an empty array to store active participants
var activeParticipants = [];

// Iterate through each participant in the master list
participants.forEach(function(participant) {
          
  // Monitor and subscribe to the participant's isActiveInSameEffect property
  // The callback function will be called whenever the signal's value changes
  participant.isActiveInSameEffect.monitor().subscribeWithSnapshot({
          
      // Capture the participant's ID in the snapshot so that it can be accessed
      // in the callback function
      userIndex: participants.indexOf(participant),
          
  // Pass the event and snapshot to the callback function
  }, function(event, snapshot) {
          
      // Participant join/leave method call will be added here at a later step
  });
          
  // Add the participant to the active participants array
  activeParticipants.push(participant);
});

The script so far

// Import the Participants module so that we can use the API
const Participants = require('Participants');
              
// Create an empty array to store active participants
var activeParticipants = [];

(async function() { // Enable async/await in JS [part 1]
          
  // Retrieve the current participant
  const self = await Participants.self;
          
  // Retrieve all other participants          
  const participants = await Participants.getAllOtherParticipants();

  // Add self to the participants array, as it only fetches
  // other participants
  participants.push(self);
   
              
  // Iterate through each participant in the master list
  participants.forEach(function(participant) {
          
    // Monitor and subscribe to the participant's isActiveInSameEffect
    // property
    // The callback function will be called whenever the signal's value
    // changes
    participant.isActiveInSameEffect.monitor().subscribeWithSnapshot({
          
      // Capture the participant's ID in the snapshot so that it can be
      // accessed in the callback function
      userIndex: participants.indexOf(participant),
          
    // Pass the event and snapshot to the callback function
    }, function(event, snapshot) {
          
      // Participant join/leave method call will be added here at a
      // later step
    });
          
    // Add the participant to the active participants array
    activeParticipants.push(participant);
  });
          
})(); // Enable async/await in JS [part 2]

Sorting the active participant list

Next, we need to sort the active participants list to ensure that each instance of the effect is using an identical version of the turn order.

As we'll need to sort the list on startup and again every time a user joins or leaves, we'll create a function to handle the sorting that we can call when needed.

We can use the built-in sort() JavaScript function to do this as we're sorting the array by each Participant object's id property.

// Sort the active participant list by ID
function sortActiveParticipantList(isActive) {
          
  activeParticipants.sort(function(a, b){
          
    if (a.id < b.id) {
      return -1;
    }
          
    if (a.id > b.id) {
      return 1;
    }
  });
}

The script so far

// Import the Participants module so that we can use the API
const Participants = require('Participants');
              
// Create an empty array to store active participants
var activeParticipants = [];

(async function() { // Enable async/await in JS [part 1]
          
  // Retrieve the current participant
  const self = await Participants.self;
          
  // Retrieve all other participants          
  const participants = await Participants.getAllOtherParticipants();

  // Add self to the participants array, as it only fetches
  // other participants
  participants.push(self);
   
              
  // Iterate through each participant in the master list
  participants.forEach(function(participant) {
          
    // Monitor and subscribe to the participant's isActiveInSameEffect
    // property
    // The callback function will be called whenever the signal's value
    // changes
    participant.isActiveInSameEffect.monitor().subscribeWithSnapshot({
          
      // Capture the participant's ID in the snapshot so that it can be 
      // accessed in the callback function
      userIndex: participants.indexOf(participant),
          
    // Pass the event and snapshot to the callback function
    }, function(event, snapshot) {
          
      // Participant join/leave method call will be added here at a
      // later step
    });
          
    // Add the participant to the active participants array
    activeParticipants.push(participant);
  });
              
  // Do an initial sort of the active participants when the effect starts
  sortActiveParticipantList();
              
  
  //==============================================
  // Custom Functions 
  //==============================================
              
  // Sort the active participant list by ID
  function sortActiveParticipantList(isActive) {
          
    activeParticipants.sort(function(a, b){
          
      if (a.id < b.id) {
        return -1;
      }
          
      if (a.id > b.id) {
        return 1;
      }
    });
  }
          
})(); // Enable async/await in JS [part 2]

Showing and hiding visual elements

To help visualize the turn order we'll display a text panel on the screen of the participant whose turn it is. In the example project, we've added a rectangle to the Scene Hierarchy and sent its visibility property to the Patch Editor.

Aside from helping participants, using some form of visual aid also makes it easier to debug and test video call effects with multiple participants, especially when used alongside the grid view in the Spark AR Player for Desktop.

Before we implement our logic, we need to add a From Script variable from within the script asset's Properties panel and name it showTurnPanel, as we'll be using this to pass data from our script to the Patch Editor.

Next we'll write a function that checks whether it's the current instance's turn and if so, updates a boolean in the Patch Editor that is then used to set the visibility of the text panel.

// Import the Patches modules so that we can use the API
const Patches = require('Patches');
          
// Initialize the turn tracking variable
var turnIndex = 0;

// Check whether this participant should display the text panel
// The panel will only display if it's this participant's turn
function checkShowTurnPanel() {
          
  // Check if this participant's ID matches the ID in the turn index
  let isMyTurn = activeParticipants[turnIndex].id === self.id;
  
  // Send the returned boolean value to the Patch Editor and assign it to 
  // the showTurnPanel boolean
  Patches.inputs.setBoolean('showTurnPanel', isMyTurn);
}

The script so far

// Load the required modules
const Participants = require('Participants');
const Patches = require('Patches');
              
// Create an empty array to store active participants
var activeParticipants = [];
              
// Initialize the turn tracking variable
var turnIndex = 0;

(async function() { // Enable async/await in JS [part 1]
          
  // Retrieve the current participant
  const self = await Participants.self;
          
  // Retrieve all other participants          
  const participants = await Participants.getAllOtherParticipants();

  // Add self to the participants array, as it only fetches
  // other participants
  participants.push(self);
   
              
  // Iterate through each participant in the master list
  participants.forEach(function(participant) {
          
    // Monitor and subscribe to the participant's isActiveInSameEffect 
    // property
    // The callback function will be called whenever the signal's value
    // changes
    participant.isActiveInSameEffect.monitor().subscribeWithSnapshot({
          
      // Capture the participant's ID in the snapshot so that it can be
      // accessed in the callback function
      userIndex: participants.indexOf(participant),
          
    // Pass the event and snapshot to the callback function
    }, function(event, snapshot) {
          
      // Participant join/leave method call will be added here at a
      // later step
    });
          
    // Add the participant to the active participants array
    activeParticipants.push(participant);
  });              
              
  
  //==============================================
  // Custom Functions 
  //==============================================
              
  // Sort the active participant list by ID
  function sortActiveParticipantList(isActive) {
          
    activeParticipants.sort(function(a, b){
          
      if (a.id < b.id) {
        return -1;
      }
          
      if (a.id > b.id) {
        return 1;
      }
    });
  }

              
  // Check whether this participant should display the text panel
  // The panel will only display if it's this participant's turn
  function checkShowTurnPanel() {
          
    // Check if this participant's ID matches the ID in the turn index
    let isMyTurn = activeParticipants[turnIndex].id === self.id;
  
    // Send the returned boolean value to the Patch Editor and assign it 
    // to the showTurnPanel boolean
    Patches.inputs.setBoolean('showTurnPanel', isMyTurn);
  }
          
})(); // Enable async/await in JS [part 2]

Setting the turn order

Now that we've implemented the ability to show or hide the turn panel, participants need to be able to pass the turn over to the next person in the queue.

To do this we can set up a Screen Tap and Hold gesture in the Patch Editor and forward the event to our script.

We also need to add a To Script variable in the script asset's Properties panel called turnPulseRequest and send it to the Patch Editor.

In our script we can then retrieve the gesture's event and add logic to pass the turn by incrementing the turnIndex. Within the gesture event's callback function we also need to make a call to the checkShowTurnPanel() function we wrote previously.

// Import the Multipeer and Diagnostics modules so that we can use their APIs
const Multipeer = require('Multipeer');
const Diagnostics = require('Diagnostics');

          
// Create a message channel to send turn index updates on
const syncTurnChannel = Multipeer.getMessageChannel('SyncTurnTopic');
          
// Get the 'Screen Tap and Hold' pulse event from the Patch Editor
const turnPulseRequest = await Patches.outputs.getPulse('turnPulseRequest');

// Subscribe to the pulse event, then pass the turn whenever it happens
turnPulseRequest.subscribe(function() {
          
  // Check if it's this instance's turn
  if (activeParticipants[turnIndex].id === self.id) {
          
    // Increment the turn index to pass the turn to the next participant
    turnIndex = (turnIndex + 1) % activeParticipants.length; 
        
    // Check if this instance needs to display the turn panel, in case this 
    // instance is the only participant in the effect
    checkShowTurnPanel();
    
    // Broadcast a message to all other participants, containing the updated
    // turn index value
    syncTurnChannel.sendMessage({'turnIndex': turnIndex}, false).catch(err => {
      Diagnostics.log(err);
    });
  }
});          

To finish up the turn passing logic, we need to intialize each participant's effect state when the experience is first launched.

// Do an initial sort of the active participants when the effect starts
sortActiveParticipantList();
              
// Do an initial check of whether this participant should display the 
// turn panel
checkShowTurnPanel();

The script so far

// Load the required modules
const Participants = require('Participants');
const Patches = require('Patches');
const Multipeer = require('Multipeer');
const Diagnostics = require('Diagnostics');
              
// Create an empty array to store active participants
var activeParticipants = [];
              
// Initialize the turn tracking variable
var turnIndex = 0;

(async function() { // Enable async/await in JS [part 1]
          
  // Retrieve the current participant
  const self = await Participants.self;
          
  // Retrieve all other participants          
  const participants = await Participants.getAllOtherParticipants();

  // Add self to the participants array, as it only fetches
  // other participants
  participants.push(self);
              
  // Create a message channel to send turn index updates on
  const syncTurnChannel = Multipeer.getMessageChannel('SyncTurnTopic');
              
  // Get the 'Screen Tap and Hold' pulse event from the Patch Editor
  const turnPulseRequest = await Patches.
                             outputs.getPulse('turnPulseRequest');
   
              
  // Iterate through each participant in the master list
  participants.forEach(function(participant) {
          
    // Monitor and subscribe to the participant's isActiveInSameEffect
    // property
    // The callback function will be called whenever the signal's value
    // changes
    participant.isActiveInSameEffect.monitor().subscribeWithSnapshot({
          
      // Capture the participant's ID in the snapshot so that it can be 
      // accessed in the callback function
      userIndex: participants.indexOf(participant),
          
    // Pass the event and snapshot to the callback function
    }, function(event, snapshot) {
          
      // Participant join/leave method call will be added here at a
      // later step
    });
          
    // Add the participant to the active participants array
    activeParticipants.push(participant);
  });           
              
  // Do an initial sort of the active participants when the effect starts
  sortActiveParticipantList();
              
  // Do an initial check of whether this participant should display the 
  // turn panel
  checkShowTurnPanel();
              
  
  //==============================================
  // Custom Functions 
  //==============================================
              
  // Sort the active participant list by ID
  function sortActiveParticipantList(isActive) {
          
    activeParticipants.sort(function(a, b){
          
      if (a.id < b.id) {
        return -1;
      }
          
      if (a.id > b.id) {
        return 1;
      }
    });
  }            
        
              
  // Check whether this participant should display the text panel
  // The panel will only display if it's this participant's turn
  function checkShowTurnPanel() {
          
    // Check if this participant's ID matches the ID in the turn index
    let isMyTurn = activeParticipants[turnIndex].id === self.id;
  
    // Send the returned boolean value to the Patch Editor and assign it 
    // to the showTurnPanel boolean
    Patches.inputs.setBoolean('showTurnPanel', isMyTurn);
  }
              

  //==============================================
  // Callback Functions 
  //==============================================
              
              
  // Subscribe to the pulse event, then pass the turn whenever it happens
  turnPulseRequest.subscribe(function() {
          
    // Check if it's this instance's turn
    if (activeParticipants[turnIndex].id === self.id) {
          
      // Increment the turn index to pass the turn to the next participant
      turnIndex = (turnIndex + 1) % activeParticipants.length; 
        
      // Check if this instance needs to display the turn panel, in case 
      // this instance is the only participant in the effect
      checkShowTurnPanel();
    
      // Broadcast a message to all other participants, containing the
      // updated turn index value
      syncTurnChannel.sendMessage({'turnIndex': turnIndex}, false).
                                                           catch(err => {
        Diagnostics.log(err);
      });
    }
  });
          
})(); // Enable async/await in JS [part 2]

Handling participants joining or leaving

Next we need to write a function that will handle participants joining or leaving the effect.

If a participant joins the effect, they will be added to the activeParticipants list and the list will be sorted once again.

We can then send the new participant the current shared background index so that their instance knows which backgroud needs to be displayed. The message is sent after a delay to give the new effect instance time to load and set up the multipeer messaging subscriptions.

If a participant exits the effect, they will be removed from the activeParticipants list.

When participants join or leave, the turnIndex needs to be updated to ensure that the current turn isn't passed over unintentionally.

To do this we find the user whose turn it currently is in the updated activeParticipants list and set the turnIndex to their index.

In a scenario where the participant whose turn it is leaves the effect the turnIndex doesn't need to be updated, since the next user in the list will take the index of the user who left. However, the turnIndex will need to be reset to zero if the participant that left was the last index in activeParticipants.

// Import the Time modules so that we can use the API
const Time = require('Time');

// Initialize the background index variable
var backgroundIndex = 0;
          
// Create a message channel to send background index updates on
const syncBGChannel = Multipeer.getMessageChannel('SyncBGTopic');

// Updates the turn index and active participant list when a user joins/leaves
// This will be passed as the callback function for the master participant
// list iteration implemented earlier
// If a participant joins 'isActive' is true, if they leave it's false
function onUserEnterOrLeave(userIndex, isActive) {
          
  // Get a participant from the participant array
  let participant = participants[userIndex];
          
  // Keep a reference of the current turn participant before adding or
  // removing anyone
  let currentTurnParticipant = activeParticipants[turnIndex];
  
  // If a participant has joined
  if (isActive) {
          
    // Log a message to the console
    Diagnostics.log("User entered the effect");
          
    // Add the participant to the active participants list
    activeParticipants.push(participant);

    // After 1 second, send the new participant the current background index
    // The delay allows them time to set up their messaging subscriptions
    Time.setTimeout(function() {
          
          
    // Broadcast a message to all other participants, containing the updated
    // background index value
      syncBGChannel.sendMessage({'background': backgroundIndex }, false).
                                                               catch(err => {
          
      // If an error occurs, log it to the console
        Diagnostics.log(err);
      });
    }, 1000);   
          
  } else {
    
    // Log a message to the console
    Diagnostics.log("User left the effect");
          
    // Update the active participants list with the new participant
    let activeIndex = activeParticipants.indexOf(participant);
    activeParticipants.splice(activeIndex, 1);
  }

  // After adding or removing the participant, sort the list again
  sortActiveParticipantList();

  // If the user whose turn it was is still in the effect, change the turnIndex
  // to that user's new index
  if (activeParticipants.includes(currentTurnParticipant)) {
    turnIndex = activeParticipants.indexOf(currentTurnParticipant);
  
  // If the user whose turn it was is no longer in the effect, update the
  // turnIndex as it could be too high now
  } else {
    turnIndex = turnIndex%activeParticipants.length;
  }

  // Call the function that checks if the text panel should be displayed by 
  // this participant
  checkShowTurnPanel();
}

Aside from handling participants joining the effect, we also need to make sure we update the master participant list when a user joins the call itself so that we can monitor when they join and leave the effect, like we do with other participants.

Participants who join a call when an effect is already in progress do not have the effect applied to them automatically. They would need to explicitly launch the effect and as such, they don't need to be added to the activeParticipants array immediately on joining.



If you're testing this effect in a test environment such as Spark AR Studio or the Spark AR Player for Desktop then the participant will need to be added to the activeParticipants list within this callback, as these test environments do automatically apply the group effect to call joiners.



// Monitor when a new participant joins the call
Participants.onOtherParticipantAdded().subscribe(function(participant) {
          
  // Add the participant to the master list
  participants.push(participant);

  // Monitor and subscribe to the participant's isActiveInSameEffect property
  // The callback function will be called whenever the signal's value changes
  participant.isActiveInSameEffect.monitor().subscribeWithSnapshot({
          
    // Capture the participant's ID in the snapshot so that it can be accessed
    // in the callback function
    userIndex: participants.indexOf(participant),
          
  // Pass the event and snapshot to the callback function
  }, function(event, snapshot) {
          
    // Call the function that handles participants joining or leaving
    onUserEnterOrLeave(snapshot.userIndex, event.newValue);
   });
});

With that, our turn-based logic is complete. You can find the final script with all of the snippets below.

The logic for changing the background and the associated patch implementations weren't covered in this article, but are available in the example project.


The finished script

// Load the required modules
const Participants = require('Participants');
const Patches = require('Patches');
const Multipeer = require('Multipeer');
const Diagnostics = require('Diagnostics');
const Time = require('Time');
              
// Create an empty array to store active participants
var activeParticipants = [];                        
              
// Initialize the turn tracking variable
var turnIndex = 0;
              
// Initialize the background index variable
var backgroundIndex = 0;

(async function() { // Enable async/await in JS [part 1]
          
  // Retrieve the current participant
  const self = await Participants.self;
          
  // Retrieve all other participants          
  const participants = await Participants.getAllOtherParticipants();

  // Add self to the participants array, as it only fetches
  // other participants
  participants.push(self);
              
  // Create a message channel to send turn index updates on
  const syncTurnChannel = Multipeer.getMessageChannel('SyncTurnTopic');
              
  // Create a message channel to send background index updates on
  const syncBGChannel = Multipeer.getMessageChannel('SyncBGTopic');
              
  // Get the 'Screen Tap and Hold' pulse event from the Patch Editor
  const turnPulseRequest = await Patches.
                             outputs.getPulse('turnPulseRequest');
   
              
  // Iterate through each participant in the master list
  participants.forEach(function(participant) {
          
    // Monitor and subscribe to the participant's isActiveInSameEffect
    // property
    // The callback function will be called whenever the signal's value
    // changes
    participant.isActiveInSameEffect.monitor().subscribeWithSnapshot({
          
      // Capture the participant's ID in the snapshot so that it can be 
      // accessed in the callback function
      userIndex: participants.indexOf(participant),
          
    // Pass the event and snapshot to the callback function
    }, function(event, snapshot) {
      
      // Call the function that handles participants joining or leaving
      onUserEnterOrLeave(snapshot.userIndex, event.newValue);
    });
          
    // Add the participant to the active participants array
    activeParticipants.push(participant);
  });
              
  // Monitor when a new participant joins the call
  Participants.onOtherParticipantAdded().subscribe(function(participant) {
          
    // Add the participant to the master list
    participants.push(participant);

    // Monitor and subscribe to the participant's isActiveInSameEffect property
    // The callback function will be called whenever the signal's value changes
    participant.isActiveInSameEffect.monitor().subscribeWithSnapshot({
          
      // Capture the participant's ID in the snapshot so that it can be accessed
      // in the callback function
      userIndex: participants.indexOf(participant),
          
    // Pass the event and snapshot to the callback function
    }, function(event, snapshot) {
          
      // Call the function that handles participants joining or leaving
      onUserEnterOrLeave(snapshot.userIndex, event.newValue);
    });
  });
              
  // Do an initial sort of the active participants when the effect starts
  sortActiveParticipantList();
              
  // Do an initial check of whether this participant should display the 
  // turn panel
  checkShowTurnPanel();
              
  
  //==============================================
  // Custom Functions 
  //==============================================
              
  // Sort the active participant list by ID
  function sortActiveParticipantList(isActive) {
          
    activeParticipants.sort(function(a, b){
          
      if (a.id < b.id) {
        return -1;
      }
          
      if (a.id > b.id) {
        return 1;
      }
    });
  }            
        
              
  // Check whether this participant should display the text panel
  // The panel will only display if it's this participant's turn
  function checkShowTurnPanel() {
          
    // Check if this participant's ID matches the ID in the turn index
    let isMyTurn = activeParticipants[turnIndex].id === self.id;
  
    // Send the returned boolean value to the Patch Editor and assign it 
    // to the showTurnPanel boolean
    Patches.inputs.setBoolean('showTurnPanel', isMyTurn);
  }
              
              
  // Updates the turn index and active participant list when a user 
  // joins or leaves
  // This will be passed as the callback function for the master
  // participant list iteration implemented earlier
  // If a participant joins 'isActive' is true, if they leave it's false
  function onUserEnterOrLeave(userIndex, isActive) {
          
    // Get a participant from the participant array
    let participant = participants[userIndex];
          
    // Keep a reference of the current turn participant before adding or
    // removing anyone
    let currentTurnParticipant = activeParticipants[turnIndex];
  
    // If a participant has joined
    if (isActive) {
          
      // Log a message to the console
      Diagnostics.log("User entered the effect");
          
      // Add the participant to the active participants list
      activeParticipants.push(participant);

      // After 1 second, send the new participant the current background
      // index
      // The delay allows them time to set up their messaging subscriptions
      Time.setTimeout(function() {
          
          
        // Broadcast a message to all other participants, containing the
        // updated background index value
        syncBGChannel.sendMessage({'background': backgroundIndex }, false).
                                                               catch(err => {
          
          // If an error occurs, log it to the console
          Diagnostics.log(err);
        });
      }, 1000);   
          
    } else {
    
      // Log a message to the console
      Diagnostics.log("User left the effect");
          
      // Update the active participants list with the new participant
      let activeIndex = activeParticipants.indexOf(participant);
      activeParticipants.splice(activeIndex, 1);
    }

    // After adding or removing the participant, sort the list again
    sortActiveParticipantList();

    // If the user whose turn it was is still in the effect, change the
    // turnIndex to that user's new index
    if (activeParticipants.includes(currentTurnParticipant)) {
      turnIndex = activeParticipants.indexOf(currentTurnParticipant);
  
    // If the user whose turn it was is no longer in the effect, update the
    // turnIndex as it could be too high now
    } else {
      turnIndex = turnIndex%activeParticipants.length;
    }

    // Call the function that checks if the text panel should be displayed 
    // by this participant
    checkShowTurnPanel();
  }              


  //==============================================
  // Callback Functions 
  //==============================================
              
              
  // Subscribe to the pulse event, then pass the turn whenever it happens
  turnPulseRequest.subscribe(function() {
          
    // Check if it's this instance's turn
    if (activeParticipants[turnIndex].id === self.id) {
          
      // Increment the turn index to pass the turn to the next participant
      turnIndex = (turnIndex + 1) % activeParticipants.length; 
        
      // Check if this instance needs to display the turn panel, in case 
      // this instance is the only participant in the effect
      checkShowTurnPanel();
    
      // Broadcast a message to all other participants, containing the
      // updated turn index value
      syncTurnChannel.sendMessage({'turnIndex': turnIndex}, false).
                                                           catch(err => {
        Diagnostics.log(err);
      });
    }
  });
          
})(); // Enable async/await in JS [part 2]
Was this article helpful?