Tutorial: Schritt-für-Schritt-Anleitung zum Erstellen einer neuen HoloLens Unity-App mit Azure Spatial Anchors

In diesem Tutorial wird gezeigt, wie Sie mit Azure Spatial Anchors eine neue HoloLens Unity-App erstellen.

Voraussetzungen

Damit Sie dieses Tutorial ausführen können, benötigen Sie folgende Komponenten:

  1. PC: Ein PC, auf dem Windows ausgeführt wird
  2. Installation von Visual Studio: Visual Studio 2019 mit der Workload Entwicklung für die universelle Windows-Plattform und der Komponente Windows 10 SDK (10.0.18362.0 oder höher). Die C++/WinRT-Visual Studio-Erweiterung (VSIX) für Visual Studio sollte über den Visual Studio Marketplace installiert werden.
  3. HoloLens: Ein HoloLens-Gerät mit aktiviertem Entwicklermodus. Für diesen Artikel benötigen Sie ein HoloLens-Gerät mit dem Windows 10-Update vom Mai 2020. Öffnen Sie zum Aktualisieren auf das neueste HoloLens-Release die App Einstellungen, navigieren Sie zu Update und Sicherheit, und wählen Sie anschließend die Schaltfläche Nach Updates suchen.
  4. Unity - Unity 2020.3.25 mit den Modulen Universal Windows Platform Build Support und Windows Build Support (IL2CPP)

Erstellen und Einrichten eines Unity-Projekts

Erstellen eines neuen Projekts

  1. Wählen Sie in Unity Hub die Option Neues Projekt aus.
  2. Wählen Sie 3D aus.
  3. Geben Sie Ihren Projektnamen und einen Speicherort ein.
  4. Wählen Sie Projekt erstellen aus, und warten Sie, bis Unity Ihr Projekt erstellt hat.

Ändern der Buildplattform

  1. Wählen Sie in Ihrem Unity-Editor Datei>Buildeinstellungen aus.
  2. Wählen Sie Universelle Windows-Plattform und dann Plattform wechseln aus. Warten Sie, bis Unity die Verarbeitung aller Dateien abgeschlossen hat.

Importieren von ASA und OpenXR

  1. Starten Sie Mixed Reality-Featuretool.
  2. Wählen Sie Ihren Projektpfad aus – den Ordner, der Unterordner wie Assets, Packages, ProjectSettings usw. enthält – und wählen Sie Features ermitteln aus.
  3. Wählen Sie beide Optionen unter Azure Mixed Reality Sevices aus.
    1. Azure Spatial Anchors SDK-Kern
    2. Azure Spatial Anchors SDK für Windows
  4. Wählen Sie unter Plattformunterstützung folgende Option aus:
    1. Mixed Reality OpenXR-Plug-In

Hinweis

Stellen Sie sicher, dass Sie den Katalog aktualisiert haben und die neueste Version ausgewählt ist für jede

MRFT – Featureauswahl

  1. Klicken Sie auf Features abrufen ->Importieren ->Genehmigen ->Beenden.
  2. Wenn Sie Ihr Unity-Fenster wieder in den Eingabefokus setzen, beginnt Unity mit dem Importieren der Module.
  3. Wenn Sie eine Meldung zur Verwendung des neuen Eingabesystems erhalten, wählen Sie Ja aus, um Unity neu zu starten und die Back-Ends zu aktivieren.

Einrichten der Projekteinstellungen

Jetzt legen wir einige Unity-Projekteinstellungen fest, die uns dabei helfen, das Windows Holographic SDK für die Entwicklung zu verwenden.

Ändern der OpenXR-Einstellungen

  1. Wählen Sie Datei>Buildeinstellungen aus (es ist möglicherweise noch aus dem vorherigen Schritt geöffnet).
  2. Wählen Sie Playereinstellungen aus.
  3. Wählen Sie XR-Plug-In-Verwaltung aus.
  4. Stellen Sie sicher, dass die Registerkarte Universelle Windows-Plattform-Einstellungen aktiviert ist, und aktivieren Sie das Kontrollkästchen neben OpenXR und Microsoft HoloLens-Featuregruppe.
  5. Wählen Sie das gelbe Warnzeichen neben OpenXR aus, um alle OpenXR-Probleme anzuzeigen.
  6. Wählen Sie Alle korrigieren aus.
  7. Um das Problem Mindestens ein Interaktionsprofil muss hinzugefügt werden zu beheben, wählen Sie Bearbeiten aus, um die OpenXR Project-Einstellungen zu öffnen. Wählen Sie dann unter Interaktionsprofile das +-Symbol aus, und wählen Sie Microsoft-Handinteraktionsprofil aus.Unity – OpenXR-Setup

Ändern der Qualitätseinstellungen

  1. Wählen Sie Bearbeiten>Projekteinstellungen>Qualität aus.
  2. Wählen Sie in der Spalte unter dem Universelle Windows-Plattform-Logo den Pfeil in der Zeile Standard und dann Sehr niedrig aus. Sie wissen, dass die Einstellung ordnungsgemäß angewandt wurde, wenn das Feld in der Spalte Universelle Windows-Plattform und der Zeile Sehr niedrig grün ist.

Festlegen von Funktionen

  1. Wechseln Sie zu Bearbeiten>Projekteinstellungen>Player (eventuell ist „Player“ noch aus dem vorherigen Schritt geöffnet).
  2. Stellen Sie sicher, dass die Registerkarte Universelle Windows-Plattform-Einstellungen ausgewählt ist.
  3. Aktivieren Sie im Konfigurationsabschnitt Veröffentlichungseinstellungen Folgendes:
    1. InternetClient
    2. InternetClientServer
    3. PrivateNetworkClientServer
    4. SpatialPerception (möglicherweise bereits aktiviert)

Einrichten der Hauptkamera

  1. Wählen Sie im Hierarchiebereich die Option Hauptkamera aus.
  2. Legen Sie im Inspector die Transformationsposition auf 0,0,0 fest.
  3. Suchen Sie die Eigenschaft Flags löschen und ändern Sie das Dropdown von Skybox in Volltonfarbe.
  4. Wählen Sie das Feld Hintergrund aus, um den Farbwähler zu öffnen.
  5. Legen Sie R, G, B und A auf 0 fest.
  6. Wählen Sie unten Komponente hinzufügen aus, und fügen Sie die Komponente Treiber für Haltungsnachverfolgung der Kamera hinzu Unity – Kamerasetup.

Ausprobieren 1

Sie sollten nun über eine leere Szene verfügen, die für die Bereitstellung auf Ihrem HoloLens-Gerät bereit ist. Um zu testen, dass alles funktioniert, erstellen Sie Ihre App in Unity, und stellen Sie sie aus Visual Studio bereit. Befolgen Sie die Anleitung unter Verwenden von Visual Studio zum Bereitstellen und Debuggen. Es sollte der Unity-Startbildschirm und anschließend eine leere Anzeige angezeigt werden.

