Samouczek: instrukcje krok po kroku dotyczące tworzenia nowej aplikacji aparatu Unity dla urządzenia HoloLens przy użyciu usługi Azure Spatial Anchors

W tym samouczku pokazano, jak utworzyć nową aplikację aparatu Unity dla urządzenia HoloLens za pomocą usługi Azure Spatial Anchors.

Wymagania wstępne

Aby ukończyć kroki tego samouczka, upewnij się, że dysponujesz następującymi elementami:

  1. KOMPUTER — komputer z systemem Windows
  2. Program Visual Studio - 2019 zainstalowany z pakietem roboczym programowania platforma uniwersalna systemu Windows i składnikiem Windows 10 SDK (10.0.18362.0 lub nowszym). Powinno być zainstalowane rozszerzenie C++/WinRT Visual Studio Extension (VSIX) dla programu Visual Studio z usługi Visual Studio Marketplace.
  3. HoloLens — urządzenie HoloLens z włączonym trybem dewelopera. Ten artykuł wymaga urządzenia HoloLens z aktualizacją systemu Windows 10 maja 2020 r. Aby wykonać aktualizację do najnowszej wersji na urządzeniu HoloLens, otwórz aplikację Ustawienia, przejdź do pozycji Aktualizacja i zabezpieczenia, a następnie wybierz przycisk Sprawdź dostępność aktualizacji.
  4. Unity Unity - 2020.3.25 z modułami platforma uniwersalna systemu Windows Obsługa kompilacji i Obsługa kompilacji systemu Windows (IL2CPP)

Tworzenie i konfigurowanie projektu aparatu Unity

Create New Project (Azure Functions: utwórz nowy projekt)

  1. W usłudze Unity Hub wybierz pozycję Nowy projekt
  2. Wybierz pozycję 3D
  3. Wprowadź nazwę projektu i wprowadź lokalizację zapisu
  4. Wybierz pozycję Utwórz projekt i poczekaj na utworzenie projektu przez aparat Unity

Zmienianie platformy kompilacji

  1. W edytorze aparatu Unity wybierz pozycję Kompilacja pliku>Ustawienia
  2. Wybierz pozycję platforma uniwersalna systemu Windows, a następnie pozycję Przełącz platformę. Poczekaj, aż aparat Unity zakończy przetwarzanie wszystkich plików.

Importowanie usług ASA i OpenXR

  1. Uruchamianie narzędzia funkcji rzeczywistości mieszanej
  2. Wybierz ścieżkę projektu — folder zawierający foldery, takie jak zasoby, pakiety, projekt Ustawienia itd. i wybierz pozycję Odnajdź funkcje
  3. W obszarze Azure Mixed Reality Services wybierz obie opcje
    1. Azure Spatial Anchors SDK Core
    2. Zestaw SDK usługi Azure Spatial Anchors dla systemu Windows
  4. W obszarze Obsługa platformy wybierz pozycję
    1. Wtyczka OpenXR rzeczywistości mieszanej

Uwaga

Upewnij się, że został odświeżony wykaz, a dla każdego z nich wybrano najnowszą wersję

MRFT - Feature Selection

  1. Naciśnij pozycję Pobierz funkcje -->Import -->Zatwierdź -->Zakończ
  2. Podczas ponownego ustawiania fokosu okna aparatu Unity rozpocznie się importowanie modułów
  3. Jeśli zostanie wyświetlony komunikat o korzystaniu z nowego systemu wejściowego, wybierz pozycję Tak , aby ponownie uruchomić aparat Unity i włączyć zaplecza.

Konfigurowanie ustawień projektu

Teraz ustawimy niektóre ustawienia projektu aparatu Unity, które pomogą nam kierować zestaw Windows Holographic SDK do programowania.

