How to Place Virtual Content Using a Device Map
This feature is experimental and may not work as expected. For more information about it, see the Device Mapping Feature page.
Now that we have created a device map and saved it to a file, we can read it to place virtual objects in the AR scene. In this how-to, we will cover reading a device map file into your Unity project and how to use it to place content.
Prerequisites
Before you start, complete How to Create a Device Map. You will need the Unity project from that tutorial for this one. You will also need one prefab asset to place as virtual content, such as a basic cube.
Placing Content on the Map
- 
Select DeviceMappingDemofrom the Hierarchy, then, in the Inspector, click Add Component. Search for and add a New Script to it, then name itTracker.cs.
- 
Open Tracker.csand replace its contents with the following snippet:using System.Collections;
 using System.IO;
 using Niantic.Lightship.AR.Mapping;
 using Niantic.Lightship.AR.PersistentAnchors;
 using UnityEngine;
 using UnityEngine.UI;
 using UnityEngine.XR.ARSubsystems;
 public class Tracker : MonoBehaviour
 {
 }
- 
In the Hierarchy, select the XROrigin, then, in the Inspector, click Add Component and add an ARPersistentAnchorManagerto it.
- 
Now that we have a persistent anchor manager, we can tell it what kind of mapping we want to use. Add the following snippet to the Trackerclass:// initialize the manager
 [SerializeField]
 private ARPersistentAnchorManager _persistentAnchorManager;
 private void Start()
 {
 // tell it we want device mapping (aka "slick" mapping)
 _persistentAnchorManager.DeviceMappingLocalizationEnabled = true;
 _persistentAnchorManager.CloudLocalizationEnabled = false;
 // enable continuous localization to mitigate tracking drift
 _persistentAnchorManager.ContinuousLocalizationEnabled = true;
 _persistentAnchorManager.TemporalFusionEnabled = true;
 _persistentAnchorManager.TransformUpdateSmoothingEnabled = true;
 _persistentAnchorManager.DeviceMappingLocalizationRequestIntervalSeconds = 0.1f;
 // start the manager with our options
 StartCoroutine(_persistentAnchorManager.RestartSubsystemAsyncCoroutine());
 }
- 
Add UI elements to the Trackerscript for choosing a device map:- Add a button to start and stop tracking:
- Right-click in the Hierarchy, then open the Create menu and select Button from the UI sub-menu. This will create a Canvas element and add the button to it.
- In the script, add a serialized field for the button.
 
- Create a private method, OnStartTrackingClicked(), and have it listen for button clicks inStart().
 [SerializeField]
 private Button _startTrackingButton;
 private void Start()
 {
 // Add this below the previous section's code in Start()
 // Listen for button click event
 _startTrackingButton.onClick.AddListener(OnStartTrackingClicked);
 }
 // When tracking button is clicked
 private void OnStartTrackingClicked()
 {
 }
- Add a button to start and stop tracking:
- 
Add logic to Tracker.csthat toggles the tracking state when the button is clicked:private ARPersistentAnchor _anchor;
 // variable for reference to the object on the map
 private GameObject _anchorVisualObject;
 private bool _isTrackingRunning;
 // When tracking button is clicked
 private void OnStartTrackingClicked()
 {
 var buttonText = _startTrackingButton.GetComponentInChildren<Text>();
 if (_isTrackingRunning)
 {
 // Stop tracking if running and clean up anchor
 if (_anchor)
 {
 _persistentAnchorManager.DestroyAnchor(_anchor);
 _anchor = null;
 _anchorVisualObject = null;
 }
 buttonText.text = "Start Tracking";
 _isTrackingRunning = false;
 }
 // otherwise, change the button to "stop" and start tracking
 else
 {
 buttonText.text = "Stop Tracking";
 _isTrackingRunning = true;
 StartCoroutine(RestartTracking());
 }
 }
- 
Add the next code snippet to restart tracking using the map from the file: - Add a serialized field for the ARDeviceMappingManager.
- We need to clean up some elements before restarting tracking:
- If there is already an anchor, destroy it. Wait a moment before continuing because the anchor can take an extra frame to be destroyed in some situations.
- Disable and re-enable ARPersistentAnchorManagerto restart tracking. We wait a moment between stopping and starting so that the location data and location manager data are cleaned up asynchronously.
 
- Once cleanup is done, we can create an ARDeviceMapby reading the saved serialized map data from the file we saved it into earlier.- Read the serialized map data into memory, then create a new ARDeviceMapand pass it toARDeviceMappingManager.SetDeviceMap()so that it will be tracked.
 
- Read the serialized map data into memory, then create a new 
- Call ARPersistentAnchorManager.TryTrackAnchor()to start tracking again using a new anchor.
 
- Add a serialized field for the 
- 
After adding this snippet to Tracker.cs, open the Hierarchy and select DeviceMappingDemo. Then, in the Inspector, findTracker.csand assign ARDeviceMappingManager from the XROrigin to the field in the script.[SerializeField]
 private ARDeviceMappingManager _deviceMappingManager;
 private IEnumerator RestartTracking()
 {
 if (_anchor)
 {
 _persistentAnchorManager.DestroyAnchor(_anchor);
 _anchor = null;
 _anchorVisualObject = null;
 }
 // wait a moment after destroying anchor
 yield return null;
 _persistentAnchorManager.enabled = false;
 // wait a moment before toggling tracking
 yield return null;
 _persistentAnchorManager.enabled = true;
 // Read a new device map from file
 var path = Path.Combine(Application.persistentDataPath, Mapper.MapFileName);
 var serializedDeviceMap = File.ReadAllBytes(path);
 var deviceMap = ARDeviceMap.CreateFromSerializedData(serializedDeviceMap);
 // Assign the device map to the mapping manager
 _deviceMappingManager.SetDeviceMap(deviceMap);
 // Set up tracking with a new anchor
 _persistentAnchorManager.TryTrackAnchor(
 new ARPersistentAnchorPayload(deviceMap.GetAnchorPayload()),
 out _anchor);
 }