Erstellen einer Spatial Anchors-Ressource

Öffnen Sie das Azure-Portal.

Wählen Sie im linken Bereich Ressource erstellen aus.

Suchen Sie über das Suchfeld nach Spatial Anchors.

Screenshot: Ergebnisse einer Suche nach Spatial Anchors

Wählen Sie Spatial Anchors und dann Erstellen aus.

Führen Sie im Bereich Spatial Anchors-Konto die folgenden Schritte aus:

  • Geben Sie einen eindeutigen Ressourcennamen in regulären alphanumerischen Zeichen ein.

  • Wählen Sie das Abonnement aus, an das die Ressource angefügt werden soll.

  • Erstellen Sie eine Ressourcengruppe durch Auswählen von Neu erstellen. Nennen Sie sie myResourceGroup, und wählen Sie OK aus.

    Eine Ressourcengruppe ist ein logischer Container, in dem Azure-Ressourcen wie Web-Apps, Datenbanken und Speicherkonten bereitgestellt und verwaltet werden. Sie können z.B. die gesamte Ressourcengruppe später in einem einfachen Schritt löschen.

  • Wählen Sie einen Standort (Region) für die Ressource aus.

  • Wählen Sie Erstellen aus, um mit der Ressourcenerstellung zu beginnen.

Screenshot: Spatial Anchors-Bereich zum Erstellen einer Ressource

Nachdem die Ressource erstellt wurde, zeigt das Azure-Portal an, dass die Bereitstellung abgeschlossen ist.

Screenshot: Abgeschlossene Ressourcenbereitstellung

Wählen Sie Zu Ressource wechseln aus. Nun können Sie die Ressourceneigenschaften anzeigen.

Kopieren Sie den Wert für Konto-ID der Ressource zur späteren Verwendung in einen Text-Editor.

Screenshot des Bereichs „Ressourceneigenschaften“

Kopieren Sie außerdem den Wert für Kontodomäne der Ressource zur späteren Verwendung in einen Text-Editor.

Screenshot: Wert für „Kontodomäne“ der Ressource

Wählen Sie unter Einstellungen die Option Zugriffsschlüssel aus. Kopieren Sie die Werte für Primärschlüssel und Kontoschlüssel zur späteren Verwendung in einen Text-Editor.

Screenshot: Bereich „Schlüssel“ für das Konto

Erstellen und Hinzufügen von Skripts

  1. Erstellen Sie in Unity im Projektbereich einen neuen Ordner namens Skripts unter dem Ordner Ressourcen.
  2. Klicken Sie mit der rechten Maustaste in den Ordner, und klicken Sie auf Erstellen ->C#-Skript. Benennen Sie es mit AzureSpatialAnchorsScript.
  3. Wechseln Sie zu GameObject –>Leer erstellen.
  4. Wählen Sie es aus, und benennen Sie es im Inspektor von GameObject in AzureSpatialAnchors um.
  5. Führen Sie auf GameObject verbleibend Folgendes aus:
    1. Legen Sie seine Position auf 0,0,0 fest.
    2. Wählen Sie Komponente hinzufügen aus, suchen Sie nach AzureSpatialAnchorsScript, und fügen Sie es hinzu.
    3. Wählen Sie Komponente hinzufügen erneut aus, suchen Sie nach demAR-Anker-Manager, und fügen Sie ihn hinzu. Dadurch wird automatisch auch AR-Sitzungsursprung hinzugefügt.
    4. Wählen Sie Komponente hinzufügen erneut aus, suchen Sie nach dem SpatialAnchorManager-Skript, und fügen Sie es hinzu.
    5. Füllen Sie in der hinzugefügten SpatialAnchorManager-Komponente die Konto-ID, den Kontoschlüssel und die Kontodomäne aus, die Sie im vorherigen Schritt aus der Spatial Anchors-Ressource im Azure-Portal kopiert haben.

Unity – ASA GameObject

App-Übersicht

Unsere App unterstützt die folgenden Interaktionen:

Geste Aktion
Tippen auf eine beliebige Stelle Sitzung starten/fortsetzen + Anker an Handposition erstellen
Tippen auf einen Anker GameObject löschen + Anker im ASA-Clouddienst löschen
Tippen + Halten für 2 Sekunden (+ Sitzung wird ausgeführt) Sitzung beenden und alle GameObjects entfernen. Anker im ASA-Clouddienst beibehalten
Tippen + Halten für 2 Sekunden (+ Sitzung wird nicht ausgeführt) Sitzung starten und nach allen Ankern suchen.

Hinzufügen der Tipperkennung

Wir fügen unserem Skript nun Code hinzu, um die Tippbewegung eines Benutzers erkennen zu können.

  1. Öffnen Sie AzureSpatialAnchorsScript.cs in Visual Studio, indem Sie in Ihrem Unity-Projektbereich auf das Skript doppelklicken.
  2. Fügen Sie Ihrer Klasse das folgende Array hinzu.
public class AzureSpatialAnchorsScript : MonoBehaviour
{
    /// <summary>
    /// Used to distinguish short taps and long taps
    /// </summary>
    private float[] _tappingTimer = { 0, 0 };
  1. Fügen Sie die folgenden beiden Methoden unterhalb der Update()-Methode hinzu. Wir fügen die Implementierung zu einem späteren Zeitpunkt hinzu.
// Update is called once per frame
void Update()
{
}

/// <summary>
/// Called when a user is air tapping for a short time 
/// </summary>
/// <param name="handPosition">Location where tap was registered</param>
private async void ShortTap(Vector3 handPosition)
{
}

/// <summary>
/// Called when a user is air tapping for a long time (>=2 sec)
/// </summary>
private async void LongTap()
{
}
  1. Fügen Sie den folgenden Import hinzu.
using UnityEngine.XR;
  1. Fügen Sie der Update()-Methode den folgenden Code hinzu. Dadurch kann die App kurze und lange (2 Sek.) Handtippbewegungen erkennen.
// Update is called once per frame
void Update()
{

    //Check for any air taps from either hand
    for (int i = 0; i < 2; i++)
    {
        InputDevice device = InputDevices.GetDeviceAtXRNode((i == 0) ? XRNode.RightHand : XRNode.LeftHand);
        if (device.TryGetFeatureValue(CommonUsages.primaryButton, out bool isTapping))
        {
            if (!isTapping)
            {
                //Stopped Tapping or wasn't tapping
                if (0f < _tappingTimer[i] && _tappingTimer[i] < 1f)
                {
                    //User has been tapping for less than 1 sec. Get hand position and call ShortTap
                    if (device.TryGetFeatureValue(CommonUsages.devicePosition, out Vector3 handPosition))
                    {
                        ShortTap(handPosition);
                    }
                }
                _tappingTimer[i] = 0;
            }
            else
            {
                _tappingTimer[i] += Time.deltaTime;
                if (_tappingTimer[i] >= 2f)
                {
                    //User has been air tapping for at least 2sec. Get hand position and call LongTap
                    if (device.TryGetFeatureValue(CommonUsages.devicePosition, out Vector3 handPosition))
                    {
                        LongTap();
                    }
                    _tappingTimer[i] = -float.MaxValue; // reset the timer, to avoid retriggering if user is still holding tap
                }
            }
        }

    }
}

Hinzufügen und Konfigurieren von SpatialAnchorManager

Das ASA SDK bietet eine einfache Schnittstelle namens SpatialAnchorManager, um Aufrufe an den ASA-Dienst durchzuführen. Wir fügen sie als Variable dem AzureSpatialAnchorsScript.cs hinzu.

Fügen Sie zuerst den Import hinzu.

using Microsoft.Azure.SpatialAnchors.Unity;

Deklarieren Sie dann die Variable.

public class AzureSpatialAnchorsScript : MonoBehaviour
{
    /// <summary>
    /// Used to distinguish short taps and long taps
    /// </summary>
    private float[] _tappingTimer = { 0, 0 };