Zmienianie Ustawienia OpenXR

  1. Wybierz pozycję Kompilacja pliku>Ustawienia (nadal może być otwarta w poprzednim kroku)
  2. Wybierz pozycję Odtwarzacz Ustawienia...
  3. Wybieranie funkcji zarządzania wtyczką XR
  4. Upewnij się, że karta platforma uniwersalna systemu Windows Ustawienia jest zaznaczona i zaznacz pole wyboru obok pozycji OpenXR i obok grupy funkcji Microsoft HoloLens
  5. Wybierz żółty znak ostrzegawczy obok pozycji OpenXR, aby wyświetlić wszystkie problemy z programem OpenXR .
  6. Wybierz pozycję Napraw wszystko
  7. Aby rozwiązać problem "Należy dodać co najmniej jeden profil interakcji", wybierz pozycję Edytuj , aby otworzyć ustawienia projektu OpenXR. Następnie w obszarze Profile interakcji wybierz + symbol i wybierz pozycję Profil interakcji z ręką firmy MicrosoftUnity - OpenXR Setup

Zmiana Ustawienia jakości

  1. Wybierz pozycję Edytuj>projekt Ustawienia> Właściwość
  2. W kolumnie pod logo platforma uniwersalna systemu Windows wybierz strzałkę w wierszu Domyślny i wybierz pozycję Bardzo niska. To ustawienie jest stosowane poprawnie, gdy pole w kolumnie platforma uniwersalna systemu Windows i bardzo niski wiersz jest zielony.

Ustawianie możliwości

  1. Przejdź do pozycji Edytuj>projekt Ustawienia> Player (nadal może być otwarty w poprzednim kroku).
  2. Upewnij się, że wybrano kartę platforma uniwersalna systemu Windows Ustawienia
  3. W sekcji Konfiguracja Ustawienia publikowania włącz następujące opcje
    1. InternetClient
    2. InternetClientServer
    3. PrivateNetworkClientServer
    4. SpatialPerception (może już być włączona)

Konfigurowanie głównego aparatu

  1. W panelu hierarchii wybierz pozycję Główny Aparat.
  2. W inspektorze ustaw jego pozycję przekształcenia na 0,0,0.
  3. Znajdź właściwość Clear Flags i zmień listę rozwijaną z Skybox na Solid Color.
  4. Wybierz pole Tło, aby otworzyć selektor kolorów.
  5. Ustaw wartości R, G, B i A na 0.
  6. Wybierz pozycję Dodaj składnik u dołu i dodaj składnik śledzonego sterownika pozy do aparatu Unity - Camera Setup

Wypróbuj plik #1

Teraz powinna istnieć pusta scena, która jest gotowa do wdrożenia na urządzeniu HoloLens. Aby sprawdzić, czy wszystko działa, skompiluj aplikację w środowisku Unity i wdróż ją z poziomu programu Visual Studio. Wykonaj czynności opisane w temacie Używanie programu Visual Studio, aby wdrożyć i debugować w tym celu. Powinien zostać wyświetlony ekran startowy aparatu Unity, a następnie jasny ekran.

Tworzenie zasobu usługi Spatial Anchors

Przejdź do portalu Azure Portal.

W okienku po lewej stronie wybierz pozycję Utwórz zasób.

Użyj pola wyszukiwania, aby wyszukać usługi Spatial Anchors.

Screenshot showing the results of a search for Spatial Anchors.

Wybierz pozycję Zakotwiczenia przestrzenne, a następnie wybierz pozycję Utwórz.

W okienku Konto usługi Spatial Anchors wykonaj następujące czynności:

  • Wprowadź unikatową nazwę zasobu przy użyciu zwykłych znaków alfanumerycznych.

  • Wybierz subskrypcję, do której chcesz dołączyć zasób.

  • Utwórz grupę zasobów, wybierając pozycję Utwórz nową. Nadaj jej nazwę myResourceGroup, a następnie wybierz przycisk OK.

    Grupa zasobów to logiczny kontener, w którym są wdrażane i zarządzane zasoby platformy Azure, takie jak aplikacje internetowe, bazy danych i konta magazynu. Na przykład można później usunąć całą grupę zasobów w jednym prostym kroku.

  • Wybierz lokalizację (region), w której ma być umieszczany zasób.

  • Wybierz pozycję Utwórz , aby rozpocząć tworzenie zasobu.

