Meta Spatial Anchors saving anchors

I am working on implementing Spatial Anchors in my app. I used this tutorial: Meta Quest Spatial Anchors Tutorial
In the tutorial he only shows how to spawn, save and load one prefab, but my app needs different prefabs.
I have figured out how to spawn in different prefabs with a Spatial Anchor component, but the saving is the thing I can’t figure out.

I have tried to find other tutorials, but they all only save one prefab.

Any ideas on how to do that or where to find more information on this?
Thank you in advance.

Anchor Loader script:

using System;
using UnityEngine;

public class AnchorLoader : MonoBehaviour
{
    private SpatialAnchorManager spatialAnchorManager;
    [SerializeField]
    private OVRSpatialAnchor anchorPrefab;
    Action<OVRSpatialAnchor.UnboundAnchor, bool> _onLoadAnchor;

    void Start() {
        spatialAnchorManager = GetComponent<SpatialAnchorManager>();
        _onLoadAnchor = OnLocalized;
    }

    public void LoadAnchorsByUuid() {
        if (!PlayerPrefs.HasKey(SpatialAnchorManager.NumUuidsPlayerPref)) {
            PlayerPrefs.SetInt(SpatialAnchorManager.NumUuidsPlayerPref, 0);
        }

        var playerUuidCount = PlayerPrefs.GetInt(SpatialAnchorManager.NumUuidsPlayerPref);

        if (playerUuidCount == 0) {
            Debug.Log("No anchors to load");
            return;
        }

        var uuids = new Guid[playerUuidCount];
        for (int i = 0; i < playerUuidCount; i++) {
            var uuidKey = "uuid" + i;
            var currentUuid = PlayerPrefs.GetString(uuidKey);

            uuids[i] = new Guid(currentUuid);
        }

        Load(new OVRSpatialAnchor.LoadOptions {
            Timeout = 0,
            StorageLocation = OVRSpace.StorageLocation.Local,
            Uuids = uuids
        });
        Debug.Log("Load Anchors by UUID method called");
    }

    private async void Load(OVRSpatialAnchor.LoadOptions options) {
        var anchors = await OVRSpatialAnchor.LoadUnboundAnchorsAsync(options);
    
        if (anchors == null) {
            return;
        }
    
        foreach (var anchor in anchors) {
            if (anchor.Localized) {
                _onLoadAnchor(anchor, true);
            } else if (!anchor.Localizing) {
                var result = await anchor.LocalizeAsync();
                _onLoadAnchor(anchor, result);
            }
        }
    }

    private void OnLocalized(OVRSpatialAnchor.UnboundAnchor unboundAnchor, bool success) {
        OVRSpatialAnchor prefab;
        if (spatialAnchorManager.GetAnchorPrefab() == null) {
            prefab = anchorPrefab;
        } else {
            prefab = spatialAnchorManager.GetAnchorPrefab();
        }

        if (!success) return;
        
        var pose = unboundAnchor.Pose;
        var spatialAnchor = Instantiate(prefab, pose.position, pose.rotation);
        unboundAnchor.BindTo(spatialAnchor);
        Debug.Log("Localized anchor with UUID: " + spatialAnchor.Uuid);        
    }
}