    /// <summary>
    /// Main interface to anything Spatial Anchors related
    /// </summary>
    private SpatialAnchorManager _spatialAnchorManager = null;

Weisen Sie in der Start()-Methode die Variable der Komponente zu, die wir in einem vorherigen Schritt hinzugefügt haben.

// Start is called before the first frame update
void Start()
{
    _spatialAnchorManager = GetComponent<SpatialAnchorManager>();
}

Um Debug- und Fehlerprotokolle zu erhalten, müssen wir die verschiedenen Rückrufe abonnieren.

// Start is called before the first frame update
void Start()
{
    _spatialAnchorManager = GetComponent<SpatialAnchorManager>();
    _spatialAnchorManager.LogDebug += (sender, args) => Debug.Log($"ASA - Debug: {args.Message}");
    _spatialAnchorManager.Error += (sender, args) => Debug.LogError($"ASA - Error: {args.ErrorMessage}");
}

Hinweis

Um die Protokolle anzeigen zu können, wählen Sie nach dem Erstellen des Projekts aus Unity und dem Öffnen der Visual Studio-Projektmappe .sln „Debuggen –> Mit Debuggen ausführen“ aus, und behalten Sie die Verbindung Ihres HoloLens-Geräts mit Ihrem Computer bei, während die App ausgeführt wird.

Starten der Sitzung

Um Anker zu erstellen und zu finden, müssen wir zuerst eine Sitzung starten. Beim Aufrufen von StartSessionAsync() erstellt SpatialAnchorManager bei Bedarf eine Sitzung und startet sie dann. Wir fügen dies unserer ShortTap()-Methode hinzu.

/// <summary>
/// Called when a user is air tapping for a short time 
/// </summary>
/// <param name="handPosition">Location where tap was registered</param>
private async void ShortTap(Vector3 handPosition)
{
    await _spatialAnchorManager.StartSessionAsync();
}

Erstellen des Ankers

Da nun eine Sitzung ausgeführt wird, können wir Anker erstellen. In dieser Anwendung möchten wir den erstellten Anker GameObjects und die erstellten Ankerbezeichner (Anker-IDs) nachverfolgen. Fügen Sie dem Code zwei Listen hinzu.

using Microsoft.Azure.SpatialAnchors.Unity;
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR;
    /// <summary>
    /// Main interface to anything Spatial Anchors related
    /// </summary>
    private SpatialAnchorManager _spatialAnchorManager = null;

    /// <summary>
    /// Used to keep track of all GameObjects that represent a found or created anchor
    /// </summary>
    private List<GameObject> _foundOrCreatedAnchorGameObjects = new List<GameObject>();