Screenshot of the Spatial Anchors pane for creating a resource.

Po utworzeniu zasobu witryna Azure Portal pokazuje, że wdrożenie zostało ukończone.

Screenshot showing that the resource deployment is complete.

Wybierz pozycję Przejdź do zasobu. Teraz możesz wyświetlić właściwości zasobu.

Skopiuj wartość identyfikatora konta zasobu do edytora tekstów do późniejszego użycia.

Screenshot of the resource properties pane.

Skopiuj również wartość domeny konta zasobu do edytora tekstów do późniejszego użycia.

Screenshot showing the resource's account domain value.

W obszarze Ustawienia wybierz pozycję Klucz dostępu. Skopiuj wartość Klucza podstawowego, Klucz konta, do edytora tekstów do późniejszego użycia.

Screenshot of the Keys pane for the account.

Tworzenie i dodawanie skryptów

  1. W środowisku Unity w okienku Projekt utwórz nowy folder o nazwie Skrypty w folderze Assets.
  2. W folderze kliknij prawym przyciskiem myszy pozycję ->Utwórz ->C# Script. Nadaj mu tytuł AzureSpatialAnchorsScript
  3. Przejdź do pozycji GameObject -> Create Empty (Utwórz pusty obiekt GameObject).
  4. Wybierz ją, a w inspektorzezmień jego nazwę z GameObject na AzureSpatialAnchors.
  5. Nadal na GameObject
    1. Ustaw pozycję na 0,0,0
    2. Wybierz pozycję Dodaj składnik i wyszukaj i dodaj skrypt AzureSpatialAnchorsScript
    3. Ponownie wybierz pozycję Dodaj składnik i wyszukaj i dodaj Menedżera kotwicy AR. Spowoduje to również automatyczne dodanie źródła sesji AR.
    4. Ponownie wybierz pozycję Dodaj składnik i wyszukaj i dodaj skrypt SpatialAnchorManager
    5. W dodanym składniku SpatialAnchorManager wypełnij pola Identyfikator konta, Klucz konta i Domena konta skopiowane w poprzednim kroku z zasobu zakotwiczenia przestrzennego w witrynie Azure Portal.

Unity - ASA GameObject

Omówienie aplikacji

Nasza aplikacja będzie obsługiwać następujące interakcje:

Gest Akcja
Naciśnij w dowolnym miejscu Rozpocznij/kontynuuj sesję + Utwórz kotwicę w pozycji dłoni
Naciśnięcie kotwicy Usuwanie GameObject i usuwanie kotwicy w usłudze ASA w chmurze
Naciśnij pozycję + Przytrzymaj przez 2 s (+ sesja jest uruchomiona) Zatrzymaj sesję i usuń wszystkie GameObjects. Utrzymywanie kotwic w usłudze ASA w chmurze
Naciśnij pozycję + Przytrzymaj przez 2 s (+ sesja nie jest uruchomiona) Uruchom sesję i poszukaj wszystkich kotwic.

Dodawanie rozpoznawania naciśnięcia

Dodajmy kod do naszego skryptu, aby móc rozpoznać gest naciśnięcia przez użytkownika.

  1. Otwórz AzureSpatialAnchorsScript.cs w programie Visual Studio, klikając dwukrotnie skrypt w okienku Projekt aparatu Unity.
  2. Dodaj następującą tablicę do klasy
public class AzureSpatialAnchorsScript : MonoBehaviour
{
    /// <summary>
    /// Used to distinguish short taps and long taps
    /// </summary>
    private float[] _tappingTimer = { 0, 0 };
  1. Dodaj następujące dwie metody poniżej metody Update(). Dodamy implementację na późniejszym etapie
// 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. Dodaj następujący import
using UnityEngine.XR;
  1. Dodaj następujący kod na początku Update() metody . Umożliwi to aplikacji rozpoznawanie krótkich i długich (2 s) gestów ręcznego dotykania
// 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
                }
            }
        }

    }
}