Spatial Anchor Manager script:

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SpatialAnchorManager : MonoBehaviour
{
    public static SpatialAnchorManager instance;

    private OVRSpatialAnchor anchorPrefab;
    public const string NumUuidsPlayerPref = "numUuids";
    [SerializeField]
    private List<GameObject> spatialAnchorPrefabs;
    private List<OVRSpatialAnchor> anchors = new List<OVRSpatialAnchor>();
    private OVRSpatialAnchor lastCreatedAnchor;
    private AnchorLoader anchorLoader;

    void Awake() {
        if (instance == null) {
            instance = this;
        }
    }
    void Start() {
        anchorLoader = GetComponent<AnchorLoader>();
        LoadSavedAnchors();
    }

    public void CreateSpatialAnchor(int prefabIndex) {
        var position = new Vector3(OVRInput.GetLocalControllerPosition(OVRInput.Controller.RTouch).x, 0, OVRInput.GetLocalControllerPosition(OVRInput.Controller.RTouch).z);
        var rotation = Quaternion.Euler(0, OVRInput.GetLocalControllerRotation(OVRInput.Controller.RTouch).eulerAngles.y, 0);
        
        GameObject instance = Instantiate(spatialAnchorPrefabs[prefabIndex], position, rotation);
        anchorPrefab = instance.AddComponent<OVRSpatialAnchor>();

        StartCoroutine(AnchorCreated(anchorPrefab));
    }

    private IEnumerator AnchorCreated(OVRSpatialAnchor workingAnchor) {
        while (!workingAnchor.Created && !workingAnchor.Localized) {
            yield return new WaitForEndOfFrame();
        }
        
        Guid anchorGuid = workingAnchor.Uuid;
        anchors.Add(workingAnchor);
        lastCreatedAnchor = workingAnchor;

        Debug.Log("Created anchor with UUID: " + anchorGuid);
    }

    public async void SaveLastCreatedAnchor() {
        if (lastCreatedAnchor == null) {
            Debug.Log("No anchor to save");
            return;
        }
        var success = await lastCreatedAnchor.SaveAsync();
    
        if (success) {
            Debug.Log("Saved anchor with UUID: " + lastCreatedAnchor.Uuid);
        } else {
            Debug.Log("Failed to save anchor");
        }
        SaveUuidToPlayerPrefs(lastCreatedAnchor.Uuid);
    }

    void SaveUuidToPlayerPrefs(Guid uuid) {
        if (!PlayerPrefs.HasKey(NumUuidsPlayerPref)) {
            PlayerPrefs.SetInt(NumUuidsPlayerPref, 0);
        }

        int playerNumUuids = PlayerPrefs.GetInt(NumUuidsPlayerPref);
        PlayerPrefs.SetString("uuid" + playerNumUuids, uuid.ToString());
    }

    public async void UnSaveLastCreatedAnchor() {
        var success = await lastCreatedAnchor.EraseAsync();

        if (success) {
            Debug.Log("Unsaved anchor with UUID: " + lastCreatedAnchor.Uuid);
            Destroy(lastCreatedAnchor.gameObject);
        } else {
            Debug.Log("Failed to unsave anchor");
        }
    }

    public void UnsaveAllAnchors() {
        foreach (var anchor in anchors) {
            UnsaveAnchor(anchor);
        }
        
        anchors.Clear();
        ClearAllUuidsFromPlayerPrefs();
        Debug.Log("Unsaved all anchors method called");
    }

    private async void UnsaveAnchor(OVRSpatialAnchor anchor) {
        await anchor.EraseAsync();
    
        Debug.Log("Unsaved anchor with UUID: " + anchor.Uuid);
    }

    private void ClearAllUuidsFromPlayerPrefs() {
        if (PlayerPrefs.HasKey(NumUuidsPlayerPref)) {
            int playerNumUuids = PlayerPrefs.GetInt(NumUuidsPlayerPref);
            for (int i = 0; i < playerNumUuids; i++) {
                PlayerPrefs.DeleteKey("uuid" + i);
            }
            PlayerPrefs.DeleteKey(NumUuidsPlayerPref);
            PlayerPrefs.Save();
        }
    }

    public void LoadSavedAnchors() {
        anchorLoader.LoadAnchorsByUuid();   
    }

    public OVRSpatialAnchor GetAnchorPrefab() {
        return anchorPrefab;
    }
}

I am also desperate about this topic.
I just cannot understand how nobody explains anything about this, after all… who wants to just spawn the same prefab on and on??

So I figured it out myself. There is probably a better way to do it, but this is the best I could come up with.

I created a new script: Spatial Anchor Save Data to hold the necessary data, in this case Anchor Guid and Name. This script is also responsible for turning those variables into a string and back into the class. I was looking for a way to do this without using MonoBehaviour, but I couldn’t figure it out.
In the inspector I added the class to every prefab that I am going to spawn in and set the name in the inspector as well. If you want to set the name via the script I think you can do that in the Spatial Anchor Manager script on line 48, just add saveData.Name = workingAnchor.name and I think it should work.

Next in the method SaveLastCreatedAnchor I get the component Spatial Anchor Save Data from the lastCreatedAnchor and send that to the method SaveUuidToPlayerPrefs which turns the Spatial Anchor Save Data into a string and saves it in PlayerPrefs.

In the Anchor Loader script the method LoadAnchorsByUuid gets the string from PlayerPrefs and, via the CreateString method in Spatial Anchor Save Data, turns it back into the class and gets the AnchorUuid. Afterwards I destroy the saveData GameObject, this is necessary because the class derives from MonoBehaviour and otherwise you can’t instantiate it. This is not ideal and I am looking for a different way of doing it.

For getting the correct prefab, I created two methods: string GetAnchorNameByUuid(Guid anchorUuid) and GameObject GetAnchorByName(string name). The first one returns the name of the prefab by comparing the Guid passed to the method with the Guid in Player.Prefs. The second method returns the GameObject associated with the name by comparing the name passed to the method with the name of the prefab in the SpatialAnchorPrefabs list.
I use these methods in the OnLocalized method to set the anchorPrefab to the correct one and instantiate it. If the variable from the method is null, it sets anchorPrefab to the defaultPrefab which is set in the inspector.

And that is basically it. It took me quite a while to figure this out so I am happy that it finally worked.
If anyone has any suggestions to improve the code or do it some other way feel free to leave a comment.

