Develop
Develop
Select your platform

Colocated Experiences Motif

Updated: Oct 1, 2025
This MR Motif is a guide through everything from the very basics of Spatial Anchors, through how to share them over the network, to how to set up co-located experiences using Colocation Discovery. Furthermore, the project includes a guide on how to set up the new Space Sharing API, allowing developers to build colocated apps where all participants can leverage detailed information about their physical surroundings with MRUK. The project also contains the new Microgestures, allowing users to interact with the shared Whiteboard sample in the project, using controllers and hands equally.
Colocation is used to streamline multi-user experiences, making social and collaborative play more immersive and natural. By using technologies such as the bluetooth-based Colocation Discovery and Shared Spatial Anchors, developers can synchronize virtual content with the real world, allowing users to interact with the same digital objects in the same physical space.

Requirements

Sample scenes

Depth Effects

The Colocation Discovery scene contains a shared whiteboard, where users can draw and write on the same surface. Furthermore, this scene contains the new Microgestures, allowing users to move and scale the whiteboard using their hands and not just controllers. This scene teaches you how to use Colocation Discovery and how to share anchors between users.

The Space Sharing scene demonstrates how to use the new Space Sharing API to share the room layout with other users. This scene contains a bouncing ball spawner, where users can spawn balls in the same physical space and see how they bounce off the shared room meshes.
Grounding Shadow

Spatial Anchors Basics

An anchor is a world-locked frame of reference that gives a position and orientation to a virtual object in the real world. Applications can use one anchor per virtual object, or choose to have multiple virtual objects use the same anchor as long as those objects are within its coverage area of three meters.
The Anchors API offers several key features:
  • Persistence across sessions: The pose of an anchor can be persisted.
  • Discovery across sessions: Anchors can be discovered and reused.
  • Sharing with other users: Anchors can be shared synchronously and asynchronously.
For managing anchors, the MR Motifs project contains the SpatialAnchorManager, SpatialAnchorStorage, and SpatialAnchorLoader classes. These classes are samples of how the Anchors API could be used and enable basic operations such as creating, saving, loading, and erasing anchors.

Basic Anchor Operations

📍 Create Anchor

Creating an anchor can be done by simply instantiating a prefab that contains the OVRSpatialAnchor component, or taking an existing object in the scene and adding the OVRSpatialAnchor component to it. This assigns a UUID and creates the anchor asynchronously.
var anchor = gameObject.AddComponent<OVRSpatialAnchor>();

while (!anchor.Created)
{
    await Task.Yield();
}
The anchor contains a property called Created to check if the creation is already successfully completed. It is good practice to wait until the anchor has been created before continuing.

💾 Save Anchor

The anchor can be saved by calling OVRSpatialAnchor.SaveAnchorAsync.
await anchor.SaveAnchorAsync();
SpatialAnchorStorage.SaveUuidToPlayerPrefs(anchor.Uuid);
The ColocatedExperiences project contains a static SpatialAnchorStorage class, which allows the user to save the anchors to the Unity PlayerPrefs. We can simply call the SaveUuidToPlayerPrefs method of the sample project, to store the spatial anchor on the device.
public static void SaveUuidToPlayerPrefs(Guid uuid)
{
    var count = PlayerPrefs.GetInt(NumUuidsKey, 0);
    PlayerPrefs.SetString($"{UuidKeyPrefix}{count}", uuid.ToString());
    PlayerPrefs.SetInt(NumUuidsKey, count + 1);
    PlayerPrefs.Save();
}
This method adds the new UUID to the PlayerPrefs with a count. This way, one can easily keep track of multiple UUIDs and access them later.

📦 Load Anchor