Dodawanie i konfigurowanie narzędzia SpatialAnchorManager

Zestaw ASA SDK oferuje prosty interfejs wywoływany SpatialAnchorManager w celu wykonania wywołań do usługi ASA. Dodajmy ją jako zmienną do naszej AzureSpatialAnchorsScript.cs

Najpierw dodaj import

using Microsoft.Azure.SpatialAnchors.Unity;

Następnie zadeklaruj zmienną

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;

W metodzie Start() przypisz zmienną do składnika dodanego w poprzednim kroku

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

Aby otrzymywać dzienniki debugowania i błędów, musimy zasubskrybować różne wywołania zwrotne

// 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}");
}

Uwaga

Aby wyświetlić dzienniki, upewnij się, że po skompilowaniu projektu z poziomu aparatu Unity otworzysz rozwiązanie .slnvisual studio, wybierz pozycję Debuguj —> uruchom polecenie Debuguj i pozostaw urządzenie HoloLens połączone z komputerem, gdy aplikacja jest uruchomiona.

Rozpocznij sesję

Aby utworzyć i znaleźć kotwice, najpierw musimy rozpocząć sesję. Podczas wywoływania StartSessionAsync()SpatialAnchorManager metody program utworzy sesję w razie potrzeby, a następnie uruchom ją. Dodajmy to do naszej ShortTap() metody.

/// <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();
}

Utwórz kotwicę

Teraz, gdy mamy uruchomioną sesję, możemy utworzyć kotwice. W tej aplikacji chcemy śledzić utworzoną kotwicę GameObjects i utworzone identyfikatory kotwicy (identyfikatory kotwic). Dodajmy dwie listy do naszego kodu.

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>();

Utwórzmy metodęCreateAnchor, która tworzy kotwicę na pozycji zdefiniowanej przez jego parametr.

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.
}

Ponieważ kotwice przestrzenne nie tylko mają położenie, ale także rotację, ustawmy rotację tak, aby zawsze orientowały się w kierunku urządzenia HoloLens podczas tworzenia.

/// <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);

}

Teraz, gdy mamy pozycję i rotację żądanej kotwicy, utwórzmy widoczny GameObjectelement . Należy pamiętać, że usługi Spatial Anchors nie wymagają, aby kotwica GameObject był widoczny dla użytkownika końcowego, ponieważ głównym celem usługi Spatial Anchors jest zapewnienie wspólnej i trwałej ramki odwołania. Na potrzeby tego samouczka zwizualizujemy kotwice jako moduły. Każda kotwica zostanie zainicjowana jako biały moduł, który zmieni się w zielony moduł po pomyślnym zakończeniu procesu tworzenia.

/// <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;

}

Uwaga

Używamy starszego cieniowania, ponieważ jest on uwzględniony w domyślnej kompilacji aparatu Unity. Inne cieniowania, takie jak domyślny cieniator, są uwzględniane tylko w przypadku ręcznego określenia lub są bezpośrednio częścią sceny. Jeśli cieniator nie jest dołączony i aplikacja próbuje go renderować, spowoduje to różowy materiał.

Teraz dodajmy i skonfigurujemy składniki usługi Spatial Anchor. Ustawiamy wygaśnięcie kotwicy na 3 dni od utworzenia kotwicy. Następnie zostaną one automatycznie usunięte z chmury. Pamiętaj, aby dodać import

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);

}

Aby zapisać kotwicę, użytkownik musi zbierać dane środowiska.

/// <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%}");
    }

}

Uwaga

Urządzenie HoloLens może prawdopodobnie ponownie używać już przechwyconych danych środowiska otaczających kotwicę, co powoduje IsReadyForCreate , że wartość true jest już po raz pierwszy wywoływana.

Teraz, gdy kotwica przestrzenna chmury została przygotowana, możemy wypróbować rzeczywiste zapisywanie tutaj.

/// <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);
    }
}

Na koniec dodajmy wywołanie funkcji do naszej ShortTap metody

/// <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);
}

Nasza aplikacja może teraz utworzyć wiele kotwic. Każde urządzenie może teraz zlokalizować utworzone kotwice (jeśli jeszcze nie wygasły), o ile znają identyfikatory kotwicy i mają dostęp do tego samego zasobu usługi Spatial Anchors na platformie Azure.

Zatrzymywanie sesji i niszczenie obiektów GameObject

Aby emulować drugie urządzenie wyszukujące wszystkie kotwice, teraz zatrzymamy sesję i usuniemy wszystkie zakotwiczone obiekty GameObjects (zachowamy identyfikatory kotwicy). Następnie rozpoczniemy nową sesję i wyślemy zapytanie do kotwic przy użyciu przechowywanych identyfikatorów kotwic.

SpatialAnchorManager może dbać o zatrzymanie sesji, po prostu wywołując jej DestroySession() metodę. Dodajmy to do naszej LongTap() metody

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

Utwórzmy metodę, aby usunąć całą kotwicę GameObjects

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

I wywołaj ją po zniszczeniu sesji w LongTap()

/// <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");
}

Znajdź kotwicę

Teraz spróbujemy ponownie znaleźć kotwice z prawidłową pozycją i rotacją, w której je utworzyliśmy. Aby to zrobić, musimy rozpocząć sesję i utworzyć element Watcher , który będzie szukać kotwic pasujących do podanych kryteriów. Jako kryteria przekażemy mu identyfikatory utworzonych wcześniej kotwic. Utwórzmy metodę LocateAnchor() i użyjmy metody SpatialAnchorManager , aby utworzyć element Watcher. Aby znaleźć strategie inne niż używanie identyfikatorów kotwic, zobacz Strategia lokalizowania zakotwiczenia

/// <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!");
    }
}

Po uruchomieniu obserwatora zostanie wyzwolony wywołanie zwrotne, gdy znajdzie kotwicę, która spełnia podane kryteria. Najpierw utwórzmy metodę zlokalizowaną na kotwicy o nazwie SpatialAnchorManager_AnchorLocated() , która zostanie skonfigurowana do wywołania, gdy obserwator znajduje kotwicę. Ta metoda utworzy wizualizację GameObject i dołączy do niej natywny składnik kotwicy. Natywny składnik kotwicy upewni się, że ustawiono prawidłową pozycję i rotację elementu GameObject .

Podobnie jak w przypadku procesu tworzenia, kotwica jest dołączona do obiektu GameObject. Ten obiekt GameObject nie musi być widoczny w scenie, aby kotwice przestrzenne działały. Na potrzeby tego samouczka zwizualizujemy każdą kotwicę jako niebieski moduł po ich zlokalizowaniu. Jeśli używasz kotwicy tylko do ustanowienia współużytkowanego systemu współrzędnych, nie ma potrzeby wizualizowania utworzonego obiektu GameObject.

/// <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);
        });
    }
}

Zasubskrybujmy teraz wywołanie zwrotne AnchorLocated, SpatialAnchorManager aby upewnić się, że nasza SpatialAnchorManager_AnchorLocated() metoda jest wywoływana po znalezieniu kotwicy przez obserwatora.

// 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;
}

Na koniec rozszerzmy naszą LongTap() metodę, aby uwzględnić znalezienie kotwicy. Użyjemy wartości logicznej IsSessionStarted , aby zdecydować, czy szukamy wszystkich kotwic lub zniszczymy wszystkie kotwice zgodnie z opisem w sekcji Przegląd aplikacji

/// <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();
    }
}

Wypróbuj plik #2

Aplikacja obsługuje teraz tworzenie kotwic i lokalizowanie ich. Skompiluj aplikację w środowisku Unity i wdróż ją z poziomu programu Visual Studio, korzystając z programu Visual Studio do wdrażania i debugowania.

