البرنامج التعليمي: إرشادات خطوة بخطوة لإنشاء تطبيق جديد HoloLens Unity باستخدام Azure Spatial Anchors

هذا البرنامج التعليمي سوف تظهر لك كيفية إنشاء التطبيق الوحدة HoloLens جديدة مع Azure Spatial Anchors.

المتطلبات الأساسية

لإكمال هذا البرنامج التعليمي، تأكد من أن لديك ما يلي:

  1. كمبيوتر شخصي - كمبيوتر يعمل بنظام Windows
  2. تم تثبيت Visual Studio - Visual Studio 2019 مع حمل عمل تطوير النظام الأساسي العام لـ Windows ومكون Windows 10 SDK (10.0.18362.0 أو أحدث). يجب تثبيت ملحق C++/WinRT Visual Studio (VSIX) لـ Visual Studio من موقع تسوق Visual Studio.
  3. HoloLens - جهاز HoloLens مع تمكين وضع المطور. تتطلب هذه المقالة جهاز HoloLens مع تحديث مايو 2020 لـ Windows 10. لتحديث أحدث إصدار على HoloLens، افتح تطبيق الإعدادات، انتقل إلى التحديث والأمانثم حدد زر تحقق من وجود تحديثات.
  4. Unity - Unity 2020.3.25 مع وحدات نمطية النظام الأساسي العام لـ Windows دعم البناء ودعم إصدار Windows (IL2CPP)

إنشاء مشروع Unity وإعداده

إنشاء مشروع جديد

  1. في Unity Hub، حدد New project
  2. تحديد ثلاثي الأبعاد
  3. أدخل اسم المشروع وأدخل حفظ الموقع
  4. حدد Create project وانتظر Unity لإنشاء مشروعك

تغيير النظام الأساسي للبناء

  1. في محرر الوحدة، حدد File>Build الإعدادات
  2. حدد النظام الأساسي العام لـ Windows ثم تبديل النظام الأساسي. انتظر حتى تنتهي Unity من معالجة جميع الملفات.

استيراد ASA وOpenXR

  1. تشغيل أداة ميزة الحقيقة المختلطة
  2. حدد مسار المشروع - المجلد الذي يحتوي على مجلدات مثل الأصول والحزم والمشروع الإعدادات وما إلى ذلك - وحدد اكتشاف الميزات
  3. ضمن Azure Mixed Reality Services، حدد كليهما
    1. Azure Spatial Anchors SDK Core
    2. Azure Spatial Anchors SDK لنظام التشغيل Windows
  4. ضمن دعم النظام الأساسي، حدد
    1. المكون الإضافي ل Mixed Reality OpenXR

إشعار

تأكد من تحديث الكتالوج وتحديد الإصدار الأحدث لكل

MRFT - Feature Selection

  1. اضغط على Get Features -->Import -->Approve -->Exit
  2. عند إعادة تركيز نافذة Unity الخاصة بك، ستبدأ Unity في استيراد الوحدات النمطية
  3. إذا تلقيت رسالة حول استخدام نظام الإدخال الجديد، فحدد نعم لإعادة تشغيل Unity وتمكين الخلفيات.

إعداد إعدادات المشروع

سنقوم الآن بتعيين بعض إعدادات مشروع الوحدة التي تساعدنا على استهداف Windows Holographic SDK للتنمية.

تغيير الإعدادات OpenXR

  1. حدد File>Build الإعدادات (قد يظل مفتوحا من الخطوة السابقة)
  2. حدد Player الإعدادات...
  3. حدد XR Plug-in Management
  4. تأكد من تحديد علامة التبويب النظام الأساسي العام لـ Windows الإعدادات وحدد المربع بجوار OpenXR وبالتالي مجموعة ميزات Microsoft HoloLens
  5. حدد علامة التحذير الصفراء بجوار OpenXR لعرض جميع مشكلات OpenXR.
  6. حدد إصلاح الكل
  7. لإصلاح المشكلة "يجب إضافة ملف تعريف تفاعل واحد على الأقل"، حدد تحرير لفتح إعدادات مشروع OpenXR. ثم ضمن ملفات تعريف التفاعل حدد + الرمز وحدد Microsoft Hand Interaction ProfileUnity - OpenXR Setup