    /// <summary>
    /// Used to keep track of all the created Anchor IDs
    /// </summary>
    private List<String> _createdAnchorIDs = new List<String>();

Erstellen Sie eine Methode CreateAnchor, die einen Anker an einer durch ihren Parameter definierten Position erstellt.

using System.Threading.Tasks;
/// <summary>
/// Creates an Azure Spatial Anchor at the given position rotated towards the user
/// </summary>
/// <param name="position">Position where Azure Spatial Anchor will be created</param>
/// <returns>Async Task</returns>
private async Task CreateAnchor(Vector3 position)
{
    //Create Anchor GameObject. We will use ASA to save the position and the rotation of this GameObject.
}

Da Raumanker nicht nur über eine Position, sondern auch eine Drehung verfügen, legen wir die Drehung so fest, dass sie sich bei der Erstellung immer am HoloLens-Geräts ausrichtet.

/// <summary>
/// Creates an Azure Spatial Anchor at the given position rotated towards the user
/// </summary>
/// <param name="position">Position where Azure Spatial Anchor will be created</param>
/// <returns>Async Task</returns>
private async Task CreateAnchor(Vector3 position)
{
    //Create Anchor GameObject. We will use ASA to save the position and the rotation of this GameObject.
    if (!InputDevices.GetDeviceAtXRNode(XRNode.Head).TryGetFeatureValue(CommonUsages.devicePosition, out Vector3 headPosition))
    {
        headPosition = Vector3.zero;
    }

    Quaternion orientationTowardsHead = Quaternion.LookRotation(position - headPosition, Vector3.up);

}

Nachdem wir nun über die Position und Drehung des gewünschten Ankers verfügen, erstellen wir ein sichtbares GameObject. Beachten Sie, dass Spatial Anchors nicht voraussetzt, dass der Anker GameObject für den Endbenutzer sichtbar sein muss, da der Hauptzweck von Spatial Anchors die Bereitstellung eines gemeinsamen und persistenten Referenzrahmens ist. Für dieses Tutorial visualisieren wir die Anker als Würfel. Jeder Anker wird als weißer Würfel initialisiert, der sich nach dem erfolgreichen Erstellungsprozess in einen grünen Würfel verwandeln wird.

/// <summary>
/// Creates an Azure Spatial Anchor at the given position rotated towards the user
/// </summary>
/// <param name="position">Position where Azure Spatial Anchor will be created</param>
/// <returns>Async Task</returns>
private async Task CreateAnchor(Vector3 position)
{
    //Create Anchor GameObject. We will use ASA to save the position and the rotation of this GameObject.
    if (!InputDevices.GetDeviceAtXRNode(XRNode.Head).TryGetFeatureValue(CommonUsages.devicePosition, out Vector3 headPosition))
    {
        headPosition = Vector3.zero;
    }

    Quaternion orientationTowardsHead = Quaternion.LookRotation(position - headPosition, Vector3.up);

    GameObject anchorGameObject = GameObject.CreatePrimitive(PrimitiveType.Cube);
    anchorGameObject.GetComponent<MeshRenderer>().material.shader = Shader.Find("Legacy Shaders/Diffuse");
    anchorGameObject.transform.position = position;
    anchorGameObject.transform.rotation = orientationTowardsHead;
    anchorGameObject.transform.localScale = Vector3.one * 0.1f;

}

Hinweis

Wir verwenden einen älteren Shader, da er in einem standardmäßigen Unity-Build enthalten ist. Andere Shader als der Standardshader werden nur einbezogen, wenn sie manuell angegeben werden oder direkt Teil der Szene sind. Wenn ein Shader nicht einbezogen wird, und die Anwendung versucht, ihn zu rendern, resultiert daraus rosafarbenes Material.

Nun fügen wir die Spatial Anchor-Komponenten hinzu und konfigurieren sie. Wir setzen den Ablauf des Ankers auf 3 Tage nach seiner Erstellung fest. Danach wird er automatisch aus der Cloud gelöscht. Denken Sie daran, den Import hinzuzufügen.

using Microsoft.Azure.SpatialAnchors;
/// <summary>
/// Creates an Azure Spatial Anchor at the given position rotated towards the user
/// </summary>
/// <param name="position">Position where Azure Spatial Anchor will be created</param>
/// <returns>Async Task</returns>
private async Task CreateAnchor(Vector3 position)
{
    //Create Anchor GameObject. We will use ASA to save the position and the rotation of this GameObject.
    if (!InputDevices.GetDeviceAtXRNode(XRNode.Head).TryGetFeatureValue(CommonUsages.devicePosition, out Vector3 headPosition))
    {
        headPosition = Vector3.zero;
    }

    Quaternion orientationTowardsHead = Quaternion.LookRotation(position - headPosition, Vector3.up);

    GameObject anchorGameObject = GameObject.CreatePrimitive(PrimitiveType.Cube);
    anchorGameObject.GetComponent<MeshRenderer>().material.shader = Shader.Find("Legacy Shaders/Diffuse");
    anchorGameObject.transform.position = position;
    anchorGameObject.transform.rotation = orientationTowardsHead;
    anchorGameObject.transform.localScale = Vector3.one * 0.1f;

    //Add and configure ASA components
    CloudNativeAnchor cloudNativeAnchor = anchorGameObject.AddComponent<CloudNativeAnchor>();
    await cloudNativeAnchor.NativeToCloud();
    CloudSpatialAnchor cloudSpatialAnchor = cloudNativeAnchor.CloudAnchor;
    cloudSpatialAnchor.Expiration = DateTimeOffset.Now.AddDays(3);

}

Um einen Anker zu speichern, muss der Benutzer Umgebungsdaten sammeln.

/// <summary>
/// Creates an Azure Spatial Anchor at the given position rotated towards the user
/// </summary>
/// <param name="position">Position where Azure Spatial Anchor will be created</param>
/// <returns>Async Task</returns>
private async Task CreateAnchor(Vector3 position)
{
    //Create Anchor GameObject. We will use ASA to save the position and the rotation of this GameObject.
    if (!InputDevices.GetDeviceAtXRNode(XRNode.Head).TryGetFeatureValue(CommonUsages.devicePosition, out Vector3 headPosition))
    {
        headPosition = Vector3.zero;
    }

    Quaternion orientationTowardsHead = Quaternion.LookRotation(position - headPosition, Vector3.up);

    GameObject anchorGameObject = GameObject.CreatePrimitive(PrimitiveType.Cube);
    anchorGameObject.GetComponent<MeshRenderer>().material.shader = Shader.Find("Legacy Shaders/Diffuse");
    anchorGameObject.transform.position = position;
    anchorGameObject.transform.rotation = orientationTowardsHead;
    anchorGameObject.transform.localScale = Vector3.one * 0.1f;

    //Add and configure ASA components
    CloudNativeAnchor cloudNativeAnchor = anchorGameObject.AddComponent<CloudNativeAnchor>();
    await cloudNativeAnchor.NativeToCloud();
    CloudSpatialAnchor cloudSpatialAnchor = cloudNativeAnchor.CloudAnchor;
    cloudSpatialAnchor.Expiration = DateTimeOffset.Now.AddDays(3);

    //Collect Environment Data
    while (!_spatialAnchorManager.IsReadyForCreate)
    {
        float createProgress = _spatialAnchorManager.SessionStatus.RecommendedForCreateProgress;
        Debug.Log($"ASA - Move your device to capture more environment data: {createProgress:0%}");
    }

}

Hinweis

Ein HoloLens-Gerät kann möglicherweise bereits um den Anker herum erfasste Umgebungsdaten wiederverwenden, sodass IsReadyForCreate bereits beim ersten Mal Aufruf „true“ ist.

Nach der Vorbereitung des cloudbasierte Raumankers können wir den tatsächlich gespeicherten hier ausprobieren.

/// <summary>
/// Creates an Azure Spatial Anchor at the given position rotated towards the user
/// </summary>
/// <param name="position">Position where Azure Spatial Anchor will be created</param>
/// <returns>Async Task</returns>
private async Task CreateAnchor(Vector3 position)
{
    //Create Anchor GameObject. We will use ASA to save the position and the rotation of this GameObject.
    if (!InputDevices.GetDeviceAtXRNode(XRNode.Head).TryGetFeatureValue(CommonUsages.devicePosition, out Vector3 headPosition))
    {
        headPosition = Vector3.zero;
    }

    Quaternion orientationTowardsHead = Quaternion.LookRotation(position - headPosition, Vector3.up);

    GameObject anchorGameObject = GameObject.CreatePrimitive(PrimitiveType.Cube);
    anchorGameObject.GetComponent<MeshRenderer>().material.shader = Shader.Find("Legacy Shaders/Diffuse");
    anchorGameObject.transform.position = position;
    anchorGameObject.transform.rotation = orientationTowardsHead;
    anchorGameObject.transform.localScale = Vector3.one * 0.1f;

    //Add and configure ASA components
    CloudNativeAnchor cloudNativeAnchor = anchorGameObject.AddComponent<CloudNativeAnchor>();
    await cloudNativeAnchor.NativeToCloud();
    CloudSpatialAnchor cloudSpatialAnchor = cloudNativeAnchor.CloudAnchor;
    cloudSpatialAnchor.Expiration = DateTimeOffset.Now.AddDays(3);

    //Collect Environment Data
    while (!_spatialAnchorManager.IsReadyForCreate)
    {
        float createProgress = _spatialAnchorManager.SessionStatus.RecommendedForCreateProgress;
        Debug.Log($"ASA - Move your device to capture more environment data: {createProgress:0%}");
    }

    Debug.Log($"ASA - Saving cloud anchor... ");

    try
    {
        // Now that the cloud spatial anchor has been prepared, we can try the actual save here.
        await _spatialAnchorManager.CreateAnchorAsync(cloudSpatialAnchor);

        bool saveSucceeded = cloudSpatialAnchor != null;
        if (!saveSucceeded)
        {
            Debug.LogError("ASA - Failed to save, but no exception was thrown.");
            return;
        }

        Debug.Log($"ASA - Saved cloud anchor with ID: {cloudSpatialAnchor.Identifier}");
        _foundOrCreatedAnchorGameObjects.Add(anchorGameObject);
        _createdAnchorIDs.Add(cloudSpatialAnchor.Identifier);
        anchorGameObject.GetComponent<MeshRenderer>().material.color = Color.green;
    }
    catch (Exception exception)
    {
        Debug.Log("ASA - Failed to save anchor: " + exception.ToString());
        Debug.LogException(exception);
    }
}

Abschließend fügen wir der ShortTap-Methode den Funktionsaufruf hinzu.

/// <summary>
/// Called when a user is air tapping for a short time 
/// </summary>
/// <param name="handPosition">Location where tap was registered</param>
private async void ShortTap(Vector3 handPosition)
{
    await _spatialAnchorManager.StartSessionAsync();
        await CreateAnchor(handPosition);
}

Unsere App kann jetzt mehrere Anker erstellen. Jedes Gerät kann nun die erstellten Anker (sofern noch nicht abgelaufen) finden, solange es die Anker-IDs kennt und Zugriff auf dieselbe Spatial Anchors-Ressource in Azure hat.

Beenden der Sitzung und Zerstören der GameObjects

Um ein zweites Gerät zu emulieren, das alle Anker findet, beenden wir nun die Sitzung und entfernen alle Anker-GameObjects (wir behalten die Anker-IDs bei). Danach starten wir eine neue Sitzung und fragen die Anker mithilfe der gespeicherten Anker-IDs ab.

Der SpatialAnchorManager kann die Sitzung einfach durch Aufruf der DestroySession()-Methode beenden. Wir fügen dies unserer LongTap()-Methode hinzu.

/// <summary>
/// Called when a user is air tapping for a long time (>=2 sec)
/// </summary>
private async void LongTap()
{
        _spatialAnchorManager.DestroySession();
}

Erstellen Sie eine Methode, um alle Anker-GameObjects zu entfernen.

/// <summary>
/// Destroys all Anchor GameObjects
/// </summary>
private void RemoveAllAnchorGameObjects()
{
    foreach (var anchorGameObject in _foundOrCreatedAnchorGameObjects)
    {
        Destroy(anchorGameObject);
    }
    _foundOrCreatedAnchorGameObjects.Clear();
}

Rufen Sie sie nach dem Zerstören der Sitzung in LongTap() auf.

/// <summary>
/// Called when a user is air tapping for a long time (>=2 sec)
/// </summary>
private async void LongTap()
{
        // Stop Session and remove all GameObjects. This does not delete the Anchors in the cloud
        _spatialAnchorManager.DestroySession();
        RemoveAllAnchorGameObjects();
        Debug.Log("ASA - Stopped Session and removed all Anchor Objects");
}

Finden des Ankers

Wir versuchen nun erneut, die Anker mit der richtigen Position und Drehung zu finden, in der wir sie erstellt haben. Dazu müssen wir eine Sitzung starten und einen Watcher erstellen, der nach Ankern sucht, die den angegebenen Kriterien entsprechen. Als Kriterien stellen wir die IDs der zuvor erstellten Anker bereit. Erstellen Sie eine Methode LocateAnchor(), und verwenden Sie SpatialAnchorManager, um einen Watcher zu erstellen. Informationen zu anderen Suchstrategien als der Verwendung von Anker-IDs finden Sie unter Grundlegende Informationen zur AnchorLocateCriteria-Klasse.

/// <summary>
/// Looking for anchors with ID in _createdAnchorIDs
/// </summary>
private void LocateAnchor()
{
    if (_createdAnchorIDs.Count > 0)
    {
        //Create watcher to look for all stored anchor IDs
        Debug.Log($"ASA - Creating watcher to look for {_createdAnchorIDs.Count} spatial anchors");
        AnchorLocateCriteria anchorLocateCriteria = new AnchorLocateCriteria();
        anchorLocateCriteria.Identifiers = _createdAnchorIDs.ToArray();
        _spatialAnchorManager.Session.CreateWatcher(anchorLocateCriteria);
        Debug.Log($"ASA - Watcher created!");
    }
}

Sobald ein Watcher gestartet wurde, löst er einen Rückruf aus, wenn er einen Anker gefunden hat, der den angegebenen Kriterien entspricht. Erstellen Sie zunächst die „Anker gefunden“-Methode SpatialAnchorManager_AnchorLocated(), die so konfiguriert wird, dass sie aufgerufen wird, wenn der Watcher einen Anker gefunden hat. Mit dieser Methode wird ein Visual GameObject erstellt und ihm die native Ankerkomponente angefügt. Die native Ankerkomponente stellt sicher, dass die richtige Position und Drehung des GameObject festgelegt ist.

Ähnlich wie beim Erstellungsprozess wird der Anker einem GameObject angefügt. Dieses GameObject muss in Ihrer Szene nicht sichtbar sein, damit Raumanker funktionieren. Für dieses Tutorial visualisieren wir jeden Anker als blauen Würfel, sobald er gefunden wurde. Wenn Sie den Anker nur verwenden, um ein gemeinsames Koordinatensystem zu erstellen, ist es nicht notwendig, das erstellte GameObject zu visualisieren.

/// <summary>
/// Callback when an anchor is located
/// </summary>
/// <param name="sender">Callback sender</param>
/// <param name="args">Callback AnchorLocatedEventArgs</param>
private void SpatialAnchorManager_AnchorLocated(object sender, AnchorLocatedEventArgs args)
{
    Debug.Log($"ASA - Anchor recognized as a possible anchor {args.Identifier} {args.Status}");

    if (args.Status == LocateAnchorStatus.Located)
    {
        //Creating and adjusting GameObjects have to run on the main thread. We are using the UnityDispatcher to make sure this happens.
        UnityDispatcher.InvokeOnAppThread(() =>
        {
            // Read out Cloud Anchor values
            CloudSpatialAnchor cloudSpatialAnchor = args.Anchor;

            //Create GameObject
            GameObject anchorGameObject = GameObject.CreatePrimitive(PrimitiveType.Cube);
            anchorGameObject.transform.localScale = Vector3.one * 0.1f;
            anchorGameObject.GetComponent<MeshRenderer>().material.shader = Shader.Find("Legacy Shaders/Diffuse");
            anchorGameObject.GetComponent<MeshRenderer>().material.color = Color.blue;

            // Link to Cloud Anchor
            anchorGameObject.AddComponent<CloudNativeAnchor>().CloudToNative(cloudSpatialAnchor);
            _foundOrCreatedAnchorGameObjects.Add(anchorGameObject);
        });
    }
}

Nun abonnieren wir den AnchorLocated-Rückruf von SpatialAnchorManager, um sicherzustellen, dass unsere SpatialAnchorManager_AnchorLocated()-Methode aufgerufen wird, sobald der Watcher einen Anker findet.

// Start is called before the first frame update
void Start()
{
    _spatialAnchorManager = GetComponent<SpatialAnchorManager>();
    _spatialAnchorManager.LogDebug += (sender, args) => Debug.Log($"ASA - Debug: {args.Message}");
    _spatialAnchorManager.Error += (sender, args) => Debug.LogError($"ASA - Error: {args.ErrorMessage}");
    _spatialAnchorManager.AnchorLocated += SpatialAnchorManager_AnchorLocated;
}

Zum Schluss erweitern wir unsere LongTap()-Methode, um das Finden des Ankers einzubeziehen. Wir verwenden den booleschen Wert IsSessionStarted, um zu entscheiden, ob wir nach allen Ankern suchen oder alle Anker zerstören, wie in der App-Übersicht beschrieben.

/// <summary>
/// Called when a user is air tapping for a long time (>=2 sec)
/// </summary>
private async void LongTap()
{
    if (_spatialAnchorManager.IsSessionStarted)
    {
        // Stop Session and remove all GameObjects. This does not delete the Anchors in the cloud
        _spatialAnchorManager.DestroySession();
        RemoveAllAnchorGameObjects();
        Debug.Log("ASA - Stopped Session and removed all Anchor Objects");
    }
    else
    {
        //Start session and search for all Anchors previously created
        await _spatialAnchorManager.StartSessionAsync();
        LocateAnchor();
    }
}

Ausprobieren 2

Ihre App unterstützt jetzt das Erstellen und Suchen von Ankern. Erstellen Sie Ihre App in Unity, und stellen Sie sie von Visual Studio aus bereit, indem Sie Verwenden von Visual Studio zum Bereitstellen und Debuggen befolgen.

Vergewissern Sie sich, dass Ihr HoloLens-Gerät mit dem Internet verbunden ist. Sobald die App gestartet wurde und die Erstellt mit Unity-Meldung nicht mehr angezeigt wird, tippen Sie kurz in Ihre Umgebung. Ein weißer Würfel sollte die Position und Drehung des zu erstellenden Ankers anzeigen. Der Ankererstellungsprozess wird automatisch aufgerufen. Indem Sie sich langsam in Ihrer Umgebung umschauen, erfassen Sie Umgebungsdaten. Sobald genügend Umgebungsdaten gesammelt sind, versucht unsere App, einen Anker an der angegebenen Position zu erstellen. Sobald der Ankererstellungsprozess abgeschlossen ist, wird der Würfel grün. Überprüfen Sie in Ihren Debugprotokollen in Visual Studio, ob alles wie beabsichtigt funktioniert hat.

Tippen Sie lange, um alle GameObjects aus Ihrer Szene zu entfernen und die Raumankersitzung zu beenden.

Nachdem Ihre Szene gelöscht wurde, können Sie erneut lange tippen, um eine Sitzung zu starten und nach den Ankern zu suchen, die Sie zuvor erstellt haben. Sobald sie gefunden sind, werden sie durch blaue Würfel an ihrer Position und Drehung visualisiert. Diese Anker können (solange sie nicht abgelaufen sind) von jedem unterstützten Gerät gefunden werden, solange es über die richtigen Anker-IDs und Zugriff auf Ihre Raumankerressource verfügt.

Löschen des Ankers

Jetzt kann unsere App Anker erstellen und suchen. Während sie die GameObjects löscht, wird der Anker in der Cloud nicht gelöscht. Wir fügen die Funktionalität hinzu, um sie auch in der Cloud zu löschen, wenn Sie auf einen vorhandenen Anker tippen.

Wir fügen eine Methode DeleteAnchor hinzu, die ein GameObject empfängt. Dann verwenden wir den SpatialAnchorManager zusammen mit der CloudNativeAnchor-Komponente des Objekts, um das Löschen des Ankers in der Cloud anzufordern.

/// <summary>
/// Deleting Cloud Anchor attached to the given GameObject and deleting the GameObject
/// </summary>
/// <param name="anchorGameObject">Anchor GameObject that is to be deleted</param>
private async void DeleteAnchor(GameObject anchorGameObject)
{
    CloudNativeAnchor cloudNativeAnchor = anchorGameObject.GetComponent<CloudNativeAnchor>();
    CloudSpatialAnchor cloudSpatialAnchor = cloudNativeAnchor.CloudAnchor;

    Debug.Log($"ASA - Deleting cloud anchor: {cloudSpatialAnchor.Identifier}");

    //Request Deletion of Cloud Anchor
    await _spatialAnchorManager.DeleteAnchorAsync(cloudSpatialAnchor);

    //Remove local references
    _createdAnchorIDs.Remove(cloudSpatialAnchor.Identifier);
    _foundOrCreatedAnchorGameObjects.Remove(anchorGameObject);
    Destroy(anchorGameObject);

    Debug.Log($"ASA - Cloud anchor deleted!");
}

Um diese Methode von ShortTap aus aufzurufen, müssen wir ermitteln können, ob ein Tippen in der Nähe eines vorhandenen sichtbaren Ankers vorgekommen ist. Erstellen Sie hierfür eine Hilfsmethode.

using System.Linq;
/// <summary>
/// Returns true if an Anchor GameObject is within 15cm of the received reference position
/// </summary>
/// <param name="position">Reference position</param>
/// <param name="anchorGameObject">Anchor GameObject within 15cm of received position. Not necessarily the nearest to this position. If no AnchorObject is within 15cm, this value will be null</param>
/// <returns>True if a Anchor GameObject is within 15cm</returns>
private bool IsAnchorNearby(Vector3 position, out GameObject anchorGameObject)
{
    anchorGameObject = null;

    if (_foundOrCreatedAnchorGameObjects.Count <= 0)
    {
        return false;
    }

    //Iterate over existing anchor gameobjects to find the nearest
    var (distance, closestObject) = _foundOrCreatedAnchorGameObjects.Aggregate(
        new Tuple<float, GameObject>(Mathf.Infinity, null),
        (minPair, gameobject) =>
        {
            Vector3 gameObjectPosition = gameobject.transform.position;
            float distance = (position - gameObjectPosition).magnitude;
            return distance < minPair.Item1 ? new Tuple<float, GameObject>(distance, gameobject) : minPair;
        });

    if (distance <= 0.15f)
    {
        //Found an anchor within 15cm
        anchorGameObject = closestObject;
        return true;
    }
    else
    {
        return false;
    }
}

Wir können jetzt unsere ShortTap-Methode erweitern, um den DeleteAnchor-Aufruf einzubeziehen.

/// <summary>
/// Called when a user is air tapping for a short time 
/// </summary>
/// <param name="handPosition">Location where tap was registered</param>
private async void ShortTap(Vector3 handPosition)
{
    await _spatialAnchorManager.StartSessionAsync();
    if (!IsAnchorNearby(handPosition, out GameObject anchorGameObject))
    {
        //No Anchor Nearby, start session and create an anchor
        await CreateAnchor(handPosition);
    }
    else
    {
        //Delete nearby Anchor
        DeleteAnchor(anchorGameObject);
    }
}

Ausprobieren 3

Erstellen Sie Ihre App in Unity, und stellen Sie sie von Visual Studio aus bereit, indem Sie Verwenden von Visual Studio zum Bereitstellen und Debuggen befolgen.

Beachten Sie, dass die Position Ihrer Handberührungsbewegung in dieser App die Mitte Ihrer Hand ist und nicht die Fingerspitzen sind.

Wenn Sie auf einen Anker tippen, entweder erstellt (grün) oder gefunden (blau), wird eine Anforderung an den Raumankerdienst gesendet, um diesen Anker aus dem Konto zu entfernen. Beenden Sie die Sitzung (langes Tippen), und starten Sie die Sitzung erneut (langes Tippen), um nach allen Ankern zu suchen. Die gelöschten Anker werden nicht mehr gefunden.

Zusammenfügen der Bestandteile

Nachdem alle Einzelkomponenten miteinander kombiniert wurden, sollte die fertige AzureSpatialAnchorsScript-Klassendatei wie hier gezeigt aussehen. Sie können diese Datei als Referenz verwenden und mit Ihrer eigenen Datei vergleichen, um mögliche Diskrepanzen zu ermitteln.

Hinweis

Sie werden feststellen, dass wir [RequireComponent(typeof(SpatialAnchorManager))] in das Skript aufgenommen haben. So stellt Unity sicher, dass dem GameObject, dem wir AzureSpatialAnchorsScript anfügen, auch SpatialAnchorManager angefügt wird.

using Microsoft.Azure.SpatialAnchors;
using Microsoft.Azure.SpatialAnchors.Unity;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.XR;


[RequireComponent(typeof(SpatialAnchorManager))]
public class AzureSpatialAnchorsScript : MonoBehaviour
{
    /// <summary>
    /// Used to distinguish short taps and long taps
    /// </summary>
    private float[] _tappingTimer = { 0, 0 };