Upewnij się, że urządzenie HoloLens jest połączone z Internetem. Po uruchomieniu aplikacji i zniknięciu komunikatu z aparatu Unity krótki naciśnięcie otoczenia. Powinien zostać wyświetlony biały moduł pokazujący położenie i obrót kotwicy do utworzenia. Proces tworzenia kotwicy jest wywoływany automatycznie. Gdy powoli patrzysz wokół otoczenia, przechwytujesz dane środowiska. Po zebraniu wystarczającej ilości danych środowiska nasza aplikacja spróbuje utworzyć kotwicę w określonej lokalizacji. Po zakończeniu procesu tworzenia kotwicy moduł zmieni kolor na zielony. Sprawdź dzienniki debugowania w programie Visual Studio, aby sprawdzić, czy wszystko działa zgodnie z oczekiwaniami.

Długie naciśnięcie, aby usunąć wszystkie GameObjects z sceny i zatrzymać sesję kotwicy przestrzennej.

Po wyczyszczonej scenie możesz ponownie nacisnąć długo, co spowoduje uruchomienie sesji i wyszukanie utworzonych wcześniej kotwic. Po znalezieniu są one wizualizowane przez niebieskie moduły w pozycji zakotwiczonej i rotacji. Te kotwice (o ile nie wygasły) można znaleźć na dowolnym obsługiwanym urządzeniu, o ile mają poprawne identyfikatory kotwic i mają dostęp do zasobu zakotwiczenia przestrzennego.

Usuń kotwicę

Teraz nasza aplikacja może tworzyć i lokalizować kotwice. GameObjectsUsunięcie elementu nie powoduje usunięcia kotwicy w chmurze. Dodajmy funkcjonalność, aby usunąć ją również w chmurze, jeśli naciśniesz istniejącą kotwicę.

Dodajmy metodę, która odbiera GameObjectelement DeleteAnchor . Następnie użyjemy SpatialAnchorManager elementu razem ze składnikiem obiektu CloudNativeAnchor , aby zażądać usunięcia kotwicy w chmurze.

/// <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!");
}

Aby wywołać tę metodę z ShortTapklasy , musimy mieć możliwość określenia, czy naciśnięcie było zbliżone do istniejącej widocznej kotwicy. Utwórzmy metodę pomocnika, która zajmuje się tym

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;
    }
}

Teraz możemy rozszerzyć naszą ShortTap metodę, aby uwzględnić wywołanie DeleteAnchor

/// <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);
    }
}

Wypróbuj plik 3

Skompiluj aplikację w środowisku Unity i wdróż ją z poziomu programu Visual Studio, korzystając z programu Visual Studio do wdrażania i debugowania.

Należy pamiętać, że lokalizacja gestu naciśnięcia ręki jest w środku ręki w tej aplikacji, a nie na czubku palców.

Po naciśnięciu do kotwicy utworzono (zielony) lub zlokalizowano (niebieski) żądanie jest wysyłane do usługi zakotwiczenia przestrzennego, aby usunąć tę kotwicę z konta. Zatrzymaj sesję (długie naciśnięcie) i ponownie uruchom sesję (długie naciśnięcie), aby wyszukać wszystkie kotwice. Usunięte kotwice nie będą już znajdować się.

Łączenie wszystkich elementów

Oto jak powinien wyglądać kompletny AzureSpatialAnchorsScript plik klasy, po zebraniu wszystkich różnych elementów. Możesz użyć go jako odwołania do porównania z własnym plikiem i dostrzec, jeśli mogą istnieć jakiekolwiek różnice w lewo.

Uwaga

Zauważysz, że dołączyliśmy [RequireComponent(typeof(SpatialAnchorManager))] do skryptu. Dzięki temu aparat Unity upewni się, że obiekt GameObject, do którego dołączymy AzureSpatialAnchorsScript , ma SpatialAnchorManager również dołączony obiekt.

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>

}

Następne kroki

W tym samouczku przedstawiono sposób implementowania podstawowej aplikacji usługi Spatial Anchors dla urządzenia HoloLens przy użyciu aparatu Unity. Aby dowiedzieć się więcej na temat korzystania z usługi Azure Spatial Anchors w nowej aplikacji systemu Android, przejdź do następnego samouczka.