تغيير الإعدادات الجودة

  1. حدد تحرير> إعدادات Project>جودة
  2. في العمود أسفل شعار النظام الأساسي العام لـ Windows، حدد السهم في الصف الافتراضي وحدد منخفض جدا. ستعرف أن الإعداد يتم تطبيقه بشكل صحيح عندما يكون المربع الموجود في العمود النظام الأساسي العام لـ Windows والصف منخفض جدا باللون الأخضر.

تعيين القدرات

  1. انتقل إلى تحرير>Project الإعدادات> Player (قد يظل مفتوحا من الخطوة السابقة).
  2. تأكد من تحديد علامة التبويب النظام الأساسي العام لـ Windows الإعدادات
  3. في قسم Publishing الإعدادات Configuration، قم بتمكين ما يلي
    1. InternetClient
    2. InternetClientServer
    3. PrivateNetworkClientServer
    4. SpatialPerception (قد يكون ممكنا بالفعل)

إعداد الكاميرا الرئيسية

  1. في لوحة التسلسل الهرمي، حدد الكاميرا الرئيسية.
  2. في المفتش، تعيين موقفها تحويل إلى 0،0،0.
  3. البحث عن الخاصية مسح الأعلام، وتغيير القائمة المنسدلة من Skybox إلى لون خالص.
  4. حدد الحقل Background لفتح منتقي الألوان.
  5. تعيين R، G، B، و A إلى 0.
  6. حدد Add Component في الأسفل وأضف Tracked Pose Driver Component إلى الكاميرا Unity - Camera Setup

جربه #1

يجب أن يكون لديك الآن مشهد فارغ جاهز للنشر على جهاز HoloLens. لاختبار عمل كل شيء، قم ببناء التطبيق الخاص بك في Unity وانشره من Visual Studio. اتبع استخدام Visual Studio للنشر والتصحيح للقيام بذلك. يجب أن تشاهد شاشة بدء الوحدة، ثم شاشة عرض واضحة.

إنشاء مورد Spatial Anchors

انتقل إلى مدخل Azure.

في الجزء الأيسر، حدد Create a resource.

استخدم مربع البحث للبحث عن Spatial Anchors.

Screenshot showing the results of a search for Spatial Anchors.

حدد Spatial Anchors، ثم حدد Create.

في جزء Spatial Anchors Account قم بتنفيذ ما يلي:

  • أدخل اسماً فريداً للمورد باستخدام أحرف أبجدية رقمية عادية.

  • قم بتحديد الاشتراك الذي تريد إرفاق المورد به.

  • إنشاء مجموعة الموارد عن طريق تحديد Create new. قم بتسميتها myResourceGroup، ثم حدد OK.

    مجموعة الموارد هي حاوية منطقية يتم فيها نشر موارد Azure وإدارتها، مثل تطبيقات الويب وقواعد البيانات وحسابات التخزين. على سبيل المثال، يمكنك اختيار حذف مجموعة الموارد بأكملها في خطوة واحدة بسيطة لاحقاً.

  • قم بتحديد موقع (منطقة) لوضع المورد فيه.

  • حدد Create لبدء إنشاء المورد.

Screenshot of the Spatial Anchors pane for creating a resource.

بعد إنشاء المورد، يظهر مدخل Microsoft Azure اكتمال عملية النشر.

Screenshot showing that the resource deployment is complete.

حدِّد الانتقال إلى المورد. يمكنك الآن أن تعرض خصائص المورد.

نسخ قيمة Account ID للمورد إلى محرر النص لاستخدامها لاحقا.

Screenshot of the resource properties pane.

أيضاً قم بنسخ قيمة Account Domain للمورد إلى محرر النص لاستخدامها لاحقا.

