The Unity-Discover showcase app is an open source application that highlights Meta Quest Mixed Reality APIs. Among the features it highlights is the use of shared spatial anchors (SSAs). In this walkthrough we will examine the Unity-Discover application’s SSA implementation.
After reading this page, you should be able to:
Outline the steps to create a new room as a host in the Unity-Discover app.
Identify the actions performed by the Unity-Discover app when a new room is created by a host.
Explain the procedure for a player to join an existing room and the subsequent actions taken by the Unity-Discover app.
How to get the sample
You do not need to clone or run the Unity-Discover showcase app to go through this walkthrough. After you finish this walkthrough, you can download the running app from Discover. If you want to dig further into the app, start with How to Run the Project in Unity in GitHub.
Unity-Discover makes use of Photon Unity Networking as its spatial anchor networking application. For information on why a spatial anchor networking utility is necessary, see Use Shared Spatial Anchors.
How to use the sample
When you run the Unity-Discover app, you have the option to create a new room as a host, or to join an existing room as a player.
If you choose to be a host, you provide a room name. Unity-Discover then:
Creates a room.
Loads in the initial scene.
Creates a spatial anchor to serve as the origin for the application.
Establishes a connection to Photon Fusion.
Shares the spatial anchor to Photon Fusion, establishing it as the SSA.
Creates a colocated space within the room with the SSA as the origin.
Aligns the host (as a player) to the SSA.
If you choose to be a player and join an existing room, Unity-Discover:
Establishes a connection to Photon Fusion.
Queries the SSA for the named room from Photon Fusion.
Adds the new player to the colocated space.
Aligns the player to the SSA.
Once the SSA is set up, all subsequent actions that start (Bicycle Mechanic or Drone Rage) are relative to that SSA.
If the host leaves the room, and anyone else is still present, one of those players becomes the host.
There are no additional SSAs in Unity-Discover. Because the app is designed to have everything relative to the one SSA, no others are needed.
Code-level application flow
Let’s take a look at how this plays out in the Unity Project. Unity-Discover is a large app, but we will only talk about the parts that help us understand the SSA implementation.
Key prefabs and scripts
Several prefabs play important roles in getting this part of the Unity-Discover app to work:
NetworkModalWindow prefab. This prefab instantiates the NetworkModalWindowController.cs script, which displays and handles the startup options.
DiscoverAppController prefab. This prefab is the central hub for the Unity-Discover app. It brings together the following:
DiscoverAppController.cs script, which manages the connections and sessions for players
NetworkSceneManagerDefault.cs script, which loads and switches among available scenes. This is part of the Fusion assembly.
MRSceneLoader script, which loads the OVRSceneManager.
ColocationDriverNetObj.cs script, which sets up the conditions for creating a colocated space.
NetworkRunner prefab, which carries communications between the Unity-Discover app and the Photon Fusion network. This is part of the Fusion assembly.
ColocationLauncher.cs script. This script creates the colocated spaces for both host and players.
SharedAnchorManager.cs script, which creates and shares the SSA.
AlignmentAnchorManager.cs script, which aligns players to the SSA.
NetworkApplicationManager.cs script, which launches the games available in the room.
Player prefab, which handles the player characteristics within the games. This is part of the Fusion assembly.
A host creates a new room
This section follows the Unity-Discover application as it starts up and creates the colocated space for gameplay by one or more players.
Get user input
At program start, the NetworkModalWindowController.cs script ShowNetworkSelectionMenu method displays the user’s selection options. Among these is whether it’s a host or player request. This decision governs whether to create a new room, or look for the room specified by the user. This is important, because the path for a host includes most of the preliminary room setup.
Then, in the DiscoverAppController.cs Start() method, the options are displayed:
// NetworkModalWindowController.cs
54 private void Start()
55 {
56 MainMenuController.Instance.EnableMenuButton(false);
57 ShowNetworkNux(); //present the user options
58 }
Start the connection and initial scene
After the user has opted to be a host or a player, the DiscoverAppController.cs script starts the next steps. If the user is a host, StartHost() is called, but if the user is a player, StartClient() is called instead. We describe the player path later in A Player Joins an Existing Room.
StartHost() makes a call to the NUXController.cs script StartNux() method, which is itself dependent on the DiscoverAppController.cs script StartConnectionAsync() method:
// DiscoverAppController.cs
125 private async void StartConnectionAsync(bool isHost, GameMode mode = GameMode.Shared)
126 {
127 if (isHost) //if this is a host, create a new room
128 {
129 NetworkModalWindowController.Instance.ShowMessage("Loading Room");
130 _ = await m_mrSceneLoader.LoadScene();
131 }
132
133 SetupForNetworkRunner(); // DiscoverAppController.cs (this file), line 247, instantiate and configures the NetworkRunner
134 NetworkModalWindowController.Instance.ShowMessage("Connecting to Photon...");
135 ColocationDriverNetObj.OnColocationCompletedCallback += OnColocationReady;
136 ColocationDriverNetObj.SkipColocation = AvatarColocationManager.Instance.IsCurrentPlayerRemote;
137 await Connect(isHost, mode); // DiscoverAppController.cs (this file), line 142
138 }
The call to m_mrSceneLoader.LoadScene() is to the MRSceneLoader.cs script, which creates the initial scene.
// MRSceneLoader.cs
17 public async UniTask<bool> LoadScene()
18 {
19 if (!m_sceneLoaded)
20 {
21 m_sceneLoadingTask = new();
Set up the session
The Connect(isHost, mode) method in DiscoverAppController.cs script sets up the session, sets up the game arguments as per the user selections, and then attempts to start the game using Runner.StartGame() (in this case, game refers to the initial room):
// DiscoverAppController.cs
139 await Connect(isHost, mode);
...
168 var joined = await Runner.StartGame(args);
Set up for colocation
The Runner object is the NetworkRunner prefab, and part of the Fusion assembly. The OnConnectedToServer action in the DiscoverAppController.cs script spawns the ColocationDriverNetObj prefab (m_colocationPrefab).
The SetUpForColocation() method does a lot. Among other things, it kicks off and waits for the Photon Network. When that is ready, the method instantiates a SharedAnchorManager and an AlignmentAnchormanager, which are both key components in setting up the SSA.
Launch the colocated space and establish the shared spatial anchor
The m_colocationLauncher variable is a ColocationLauncher object. In the ColocationLauncher.cs script, the CreateColocatedSpace() method calls CreateAlignmentAnchor, which creates the spatial anchor. This is addressed in more detail in the following sections.
// ColocationLauncher.cs
213 private async UniTaskVoid CreateNewColocatedSpace() {
214 _myAlignmentAnchor = await CreateAlignmentAnchor();
215 if (_myAlignmentAnchor == null) {
216 Debug.LogError("ColocationLauncher: Could not create the anchor");
217 return;
218 }
Create the anchor
Here’s how the anchor is created. CreateAlignmentAnchor is also in the ColocationLauncher.cs script. It immediately calls the SharedAnchorManager.cs script CreateAnchor method, which in turn calls InstantiateAnchor right away. InstantiateAnchor uses the game object to create a new anchor based on the AnchorPrefab prefab.
// ColocationLauncher.cs
316 private async UniTask<OVRSpatialAnchor> CreateAlignmentAnchor() {
315 var anchor = await _sharedAnchorManager.CreateAnchor(Vector3.zero, Quaternion.identity);
316 if (anchor == null) {
317 Debug.Log("ColocationLauncher: _sharedAnchorManager.CreateAnchor returned null");
318 }
// SharedAnchorManager.cs
31 public async UniTask<OVRSpatialAnchor> CreateAnchor(Vector3 position, Quaternion orientation) {
32 Debug.Log("CreateAnchor: Attempt to InstantiateAnchor");
33 var anchor = InstantiateAnchor();
34 Debug.Log("CreateAnchor: Attempt to Set Position and Rotation of Anchor");
...
153 private OVRSpatialAnchor InstantiateAnchor() {
154 GameObject anchorGo;
155 if (AnchorPrefab != null) {
156 anchorGo = Object.Instantiate(AnchorPrefab);
157 } else {
158 anchorGo = new GameObject();
159 anchorGo.AddComponent<OVRSpatialAnchor>();
160 }
Save the anchor
After you create the anchor, next you must save it.
In the SharedAnchorManager.cs script, the SaveAnchors method is where we finally see the call to OVRSpatialAnchor.SaveAnchorAsync, which saves the new anchor to persistent storage.
// SharedAnchorManager.cs
public async UniTask<bool> SaveAnchors() {
...
var result = await OVRSpatialAnchor.SaveAnchorsAsync(_anchors);
return result.Success;
The anchor is now ready to be shared to other players.
Align the host and anchor
Earlier we described how in the ColocationLauncher.cs script, the CreateColocatedSpace() method calls CreateAlignmentAnchor. Later in that same method, it calls AlignPlayerToAnchor:
This method is part of the AlignmentAnchorManager.cs script, and is called for every player, including the host. This is an async operation, so the actual work falls to the AlignmentCoroutine method to align both the user camera and hands.
At this point, the game options are displayed to the host. In the following graphic, you can see the location of the SSA (the 0,0,0 point for all room activity). This is the point where the host was standing when the spatial anchor was created.
Sharing the anchor
Each of the colocation methods in the ColocationLauncher.cs script make a call to AttemptToShareAndLocalizeToAnchor (also in ColocationLauncher.cs). At the end of this method, a call to TellOwnerToShareAnchor transfers control to that method later in the script.`
// ColocationLauncher.cs
244 private UniTask<bool> AttemptToShareAndLocalizeToAnchor(Anchor anchor) {
245 Debug.Log(
246 $"ColocationLauncher: Called AttemptToShareAndLocalizeToAnchor with id: {anchor.uuid} and oculusId: {_myOculusId}"
247 );
...
277 _networkMessenger.SendMessageUsingOculusId(
278 _caapEventCodeDictionary[CaapEventCode.TellOwnerToShareAnchor],
279 anchorOwner,
280 data
281 );
The TellOwnerToShareAnchor method calls the SharedAnchorManager.cs script ShareAnchorsWithUser method.
When a player chooses to join an existing room, the flow is much simpler. If the user is a player, the DiscoverAppController.cs script StartClient() method is called and runs the NUXController.cs method StartNux().
StartNux is dependent on the DiscoverAppController.cs script method StartConnectionAsync(). As with the host, the OnConnectedToServer action is invoked. However, only the player portions are executed:
For the player joining a room, the ColocationLauncher.cs script ColocateAutomaticallyInternal() method, gets all known alignment anchors (for Unity-Discover, there is just one), and aligns the player to them.
We will look at how the player is aligned to the SSA next.
Aligning the player and loading the shared spatial anchor
As we learned in Sharing the Anchor, each of the colocation methods in the ColocationLauncher.cs script make a call to AttemptToShareAndLocalizeToAnchor. For a joining player, the method calls LocalizeAnchor:
// ColocationLauncher.cs
244 private UniTask<bool> AttemptToShareAndLocalizeToAnchor(Anchor anchor) {
245 Debug.Log(
246 $"ColocationLauncher: Called AttemptToShareAndLocalizeToAnchor with id: {anchor.uuid} and oculusId: {_myOculusId}"
247 );
...
254 var sharedAnchorId = new Guid(anchor.uuid.ToString());
255 LocalizeAnchor(sharedAnchorId);
256 return _alignToAnchorTask.Task;
The LocalizeAnchor method calls the SharedAnchorManager.cs script method RetrieveAnchors: