Skip to main content

Tic Tac Toe [Beta]

Overview

This guide walks through how to build a simple, colocated game of Tic Tac Toe for Spectacles (2024). This example project is a Connected Lens that demonstrates:

Download the project here.

For more information, see Connected Lenses on Spectacles [Beta] and Sync Framework on Spectacles [Beta].

Prefab Setup: Combining Spectacles Interaction Kit and Sync Framework

The basic mechanic for playing Tic Tac Toe on Spectacles is manipulating X and O objects. Players move pieces with their hands within a shared coordinate space. This interaction is achieved by creating X and O prefabs that combine Spectacles Interaction Kit and Sync Framework components. The steps to setup the X and O prefab are outlined below:

  1. Begin setting up X and O objects with their respective render mesh visual components.

  2. Add a collider and size it to fit the X and O visuals.

  3. Add the following Spectacles Interaction Kit components to make the object interactive:

  • To make objects targetable, add an Interactable component.

  • To make objects manipulatable, add an InteractableManipulation component.

  • To highlight the object when it is being targeted, add an InteractableOutlineFeedback component.

  1. Finally, to sync the transforms of objects, add a SyncTransform component from Sync Framework. Sync the position, rotation, and scale locally since pieces will be manipulated within the colocated coordinate space. Enable smoothing so that object manipulation appears smooth for all players.

  2. Drag the X and O objects from the Scene Hierarchy into the Asset Browser to turn them into prefabs. If you make any additional changes to the prefabs, remember to click Apply in the Inspector to save your updates.

Instantiator Setup: Spawning Colocated Objects

The Sync Framework Instantiator is used to spawn pieces for all players in the colocated session.

To begin configuring the Instantiator , add the Instantiator script to the Scene Hierarchy.

In the Inspector panel for the Instantiator script, add the X and O prefabs to the Prefabs input.

The X and O prefabs are spawned as children of the colocated coordinate space. In the Scene Hierarchy, add a scene object under Connected Lenses > Colocated World [CONFIGURE ME] > Enable on Ready. In the project, the object is named TicTacToeRoot.

In the Instantiator Inspector panel, enable Spawn as Children and populate the Spawn Under Parent input with the TicTacToeRoot scene object.

The Instantiator is now ready to be used by the Controller.js script to spawn X and O prefabs.

Scripting with Sync Framework

Sync Framework provides many tools for scripting custom logic to sync experiences. The Controller.js and Piece.js scripts provide examples of using Session Controller APIs, the Sync Entity class, networked events, storage properties, and the Instantiator.

While example code below is shown in Javascript, Sync Framework is compatible with both JavaScript and TypeScript.

Controller.js Script

The Controller.js script is responsible for assigning player roles (i.e. player X and player O), keeping track of whose turn it is, and spawning pieces until the game is over.

The Controller is initialized on start, which ensures Spectacles Interaction Kit is also initialized and ready to use.

// Initialize on start to make sure Spectacles Interaction Kit (SIK) is ready to use
script.createEvent('OnStartEvent').bind(init);

First, the Controller.js script creates a new Sync Entity, which enables the script to sync data among players. A Sync Entity can sync data through both networked events and storage properties. Here, a networked event is configured to start the game, and a storage property is used to keep track of turns. Callback functions are added to run logic in response to the start event and changes to the turns property.

An onReady callback is also added to the Session Controller notifyOnReady event.

It is important that the Session Controller is ready before calling any other Session Controller APIs to prevent race conditions.

// Initialize
function init() {
// Create new sync entity for this script
syncEntity = new SyncEntity(script);

// Add networked event to start the game
syncEntity.onEventReceived.add('start', start);

// Add a storage property to track how many turns have been played, initialize to 0
turnsProp = StorageProperty.manualInt('turnsCount', 0);
syncEntity.addStorageProperty(turnsProp);
turnsProp.onAnyChange.add(setTurn);

// Set up the session controller notify on ready callback
// Note: Only use session controller APIs once the session controller is ready
global.sessionController.notifyOnReady(onReady);
}

Once the Session Controller is ready, the onReady function runs and assigns players to be X, O, or a spectator when they join. Once a second player joins the experience, player O sends the start event to tell everyone to start the game.