    /// <summary>
    /// Main interface to anything Spatial Anchors related
    /// </summary>
    private SpatialAnchorManager _spatialAnchorManager = null;

    /// <summary>
    /// Used to keep track of all GameObjects that represent a found or created anchor
    /// </summary>
    private List<GameObject> _foundOrCreatedAnchorGameObjects = new List<GameObject>();

    /// <summary>
    /// Used to keep track of all the created Anchor IDs
    /// </summary>
    private List<String> _createdAnchorIDs = new List<String>();

    // <Start>
    // Start is called before the first frame update
    void Start()
    {
        _spatialAnchorManager = GetComponent<SpatialAnchorManager>();
        _spatialAnchorManager.LogDebug += (sender, args) => Debug.Log($"ASA - Debug: {args.Message}");
        _spatialAnchorManager.Error += (sender, args) => Debug.LogError($"ASA - Error: {args.ErrorMessage}");
        _spatialAnchorManager.AnchorLocated += SpatialAnchorManager_AnchorLocated;
    }
    // </Start>

    // <Update>
    // Update is called once per frame
    void Update()
    {

        //Check for any air taps from either hand
        for (int i = 0; i < 2; i++)
        {
            InputDevice device = InputDevices.GetDeviceAtXRNode((i == 0) ? XRNode.RightHand : XRNode.LeftHand);
            if (device.TryGetFeatureValue(CommonUsages.primaryButton, out bool isTapping))
            {
                if (!isTapping)
                {
                    //Stopped Tapping or wasn't tapping
                    if (0f < _tappingTimer[i] && _tappingTimer[i] < 1f)
                    {
                        //User has been tapping for less than 1 sec. Get hand position and call ShortTap
                        if (device.TryGetFeatureValue(CommonUsages.devicePosition, out Vector3 handPosition))
                        {
                            ShortTap(handPosition);
                        }
                    }
                    _tappingTimer[i] = 0;
                }
                else
                {
                    _tappingTimer[i] += Time.deltaTime;
                    if (_tappingTimer[i] >= 2f)
                    {
                        //User has been air tapping for at least 2sec. Get hand position and call LongTap
                        if (device.TryGetFeatureValue(CommonUsages.devicePosition, out Vector3 handPosition))
                        {
                            LongTap();
                        }
                        _tappingTimer[i] = -float.MaxValue; // reset the timer, to avoid retriggering if user is still holding tap
                    }
                }
            }

        }
    }
    // </Update>