Screenshot showing the resource's account domain value.

ضمن "Settings"، حددAccess key. نسخ قيمتي Primary key وAccount Key إلى محرر النص لاستخدامهما لاحقا.

Screenshot of the Keys pane for the account.

إنشاء البرامج النصية وإضافتها

  1. في Unity في جزء Project، أنشئ مجلدا جديدا يسمى Scripts، في مجلد Assets .
  2. في المجلد انقر بزر الماوس الأيمن فوق ->Create ->C# Script. عنوانه AzureSpatialAnchorsScript
  3. انتقل إلى GameObject ->Create Empty.
  4. حدده، وفي Inspector أعد تسميته من GameObject إلى AzureSpatialAnchors.
  5. لا يزال على GameObject
    1. تعيين موضعه إلى 0,0,0
    2. حدد Add Component وابحث عن AzureSpatialAnchorsScript وأضفه
    3. حدد Add Component مرة أخرى وابحث عن AR Anchor Manager وأضفه. سيؤدي ذلك إلى إضافة أصل جلسة عمل AR تلقائيا أيضا.
    4. حدد Add Component مرة أخرى وابحث عن البرنامج النصي SpatialAnchorManager وأضفه
    5. في المكون SpatialAnchorManager المضاف، املأ معرف الحساب ومفتاح الحساب ومجال الحساب الذي نسخته في الخطوة السابقة من مورد المراسي المكانية في مدخل Microsoft Azure.

Unity - ASA GameObject

نظرة عامة على التطبيق

سيدعم تطبيقنا التفاعلات التالية:

إيماءة الإجراء
اضغط على أي مكان بدء/متابعة جلسة العمل + إنشاء ارتساء في موضع اليد
الضغط على نقطة ارتساء حذف GameObject + حذف ارتساء في خدمة ASA السحابية
اضغط على + تعليق لمدة ثانية (+ جلسة قيد التشغيل) أوقف جلسة العمل وأزل الكل GameObjects. الاحتفاظ بالارتساء في خدمة ASA السحابية
اضغط على + تعليق لمدة 2 ثانية (+ جلسة العمل غير قيد التشغيل) ابدأ جلسة العمل وابحث عن جميع نقاط الارتساء.

إضافة التعرف على اللمس

دعونا نضيف بعض التعليمات البرمجية إلى البرنامج النصي الخاص بنا لكي نتمكن من التعرف على إيماءة لمس المستخدم.

  1. افتح AzureSpatialAnchorsScript.cs في Visual Studio بالنقر نقرا مزدوجا فوق البرنامج النصي في جزء Unity Project.
  2. إضافة الصفيف التالي إلى الفئة الخاصة بك
public class AzureSpatialAnchorsScript : MonoBehaviour
{
    /// <summary>
    /// Used to distinguish short taps and long taps
    /// </summary>
    private float[] _tappingTimer = { 0, 0 };
  1. أضف الأسلوبين التاليين أسفل أسلوب Update(). سنضيف التنفيذ في مرحلة لاحقة
// 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. إضافة الاستيراد التالي
using UnityEngine.XR;
  1. أضف التعليمات البرمجية Update() التالية أعلى الأسلوب. سيسمح هذا للتطبيق بالتعرف على إيماءات اللمس باليد القصيرة والطويلة (2 ثانية)
// 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
                }
            }
        }

    }
}

إضافة وتكوين SpatialAnchorManager

تقدم ASA SDK واجهة بسيطة تسمى SpatialAnchorManager لإجراء مكالمات إلى خدمة ASA. دعونا نضيفه كمتغير إلى AzureSpatialAnchorsScript.cs

إضافة الاستيراد أولا

using Microsoft.Azure.SpatialAnchors.Unity;

ثم قم بتعريف المتغير

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;

في Start() الأسلوب ، قم بتعيين المتغير إلى المكون الذي أضفناه في خطوة سابقة

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