For loading anchors there is a class called SpatialAnchorLoader, as this involves three separate steps: Loading, Localizing, and Binding. When loading an anchor they are initially unbound. When an anchor is unbound, it means the anchor is not yet connected to its intended GameObject’s OVRSpatialAnchor component. The anchor must be bound to manage its lifecycle and to provide access to other features such as save and erase.
To load an anchor, its UUID is needed. As all UUIDs are stored in the PlayerPrefs, the user can query a list of UUIDs from the static SpatialAnchorStorage class.
var uuids = SpatialAnchorStorage.LoadAllUuidsFromPlayerPrefs();
Next, the LoadUnboundAnchorsAsync method is called. Also here, it is best-practice to await a successful result, before the anchor is then localized by calling LocalizeAsync. Localizing an anchor causes the system to determine the anchor’s pose in the world.
var unboundAnchors = new List<OVRSpatialAnchor.UnboundAnchor>();
await OVRSpatialAnchor.LoadUnboundAnchorsAsync(uuids, unboundAnchors);

foreach (var unboundAnchor in unboundAnchors)
{
    if (await unboundAnchor.LocalizeAsync())
    {
        // Bind anchor here
    }
}
We await the successful localization before binding the anchor. To bind the unbound anchor to its OVRSpatialAnchor component, the built-in BindTo method can be called.
if (!unboundAnchor.TryGetPose(out var pose))
{
    return;
}

var anchorObject = Instantiate(anchorPrefab.gameObject, pose.position, pose.rotation);
var spatialAnchor = anchorObject.GetComponent<OVRSpatialAnchor>();

unboundAnchor.BindTo(spatialAnchor);

🗑️ Erase Anchor

We use the OVRSpatialAnchor.EraseAnchorAsync method to erase a spatial anchor from persistent storage. After that, it is best-practice to destroy the anchor GameObject to stop tracking it in the runtime.
await anchor.EraseAnchorAsync();
Destroy(anchor.gameObject);
And with this how anchors are easily created, saved, loaded, and erased. To create colocated experiences it is also important to know how to share anchors and align other users to their pose.

Share an Anchor & Colocation Setup

To set up colocation, it is recommended to use Colocation Discovery since version 71 of the Meta XR SDK in Unity. This is a feature that allows users to advertise to and discover nearby users via Bluetooth. The class it belongs to is called OVRColocationSession. Typically, the advertising clients seek to act as the host for a multi-user experience, and the discovering clients are interested in joining a hosted experience.

Requirements

To enable Colocation Discovery, the Colocation Session Support on the OVR Manager needs to be Required.
Colocation Discovery requires one of the following configurations:
  • The user is a member of a verified developer team.
  • The user is a test user from the developer team owning the app.
  • The user is invited by a developer team to a release channel (except for production).
See Configurations for more information.
For shared spatial anchors, also the Shared Spatial Anchor Support permission needs to be set to “Required”. Furthermore, the device must be connected to the internet. Lastly, Enhanced Spatial Services must be enabled on the Meta Quest device.
To turn on Enhanced Spatial Services, go to Settings > Privacy and Safety > Device Permissions, and enable Enhanced Spatial Services. The app will detect when this setting is disabled and inform users to turn it on.

Colocation Discovery & Group-Based Anchor Sharing

As of v71, Spatial Anchor sharing and loading is groups instead of user based. This eliminates the need for users to be entitled to the app and therefore for developers to manage user IDs through their verified app on the Developer Dashboard. Instead, an arbitrary group UUID is used for sharing and loading anchors, making Group Sharing the recommended approach.
Before sharing a spatial anchor with a group, one of the participants, usually the host, must create a single UUID representing the group and communicate it to the others. The method of that communication can be either via an app-managed network connection, such as Unity Netcode or Photon Fusion, or via Colocation Discovery, which greatly reduces end-user friction around setting up colocated experiences.

📢 Colocation Discovery: Session Advertisement & Anchor Sharing

The group ID is automatically generated as the result of advertising. In the code below, the host starts the advertisement. After successful advertisement the user can read the group ID from the advertisement result.
var advertisementResult = await OVRColocationSession.StartAdvertisementAsync(null);
_groupId = advertisementResult.Value;

// Create and save anchor here
Instead of sending null in the StartAdvertisementAsync method, the host can also send some session information in the form of a string, such as a session name or simply a message to other users. We can send a maximum of 1024 bytes of data.
After creating and saving the spatial anchor as seen in the previous section, the host is then able to call the group-based function for sharing anchors.
OVRSpatialAnchor.ShareAsync(new List<OVRSpatialAnchor> { anchor }, _groupId);