    // <ShortTap>
    /// <summary>
    /// Called when a user is air tapping for a short time 
    /// </summary>
    /// <param name="handPosition">Location where tap was registered</param>
    private async void ShortTap(Vector3 handPosition)
    {
        await _spatialAnchorManager.StartSessionAsync();
        if (!IsAnchorNearby(handPosition, out GameObject anchorGameObject))
        {
            //No Anchor Nearby, start session and create an anchor
            await CreateAnchor(handPosition);
        }
        else
        {
            //Delete nearby Anchor
            DeleteAnchor(anchorGameObject);
        }
    }
    // </ShortTap>

    // <LongTap>
    /// <summary>
    /// Called when a user is air tapping for a long time (>=2 sec)
    /// </summary>
    private async void LongTap()
    {
        if (_spatialAnchorManager.IsSessionStarted)
        {
            // Stop Session and remove all GameObjects. This does not delete the Anchors in the cloud
            _spatialAnchorManager.DestroySession();
            RemoveAllAnchorGameObjects();
            Debug.Log("ASA - Stopped Session and removed all Anchor Objects");
        }
        else
        {
            //Start session and search for all Anchors previously created
            await _spatialAnchorManager.StartSessionAsync();
            LocateAnchor();
        }
    }
    // </LongTap>