من أجل تلقي سجلات التصحيح والخطأ، نحتاج إلى الاشتراك في عمليات رد الاتصال المختلفة

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

إشعار

لعرض السجلات تأكد بعد إنشاء المشروع من Unity وفتح حل .slnvisual studio ، حدد Debug --> Run with Debugging واترك HoloLens متصلا بالكمبيوتر أثناء تشغيل التطبيق.

بدء جلسة العمل

لإنشاء نقاط ارتساء والعثور عليها، علينا أولا بدء جلسة عمل. عند استدعاء StartSessionAsync()، SpatialAnchorManager سيتم إنشاء جلسة عمل إذا لزم الأمر ثم بدء تشغيلها. دعونا نضيف هذا إلى أسلوبنا 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();
}

إنشاء ارتساء

الآن بعد أن أصبح لدينا جلسة تشغيل يمكننا إنشاء نقاط ارتساء. في هذا التطبيق، نود تتبع الارتساء GameObjects الذي تم إنشاؤه ومعرفات الارتساء التي تم إنشاؤها (معرفات الارتساء). دعونا نضيف قائمتين إلى التعليمات البرمجية الخاصة بنا.

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

دعونا ننشئ أسلوبا CreateAnchor ينشئ ارتساء في موضع محدد بواسطة المعلمة الخاصة به.

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

نظرا لأن نقاط الارتساء المكانية ليس لها موضع فحسب، بل أيضا دوران، دعنا نضبط الاستدارة لتوجيه دائما نحو HoloLens عند الإنشاء.

/// <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. لاحظ أن Spatial Anchors لا تتطلب أن تكون المرساة GameObject مرئية للمستخدم النهائي لأن الغرض الرئيسي من Spatial Anchors هو توفير إطار مرجعي شائع ومستمر. لغرض هذا البرنامج التعليمي، سوف نتصور المراسي كمكعبات. سيتم تهيئة كل مرساة كمكعب أبيض ، والذي سيتحول إلى مكعب أخضر بمجرد نجاح عملية الإنشاء.

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

}

إشعار

نحن نستخدم أداة التحكم بوحدات البكلور القديمة، نظرا لأنها مضمنة في بنية Unity افتراضية. يتم تضمين أدوات التحكم الأخرى مثل أداة التحكم بوحدات البكمل الافتراضية فقط إذا تم تحديدها يدويا أو كانت جزءا مباشرا من المشهد. إذا لم يتم تضمين أداة التحكم بوحدات البكوتر وكان التطبيق يحاول عرضها، فسينتج عن ذلك مادة وردية اللون.

الآن دعونا نضيف مكونات Spatial Anchor ونكونها. نحن نضبط انتهاء صلاحية الارتساء على 3 أيام من إنشاء الارتساء. بعد ذلك سيتم حذفها تلقائيا من السحابة. تذكر إضافة الاستيراد

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

}

لحفظ نقطة ارتساء، يجب على المستخدم جمع بيانات البيئة.

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

}

إشعار

يمكن ل HoloLens إعادة استخدام بيانات البيئة الملتقطة بالفعل المحيطة بالارتساء، مما IsReadyForCreate يؤدي إلى أن تكون صحيحة بالفعل عند استدعاؤها لأول مرة.

الآن بعد أن تم إعداد ارتساء السحابة المكاني، يمكننا تجربة الحفظ الفعلي هنا.

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

وأخيرا دعونا نضيف استدعاء الدالة إلى أسلوبنا 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();
        await CreateAnchor(handPosition);
}

يمكن لتطبيقنا الآن إنشاء نقاط ارتساء متعددة. يمكن لأي جهاز الآن تحديد موقع نقاط الارتساء التي تم إنشاؤها (إذا لم تنته صلاحيتها بعد) طالما أنهم يعرفون معرفات الارتساء ولديهم حق الوصول إلى نفس مورد Spatial Anchors على Azure.

إيقاف الجلسة وتدمير GameObjects