function onReady() {
let playerCount = global.sessionController.getUsers().length;

// Assign pieces to users
// The first player is X, the second is O, everyone else is a spectator
if (playerCount === 1) {
player = 'X';
} else if (playerCount === 2) {
player = 'O';
} else {
player = '';
}

// Wait for the syncEntity to be ready before using it
syncEntity.notifyOnReady(function () {
// If O is assigned and no turns have been played, send event to start the game
if (player === 'O' && turnsProp.currentValue === 0) {
syncEntity.sendEvent('start');
}
});
}

When the start event is received, player X goes first and spawns their piece.

function start() {
// Player X spawns first piece to start the game
if (player === 'X') {
spawn(xPrefab);
}
}

The spawn function instantiates the piece for all players in the session using the Instantiator. Similar to the Session Controller, it is important to check that the Instantiator is ready to be used.

function spawn(prefab) {
if (instantiator.isReady()) {
// Spawn piece using the Sync Framework instantiator, set local start position
let options = new InstantiationOptions();
options.localPosition = new vec3(0, -25, -100);
instantiator.instantiate(prefab, options);
}
}

The Controller.js script provides a script API for finishing a turn, which is used by the Piece.js script. The finishTurn script API increments the turns property, which tracks the number of turns played so far.

function finishTurn() {
// Increment the turns property
let turnsCount = turnsProp.currentValue + 1;
turnsProp.setPendingValue(turnsCount);
}

script.finishTurn = finishTurn;

When the value of the turns property changes, the setTurn callback function gets called, which checks whose turn it is and spawns their piece. If the maximum number of turns have occurred, the game is over.

function setTurn(newCount, oldCount) {
// No player has completed a turn yet, don't do anything
if (newCount === 0) return;

// The maximum number of turns have been played, the game is over
if (newCount === MAX_TURNS) {
print('Game is over!');
return;
}

// Check whose turn it is and spawn their piece
if (newCount % 2 === 0 && player === 'X') {
spawn(xPrefab);
} else if (newCount % 2 === 1 && player === 'O') {
spawn(oPrefab);
}
}

Piece.js Script

The Piece.js script is responsible for enabling or disabling manipulation based on who the piece belongs to, and telling the Controller.js script when the piece has been moved and the player’s turn is finished. The Piece.js script is attached to the X and O prefabs and holds a reference to the Controller.js script.

The Piece.js script initializes when the prefab is instantiated. It first gets the Sync Entity on the object, which is associated with the SyncTransform script, and then waits for the sync entity to be ready to use.

function init() {
sceneObj = script.getSceneObject();

// Get sync entity for SyncTransform script
syncEntity = SyncEntity.getSyncEntityOnSceneObject(sceneObj);

// Check sync entity is ready before using it
syncEntity.notifyOnReady(onReady);
}

init();

Once the Sync Entity is ready, the Piece.js script checks whether it was created by the local player or a remote player. If the piece was created locally, manipulation is enabled, otherwise manipulation is disabled. For locally created pieces, a finishTurn callback function is added to the SIK onManipulationEnd event.

function onReady() {
// Get SIK InteractableManipulation component
let manipulatable = sceneObj.getComponent(
SIK.InteractionConfiguration.requireType('InteractableManipulation')
);

if (syncEntity.networkRoot.locallyCreated) {
// Piece belongs to me, I can manipulate it
manipulatable.setCanTranslate(true);
manipulatable.setCanRotate(true);
manipulatable.setCanScale(true);
manipulatable.onManipulationEnd.add(finishTurn);
} else {
// Piece belongs to other player, I can't manipulate it
manipulatable.setCanTranslate(false);
manipulatable.setCanRotate(false);
manipulatable.setCanScale(false);
}
}

The finishTurn callback function tells the Controller.js script when the player has moved their piece and played their turn. It does this by calling the Controller’s finishTurn script API.

function finishTurn() {
if (!isTurnFinished) {
// Piece was moved, tell controller that my turn is complete
controller.finishTurn();
isTurnFinished = true;
}
}

Additional Resources

This example project is intended to demonstrate Sync Framework for Spectacles. There are many ways to design and implement this kind of game! Here are some ideas to take this example project further:

  • Customize the Start Menu and add player onboarding
  • Let players choose and customize their pieces
  • Snap X and O pieces into a 2D grid or 3D matrix layout
  • Add game logic to check for a winner
  • Sync Spectacles Interaction Kit buttons to start and reset the experience

To learn more about Spectacles design considerations and best practices, see Design for Spectacles

To learn more about building interactions for Spectacles, see Spectacles Interaction Kit

Was this page helpful?
Yes
No

AI-Powered Search