    // <RemoveAllAnchorGameObjects>
    /// <summary>
    /// Destroys all Anchor GameObjects
    /// </summary>
    private void RemoveAllAnchorGameObjects()
    {
        foreach (var anchorGameObject in _foundOrCreatedAnchorGameObjects)
        {
            Destroy(anchorGameObject);
        }
        _foundOrCreatedAnchorGameObjects.Clear();
    }
    // </RemoveAllAnchorGameObjects>

    // <IsAnchorNearby>
    /// <summary>
    /// Returns true if an Anchor GameObject is within 15cm of the received reference position
    /// </summary>
    /// <param name="position">Reference position</param>
    /// <param name="anchorGameObject">Anchor GameObject within 15cm of received position. Not necessarily the nearest to this position. If no AnchorObject is within 15cm, this value will be null</param>
    /// <returns>True if a Anchor GameObject is within 15cm</returns>
    private bool IsAnchorNearby(Vector3 position, out GameObject anchorGameObject)
    {
        anchorGameObject = null;

        if (_foundOrCreatedAnchorGameObjects.Count <= 0)
        {
            return false;
        }

        //Iterate over existing anchor gameobjects to find the nearest
        var (distance, closestObject) = _foundOrCreatedAnchorGameObjects.Aggregate(
            new Tuple<float, GameObject>(Mathf.Infinity, null),
            (minPair, gameobject) =>
            {
                Vector3 gameObjectPosition = gameobject.transform.position;
                float distance = (position - gameObjectPosition).magnitude;
                return distance < minPair.Item1 ? new Tuple<float, GameObject>(distance, gameobject) : minPair;
            });

        if (distance <= 0.15f)
        {
            //Found an anchor within 15cm
            anchorGameObject = closestObject;
            return true;
        }
        else
        {
            return false;
        }
    }
    // </IsAnchorNearby>
  