لمحاكاة جهاز ثان للعثور على جميع نقاط الارتساء، سنقوم الآن بإيقاف الجلسة وإزالة جميع الإشارات المرجعية GameObjects (سنحتفظ ب معرفات الارتساء). بعد ذلك سنبدأ جلسة عمل جديدة والاستعلام عن نقاط الارتساء باستخدام معرفات الارتساء المخزنة.

SpatialAnchorManager يمكن أن تأخذ الرعاية من توقف الجلسة ببساطة عن طريق استدعاء أسلوبها DestroySession() . دعونا نضيف هذا إلى أسلوبنا LongTap()

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

دعونا ننشئ أسلوبا لإزالة جميع نقاط الارتساء GameObjects

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

واستدعيها بعد تدمير الجلسة في 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");
}

تحديد موقع الارتساء

سنحاول الآن العثور على نقاط الارتساء مرة أخرى بالموضع والتناوب الصحيحين الذين أنشأناهما فيها. للقيام بذلك، نحتاج إلى بدء جلسة عمل وإنشاء Watcher التي ستبحث عن نقاط الارتساء التي تناسب المعايير المحددة. كمعيار سوف نغذيه معرفات نقاط الارتساء التي أنشأناها سابقا. دعونا ننشئ أسلوبا LocateAnchor() ونستخدم SpatialAnchorManager لإنشاء Watcher. لتحديد موقع الاستراتيجيات بخلاف استخدام معرفات الارتساء، راجع استراتيجية تحديد موقع الارتساء

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

بمجرد بدء تشغيل المراقب، سيطلق رد اتصال عند العثور على ارتساء يناسب المعايير المحددة. دعونا أولا ننشئ أسلوبنا في موقع الارتساء المسمى SpatialAnchorManager_AnchorLocated() الذي سنقوم بتكوينه ليتم استدعاؤه عندما يكون المراقب قد حدد موقع نقطة ارتساء. سيقوم هذا الأسلوب بإنشاء عنصر مرئي GameObject وإرفاق مكون الارتساء الأصلي به. سيتأكد مكون الارتساء الأصلي من تعيين الموضع والتناوب الصحيحين.GameObject

على غرار عملية الإنشاء، يتم إرفاق الارتساء ب GameObject. لا يجب أن يكون GameObject مرئيا في المشهد الخاص بك حتى تعمل نقاط الارتساء المكانية. لغرض هذا البرنامج التعليمي، سوف نتصور كل مرساة كمكعب أزرق بمجرد تحديد موقعها. إذا كنت تستخدم الارتساء فقط لإنشاء نظام إحداثيات مشترك، فلن تكون هناك حاجة لتصور 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);
        });
    }
}

لنشترك الآن في رد الاتصال AnchorLocated من للتأكد من SpatialAnchorManager استدعاء أسلوبنا SpatialAnchorManager_AnchorLocated() بمجرد أن يعثر المراقب على نقطة ارتساء.

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

وأخيرا، دعونا نوسع أسلوبنا LongTap() ليشمل العثور على المرساة. سنستخدم القيمة المنطقية IsSessionStarted لتحديد ما إذا كنا نبحث عن جميع نقاط الارتساء أو ندمر جميع نقاط الارتساء كما هو موضح في نظرة عامة على التطبيق

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

جربه #2

يدعم تطبيقك الآن إنشاء نقاط الارتساء وتحديد موقعها. أنشئ تطبيقك في Unity وانشره من Visual Studio باتباع استخدام Visual Studio للنشر والتصحيح.

تأكد من اتصال HoloLens بالإنترنت. بمجرد بدء التطبيق واختفاء رسالة Unity التي تم إجراؤها، اضغط ضغطا قصيرا في محيطك. يجب أن يظهر مكعب أبيض لإظهار موضع وتدارة المرساة التي سيتم إنشاؤها. يتم استدعاء عملية إنشاء الارتساء تلقائيا. بينما تنظر ببطء حول محيطك، فإنك تلتقط بيانات البيئة. بمجرد جمع بيانات بيئة كافية، سيحاول تطبيقنا إنشاء نقطة ارتساء في الموقع المحدد. بمجرد اكتمال عملية إنشاء الارتساء، سيتحول المكعب إلى اللون الأخضر. تحقق من سجلات تتبع الأخطاء في visual studio لمعرفة ما إذا كان كل شيء يعمل على النحو المنشود.