Anchor Loader script

using System;
using System.Collections.Generic;
using UnityEngine;

public class AnchorLoader : MonoBehaviour
{
    [SerializeField]
    private OVRSpatialAnchor defaultAnchor;
    private Action<bool, OVRSpatialAnchor.UnboundAnchor> _onLocalized;

    private int _playerUuidCount;
    
    void Start() {
        _onLocalized = OnLocalized;
    }

    public void LoadAnchorsByUuid() {
        if (!PlayerPrefs.HasKey(SpatialAnchorManager.NumUuidsPlayerPref)) {
            PlayerPrefs.SetInt(SpatialAnchorManager.NumUuidsPlayerPref, 0);
        }

        _playerUuidCount = PlayerPrefs.GetInt(SpatialAnchorManager.NumUuidsPlayerPref);
        if (_playerUuidCount == 0) {
            Debug.Log("No anchors to load");
            return;
        }

        var uuids = new Guid[_playerUuidCount];
        for (int i = 0; i < _playerUuidCount; i++) {
            var playerPrefs = PlayerPrefs.GetString("uuid" + i);
            var saveData = SpatialAnchorSaveData.CreateFromString(playerPrefs);
            uuids[i] = saveData.AnchorUuid;

            Destroy(saveData.gameObject);
        }
        Load(uuids);
    }

    private async void Load(IEnumerable<Guid> uuids) {
        var unboundAnchors = new List<OVRSpatialAnchor.UnboundAnchor>();
        var result = await OVRSpatialAnchor.LoadUnboundAnchorsAsync(uuids, unboundAnchors);
    
        if (result.Success) {
            Debug.Log("Anchors loaded successfully.");

            foreach (var anchor in result.Value) {
                anchor.LocalizeAsync().ContinueWith(_onLocalized, anchor);
            }
        } else {
            Debug.LogError($"Failed to load anchors: {result.Status}");
        }
    }

    private void OnLocalized(bool success, OVRSpatialAnchor.UnboundAnchor unboundAnchor) {
        if (!success) return;

        string name = GetAnchorNameByUuid(unboundAnchor.Uuid);
        GameObject getAnchor = GetAnchorByName(name);
        OVRSpatialAnchor anchorPrefab;

        if (getAnchor != null) {
            anchorPrefab = getAnchor.GetComponent<OVRSpatialAnchor>();
        } else {
            anchorPrefab = defaultAnchor;
        }
        
        if (unboundAnchor.TryGetPose(out Pose pose)) {
            OVRSpatialAnchor spatialAnchor = Instantiate(anchorPrefab, pose.position, pose.rotation);

            unboundAnchor.BindTo(spatialAnchor);
            Debug.Log("Localized anchor with UUID: " + spatialAnchor.Uuid + " and name: " + name);

            SpatialAnchorManager.instance.anchors.Add(spatialAnchor);
        } else {
            Debug.LogError("Failed to get pose for unbound anchor with UUID: " + unboundAnchor.Uuid);
        }      
    }

    private GameObject GetAnchorByName(string name) {
        foreach (var anchor in SpatialAnchorManager.instance.SpatialAnchorPrefabs) {
            var anchorData = anchor.GetComponent<SpatialAnchorSaveData>();
            if (anchorData.Name.Contains(name)) {
                return anchor;
            }
        }
        return null;
    }

    private string GetAnchorNameByUuid(Guid anchorUuid) {
        string name;
        for (int i = 0; i < _playerUuidCount; i++) {
            var anchorData = PlayerPrefs.GetString("uuid" + i);
            var saveData = SpatialAnchorSaveData.CreateFromString(anchorData);
            if (saveData.AnchorUuid == anchorUuid) {
                name = saveData.Name;
                Destroy(saveData.gameObject);
                return name;
            }
        }
        return null;
    }
}