- 
When the anchor enters the "tracking" state, place virtual objects according to the device map: - Create variables to hold the virtual content:
- Add a serialized field variable for the prefab, then assign it to yours.
 
- In Start(), subscribe an event listener to changes to the anchor state.
- Create a function to handle event updates:
- If the anchor is in the tracking state, instantiate an object from the prefab with the tracked anchor as its parent.
 
 [SerializeField]
 private GameObject _locationAnchorPrefab;
 private void Start()
 {
 // Add this below the previous section's code in Start()
 // Listen for anchor updates and catch them
 _persistentAnchorManager.arPersistentAnchorStateChanged += OnArPersistentAnchorStateChanged;
 }
 // Event listener for anchor updates
 private void OnArPersistentAnchorStateChanged(ARPersistentAnchorStateChangedEventArgs args)
 {
 if (args.arPersistentAnchor.trackingState == TrackingState.Tracking)
 {
 if (!_anchorVisualObject)
 {
 _anchorVisualObject = Instantiate(_locationAnchorPrefab, _anchor.transform);
 Debug.Log("Tracking");
 }
 }
 }
- Create variables to hold the virtual content:
Your virtual object should now be visible!
Completed Tracker Script
If you are having trouble with your tracking script, compare it to the finished product here!
Click to reveal the final Tracker.cs script
using System.Collections;
using System.IO;
using Niantic.Lightship.AR.Mapping;
using Niantic.Lightship.AR.PersistentAnchors;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.XR.ARSubsystems;
public class Tracker : MonoBehaviour
{
    // initialize the managers, prefab, menu, and map file I/O
    [SerializeField]
    private ARPersistentAnchorManager _persistentAnchorManager;
    [SerializeField]
    private ARDeviceMappingManager _deviceMappingManager;
    [SerializeField]
    private GameObject _locationAnchorPrefab;
    [SerializeField]
    private Button _startTrackingButton;
    private GameObject _anchorVisualObject;
    private ARPersistentAnchor _anchor;
    private bool _isTrackingRunning;
    private void Start()
    {
        // Event listeners for anchor and drop-down menu
        _persistentAnchorManager.arPersistentAnchorStateChanged += OnArPersistentAnchorStateChanged;
        _startTrackingButton.onClick.AddListener(OnStartTrackingClicked);
        // tell it we want device mapping (aka "slick" mapping)
        _persistentAnchorManager.DeviceMappingLocalizationEnabled = true;
        _persistentAnchorManager.CloudLocalizationEnabled = false;
        // enable continuous localization to mitigate tracking drift
        _persistentAnchorManager.ContinuousLocalizationEnabled = true;
        _persistentAnchorManager.TemporalFusionEnabled = true;
        _persistentAnchorManager.TransformUpdateSmoothingEnabled = true;
        _persistentAnchorManager.DeviceMappingLocalizationRequestIntervalSeconds = 0.1f;
        // restart the manager with our options
        StartCoroutine(_persistentAnchorManager.RestartSubsystemAsyncCoroutine());
    }
    // When tracking button is clicked
    private void OnStartTrackingClicked()
    {
        var buttonText = _startTrackingButton.GetComponentInChildren<Text>();
        if (_isTrackingRunning)
        {
            // Stop tracking if running
            if (_anchor)
            {
                _persistentAnchorManager.DestroyAnchor(_anchor);
                _anchor = null;
                _anchorVisualObject = null;
            }
            buttonText.text = "Start Tracking";
            _isTrackingRunning = false;
        }
        else
        {
            buttonText.text = "Stop Tracking";
            _isTrackingRunning = true;
            StartCoroutine(RestartTracking());
        }
    }
    private IEnumerator RestartTracking()
    {
        if (_anchor)
        {
            _persistentAnchorManager.DestroyAnchor(_anchor);
            _anchor = null;
            _anchorVisualObject = null;
        }
        yield return null;
        _persistentAnchorManager.enabled = false;
        // start tracking after stop tracking needs "some" time in between...
        yield return null;
        _persistentAnchorManager.enabled = true;
        // Read a new device map from file
        var path = Path.Combine(Application.persistentDataPath, Mapper.MapFileName);
        var serializedDeviceMap = File.ReadAllBytes(path);
        var deviceMap = ARDeviceMap.CreateFromSerializedData(serializedDeviceMap);
        // Assign the device map to the mapping manager
        _deviceMappingManager.SetDeviceMap(deviceMap);
        // Set up tracking with a new anchor
        _persistentAnchorManager.TryTrackAnchor(
            new ARPersistentAnchorPayload(deviceMap.GetAnchorPayload()),
            out _anchor);
    }
    // Event listener for anchor updates
    private void OnArPersistentAnchorStateChanged(ARPersistentAnchorStateChangedEventArgs args)
    {
        if (args.arPersistentAnchor.trackingState == TrackingState.Tracking)
        {
            if (!_anchorVisualObject)
            {
                _anchorVisualObject = Instantiate(_locationAnchorPrefab, _anchor.transform);
                Debug.Log("Tracking");
            }
        }
    }
}