اضغط ضغطا طويلا لإزالة الكل GameObjects من المشهد وإيقاف جلسة الارتساء المكاني.

بمجرد مسح المشهد الخاص بك، يمكنك الضغط لفترة طويلة مرة أخرى، والتي ستبدأ جلسة وتبحث عن نقاط الارتساء التي قمت بإنشائها مسبقا. بمجرد العثور عليها، يتم تصورها بواسطة مكعبات زرقاء في الموضع والتناوب المرساة. يمكن العثور على نقاط الارتساء هذه (طالما لم تنته صلاحيتها) بواسطة أي جهاز مدعوم طالما أن لديها معرفات الارتساء الصحيحة ولديها حق الوصول إلى مورد الارتساء المكاني الخاص بك.

حذف ارتساء

الآن يمكن لتطبيقنا إنشاء نقاط الارتساء وتحديد موقعها. بينما يحذف GameObjects، فإنه لا يحذف نقطة الارتساء في السحابة. دعونا نضيف الوظيفة لحذفها أيضا في السحابة إذا ضغطت على نقطة ارتساء موجودة.

دعونا نضيف أسلوبا DeleteAnchor يتلقى GameObject. سنستخدم SpatialAnchorManager بعد ذلك مع مكون الكائن CloudNativeAnchor لطلب حذف الارتساء في السحابة.

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

لاستدعاء هذا الأسلوب من ، نحتاج إلى أن نكون قادرين على تحديد ما إذا كان الضغط بالقرب من ShortTapارتساء مرئي موجود. دعونا ننشئ أسلوبا مساعدا يهتم بذلك

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

يمكننا الآن توسيع أسلوبنا ShortTap ليشمل 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);
    }
}

جربه #3

أنشئ تطبيقك في Unity وانشره من Visual Studio باتباع استخدام Visual Studio للنشر والتصحيح.

لاحظ أن موقع إيماءة اللمس باليد هو مركز يدك في هذا التطبيق وليس طرف أصابعك.

عند الضغط على ارتساء، يتم إما إنشاء (أخضر) أو تحديد موقع (أزرق) يتم إرسال طلب إلى خدمة الارتساء المكاني لإزالة نقطة الارتساء هذه من الحساب. أوقف جلسة العمل (اضغط ضغطا طويلا) وابدأ جلسة العمل مرة أخرى (اضغط ضغطا طويلا) للبحث عن جميع نقاط الارتساء. لن يتم تحديد موقع نقاط الارتساء المحذوفة بعد الآن.

تجميع كل شيء سويًا

إليك كيف يجب أن يبدو ملف الفئة الكامل AzureSpatialAnchorsScript ، بعد تجميع جميع العناصر المختلفة معا. يمكنك استخدامه كمرجع للمقارنة مقابل الملف الخاص بك، وحدد ما إذا وجدت أي اختلافات.

إشعار

ستلاحظ أننا قمنا بتضمين [RequireComponent(typeof(SpatialAnchorManager))] البرنامج النصي. مع هذا، ستتأكد Unity من أن GameObject حيث نرفق AzureSpatialAnchorsScript به، يحتوي أيضا على SpatialAnchorManager المرفق به.

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>

}

الخطوات التالية

في هذا البرنامج التعليمي، تعلمت كيفية تنفيذ تطبيق Spatial Anchors أساسي ل HoloLens باستخدام Unity. لمعرفة المزيد حول كيفية استخدام Azure Spatial Anchors في تطبيق Android جديد، تابع البرنامج التعليمي التالي.