Spatial Anchor Manager script

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SpatialAnchorManager : MonoBehaviour
{
    public static SpatialAnchorManager instance;

    private AnchorLoader anchorLoader;
    public const string NumUuidsPlayerPref = "numUuids";

    public List<GameObject> SpatialAnchorPrefabs;
    public List<OVRSpatialAnchor> anchors = new();

    private OVRSpatialAnchor lastCreatedAnchor;

    void Awake() {
        if (instance == null) {
            instance = this;
        }
    }

    void Start() {
        anchorLoader = GetComponent<AnchorLoader>();
    }

    public void CreateSpatialAnchor(int prefabIndex) {
        var position = new Vector3(OVRInput.GetLocalControllerPosition(OVRInput.Controller.RTouch).x, 0, OVRInput.GetLocalControllerPosition(OVRInput.Controller.RTouch).z);
        var rotation = Quaternion.Euler(0, OVRInput.GetLocalControllerRotation(OVRInput.Controller.RTouch).eulerAngles.y, 0);
        
        GameObject workingAnchor = Instantiate(SpatialAnchorPrefabs[prefabIndex], position, rotation);        

        StartCoroutine(AnchorCreated(workingAnchor.GetComponent<OVRSpatialAnchor>()));
    }

    private IEnumerator AnchorCreated(OVRSpatialAnchor workingAnchor) {
        while (!workingAnchor.Created && !workingAnchor.Localized) {
            yield return new WaitForEndOfFrame();
        }
        
        Guid anchorGuid = workingAnchor.Uuid;
        anchors.Add(workingAnchor);

        var saveData = workingAnchor.GetComponent<SpatialAnchorSaveData>();
        saveData.AnchorUuid = anchorGuid;

        lastCreatedAnchor = workingAnchor;

        Debug.Log("Created anchor with UUID: " + anchorGuid);
    }

    public async void SaveLastCreatedAnchor() {
        if (lastCreatedAnchor == null) {
            Debug.Log("No anchor to save");
            return;
        }
        var result = await lastCreatedAnchor.SaveAnchorAsync();
    
        if (result.Success) {
            Debug.Log("Saved anchor with UUID: " + lastCreatedAnchor.Uuid);
            var saveData = lastCreatedAnchor.GetComponent<SpatialAnchorSaveData>();
            SaveUuidToPlayerPrefs(saveData);
        } else {
            Debug.Log("Failed to save anchor");
        }
    }

    void SaveUuidToPlayerPrefs(SpatialAnchorSaveData data) {
        if (!PlayerPrefs.HasKey(NumUuidsPlayerPref)) {
            PlayerPrefs.SetInt(NumUuidsPlayerPref, 0);
            Debug.Log("Save: NumUuidsPlayerPref not found, creating new one");
        }
        
        int playerNumUuids = PlayerPrefs.GetInt(NumUuidsPlayerPref);
        PlayerPrefs.SetString("uuid" + playerNumUuids, data.ToString());
        Debug.Log("Saved UUID to player prefs: " + data.ToString() + " with key: " + "uuid" + playerNumUuids);
        PlayerPrefs.SetInt(NumUuidsPlayerPref, ++playerNumUuids);
    }

    public async void UnSaveLastCreatedAnchor() {
        if (lastCreatedAnchor == null) {
            Debug.Log("No anchor to unsave");
            return;
        }

        var result = await lastCreatedAnchor.EraseAnchorAsync();

        if (result.Success) {
            Debug.Log("Unsaved anchor with UUID: " + lastCreatedAnchor.Uuid);
            anchors.Remove(lastCreatedAnchor);
            Destroy(lastCreatedAnchor.gameObject);
        } else {
            Debug.Log("Failed to unsave anchor");
        }
    }

    public void UnsaveAllAnchors() {
        foreach (var anchor in anchors) {
            if (anchor == null) continue;
            UnsaveAnchor(anchor);
            Destroy(anchor.gameObject);
        }
        
        anchors.Clear();
        ClearAllUuidsFromPlayerPrefs();
        Debug.Log("Unsaved all anchors method called");
    }

    private async void UnsaveAnchor(OVRSpatialAnchor anchor) {
        await anchor.EraseAnchorAsync();
    
        Debug.Log("Unsaved anchor with UUID: " + anchor.Uuid);
    }

    private void ClearAllUuidsFromPlayerPrefs() {
        if (PlayerPrefs.HasKey(NumUuidsPlayerPref)) {
            int playerNumUuids = PlayerPrefs.GetInt(NumUuidsPlayerPref);
            for (int i = 0; i < playerNumUuids; i++) {
                PlayerPrefs.DeleteKey("uuid" + i);
            }
            PlayerPrefs.DeleteKey(NumUuidsPlayerPref);
            PlayerPrefs.Save();
        }
    }

    public void LoadSavedAnchors() {
        anchorLoader.LoadAnchorsByUuid();   
    }
}

Spatial Anchor Save Data script

using System;
using UnityEngine;

public class SpatialAnchorSaveData : MonoBehaviour
{
    public Guid AnchorUuid;
    public string Name;

    public override string ToString() {
        return $"{AnchorUuid}, {Name}";
    }

    public static SpatialAnchorSaveData CreateFromString(string data)
    {
        GameObject tempGameObject = new("TempSaveData");
        var saveData = tempGameObject.AddComponent<SpatialAnchorSaveData>();
        saveData.FromString(data);
        return saveData;
    }

    public SpatialAnchorSaveData FromString(string data) {
        var parts = data.Split(new[] { ", " }, StringSplitOptions.None);
        if (parts.Length != 2) {
            throw new FormatException("Invalid data format");
        }
        AnchorUuid = new Guid(parts[0]);
        Name = parts[1];
        return this;
    }
}