    // <CreateAnchor>
    /// <summary>
    /// Creates an Azure Spatial Anchor at the given position rotated towards the user
    /// </summary>
    /// <param name="position">Position where Azure Spatial Anchor will be created</param>
    /// <returns>Async Task</returns>
    private async Task CreateAnchor(Vector3 position)
    {
        //Create Anchor GameObject. We will use ASA to save the position and the rotation of this GameObject.
        if (!InputDevices.GetDeviceAtXRNode(XRNode.Head).TryGetFeatureValue(CommonUsages.devicePosition, out Vector3 headPosition))
        {
            headPosition = Vector3.zero;
        }

        Quaternion orientationTowardsHead = Quaternion.LookRotation(position - headPosition, Vector3.up);

        GameObject anchorGameObject = GameObject.CreatePrimitive(PrimitiveType.Cube);
        anchorGameObject.GetComponent<MeshRenderer>().material.shader = Shader.Find("Legacy Shaders/Diffuse");
        anchorGameObject.transform.position = position;
        anchorGameObject.transform.rotation = orientationTowardsHead;
        anchorGameObject.transform.localScale = Vector3.one * 0.1f;

        //Add and configure ASA components
        CloudNativeAnchor cloudNativeAnchor = anchorGameObject.AddComponent<CloudNativeAnchor>();
        await cloudNativeAnchor.NativeToCloud();
        CloudSpatialAnchor cloudSpatialAnchor = cloudNativeAnchor.CloudAnchor;
        cloudSpatialAnchor.Expiration = DateTimeOffset.Now.AddDays(3);

        //Collect Environment Data
        while (!_spatialAnchorManager.IsReadyForCreate)
        {
            float createProgress = _spatialAnchorManager.SessionStatus.RecommendedForCreateProgress;
            Debug.Log($"ASA - Move your device to capture more environment data: {createProgress:0%}");
        }

        Debug.Log($"ASA - Saving cloud anchor... ");

        try
        {
            // Now that the cloud spatial anchor has been prepared, we can try the actual save here.
            await _spatialAnchorManager.CreateAnchorAsync(cloudSpatialAnchor);

            bool saveSucceeded = cloudSpatialAnchor != null;
            if (!saveSucceeded)
            {
                Debug.LogError("ASA - Failed to save, but no exception was thrown.");
                return;
            }

            Debug.Log($"ASA - Saved cloud anchor with ID: {cloudSpatialAnchor.Identifier}");
            _foundOrCreatedAnchorGameObjects.Add(anchorGameObject);
            _createdAnchorIDs.Add(cloudSpatialAnchor.Identifier);
            anchorGameObject.GetComponent<MeshRenderer>().material.color = Color.green;
        }
        catch (Exception exception)
        {
            Debug.Log("ASA - Failed to save anchor: " + exception.ToString());
            Debug.LogException(exception);
        }
    }
    // </CreateAnchor>

    // <LocateAnchor>
    /// <summary>
    /// Looking for anchors with ID in _createdAnchorIDs
    /// </summary>
    private void LocateAnchor()
    {
        if (_createdAnchorIDs.Count > 0)
        {
            //Create watcher to look for all stored anchor IDs
            Debug.Log($"ASA - Creating watcher to look for {_createdAnchorIDs.Count} spatial anchors");
            AnchorLocateCriteria anchorLocateCriteria = new AnchorLocateCriteria();
            anchorLocateCriteria.Identifiers = _createdAnchorIDs.ToArray();
            _spatialAnchorManager.Session.CreateWatcher(anchorLocateCriteria);
            Debug.Log($"ASA - Watcher created!");
        }
    }
    // </LocateAnchor>

    // <SpatialAnchorManagerAnchorLocated>
    /// <summary>
    /// Callback when an anchor is located
    /// </summary>
    /// <param name="sender">Callback sender</param>
    /// <param name="args">Callback AnchorLocatedEventArgs</param>
    private void SpatialAnchorManager_AnchorLocated(object sender, AnchorLocatedEventArgs args)
    {
        Debug.Log($"ASA - Anchor recognized as a possible anchor {args.Identifier} {args.Status}");

        if (args.Status == LocateAnchorStatus.Located)
        {
            //Creating and adjusting GameObjects have to run on the main thread. We are using the UnityDispatcher to make sure this happens.
            UnityDispatcher.InvokeOnAppThread(() =>
            {
                // Read out Cloud Anchor values
                CloudSpatialAnchor cloudSpatialAnchor = args.Anchor;

                //Create GameObject
                GameObject anchorGameObject = GameObject.CreatePrimitive(PrimitiveType.Cube);
                anchorGameObject.transform.localScale = Vector3.one * 0.1f;
                anchorGameObject.GetComponent<MeshRenderer>().material.shader = Shader.Find("Legacy Shaders/Diffuse");
                anchorGameObject.GetComponent<MeshRenderer>().material.color = Color.blue;

                // Link to Cloud Anchor
                anchorGameObject.AddComponent<CloudNativeAnchor>().CloudToNative(cloudSpatialAnchor);
                _foundOrCreatedAnchorGameObjects.Add(anchorGameObject);
            });
        }
    }
    // </SpatialAnchorManagerAnchorLocated>

    // <DeleteAnchor>
    /// <summary>
    /// Deleting Cloud Anchor attached to the given GameObject and deleting the GameObject
    /// </summary>
    /// <param name="anchorGameObject">Anchor GameObject that is to be deleted</param>
    private async void DeleteAnchor(GameObject anchorGameObject)
    {
        CloudNativeAnchor cloudNativeAnchor = anchorGameObject.GetComponent<CloudNativeAnchor>();
        CloudSpatialAnchor cloudSpatialAnchor = cloudNativeAnchor.CloudAnchor;

        Debug.Log($"ASA - Deleting cloud anchor: {cloudSpatialAnchor.Identifier}");

        //Request Deletion of Cloud Anchor
        await _spatialAnchorManager.DeleteAnchorAsync(cloudSpatialAnchor);

        //Remove local references
        _createdAnchorIDs.Remove(cloudSpatialAnchor.Identifier);
        _foundOrCreatedAnchorGameObjects.Remove(anchorGameObject);
        Destroy(anchorGameObject);

        Debug.Log($"ASA - Cloud anchor deleted!");
    }
    // </DeleteAnchor>

}

Nächste Schritte

In diesem Tutorial haben Sie gelernt, wie Sie eine einfache Spatial Anchors-Anwendung für HoloLens mit Unity implementieren. Weitere Informationen zur Verwendung von Azure Spatial Anchors in einer neuen Android-App finden Sie im nächsten Tutorial.