🔍 Colocation Discovery: Session Discovery & Anchor Loading

After the host has started advertising the session and has successfully shared the anchor, including the created group ID, all other users are ready to discover the session. Users can subscribe to the OnColocationSessionDiscovered event and then start the Discovery with StartDiscoveryAsync.
OVRColocationSession.ColocationSessionDiscovered += OnColocationSessionDiscovered;
OVRColocationSession.StartDiscoveryAsync();
Once the colocation session has been discovered, the OnColocationSessionDiscovered callback is activated. We can then read the group ID from the discovered session and use it to load the anchor.
private void OnColocationSessionDiscovered(OVRColocationSession.Data sessionData)
{
    _groupId = session.AdvertisementUuid;
    // Load anchor here
}
The user can now load the anchor but this time the function is slightly different and is called LoadUnboundSharedAnchorsAsync and takes the group ID as input.
var unboundAnchors = new List<OVRSpatialAnchor.UnboundAnchor>();
await OVRSpatialAnchor.LoadUnboundSharedAnchorsAsync(_groupId, unboundAnchors);
After successfully loading, keep in mind to localize and bind the anchor to an OVRSpatialAnchor component as shown previously.

📐 Anchor Alignment

Next, the users are ready to be aligned to the anchor’s pose. This is necessary for a truly colocated experience and so that the users have the same tracking space as the host. To do this the app can simply adjust the position and rotation of the user's Camera Rig to the anchor’s pose.
cameraRig.Transform.position = anchor.Transform.InverseTransformPoint(Vector3.zero);
cameraRig.Transform.eulerAngles = new Vector3(0, -anchor.Transform.eulerAngles.y, 0);

This is all that is needed to create a seamless colocated experience in just a few lines of code.

Space Sharing

A popular use case when doing colocation is to also share the room layout with other users. With version 74 of the Meta XR SDK a powerful Space Sharing API has been introduced, making it extremely easy and seamless to share rooms across clients in a co-located experience.

Requirements and Limitations

  • Only the host needs to have scanned the room beforehand or at the start of the experience.
  • The APK must be uploaded to a release channel on the Developer Dashboard and all users or test users must be invited to that channel or be part of the team.
  • It is not possible to share a space between two devices with the same account. Therefore, there are two ways to test space sharing during development:
    • Use an additional device of a friend/colleague and share a space between the two. Make sure they are entitled to use the app.
    • Create a test user and log in with that test user on a second device. In this case, also make sure the test user email is invited to the release channel and entitled to use the app.

Space Sharing Setup

We can combine the Space Sharing API with Colocation Discovery, which makes this whole setup extremely straightforward. To share a room, all the host has to do is to talk to the MRUK singleton instance, get a list of MRUK rooms, and call the built-in ShareRoomsAsync method.
// For sharing multiple rooms
var rooms = MRUK.Instance.Rooms;
MRUK.Instance.ShareRoomsAsync(rooms, _groupId);

// For sharing a single (current) room
var room = MRUK.Instance.GetCurrentRoom();
room.ShareRoomAsync(_groupId);
Similarly, it is possible for the other users to load all rooms shared with the group ID and align themselves to the room’s floor world pose.
MRUK.Instance.LoadSceneFromSharedRooms(null, _groupId, alignmentData: (roomUuid, remoteFloorWorldPose));
For more detailed code and information on how to get all room information such as the floor world pose, check out the sample code in the SpaceSharingManager class of the Colocated Experiences MR Motif!

Troubleshooting Guide

Result Unsupported

This error can occur when using Colocation Discovery and indicates that the correct permissions are not set. Make sure to set the Colocation Session Support and Shared Spatial Anchor Support permissions to Required in the OVRManager.

Failure_GroupNotFound

This error can occur when using Space Sharing and indicates that no group ID can be found. Make sure to use the correct group ID when sharing and loading anchors, and that the group ID is actually shared with the other users trying to load the anchor. Sharing anchors between two apps with different package names is not supported.

Resources

Did you find this page helpful?