共用方式為


HoloLens (第 1 代) 和 Azure 309:Application Insights

注意

混合實境學院教學課程的設計是以 HoloLens (第 1 代) 和混合實境沉浸式頭戴裝置為準。 因此,對於仍在尋找這些裝置開發指引的開發人員而言,我們覺得這些教學課程很重要。 這些教學課程不會使用用於 HoloLens 2 的最新工具組或互動進行更新。 系統會保留這些資訊,以繼續在支援的裝置上運作。 未來將會張貼一系列新的教學課程,以示範如何為 HoloLens 2 進行開發。 張貼這些教學課程的連結將會更新此通知。

混合實境學院教學課程歡迎畫面。

在此課程中,您將瞭解如何使用 Azure 應用程式 Insights API 將 Application Insights 功能新增至混合實境應用程式,以收集使用者行為的相關分析。

Application Insights 是一項Microsoft服務,可讓開發人員從其應用程式收集分析,並從易於使用的入口網站加以管理。 分析可以是任何內容,從效能到您想要收集的自定義資訊。 如需詳細資訊,請流覽 Application Insights 頁面

完成本課程之後,您將有混合實境沉浸式頭戴式裝置應用程式,其可以執行下列動作:

  1. 允許使用者注視並移動場景。
  2. 使用注視和接近現場對象,觸發分析傳送至 Application Insights 服務
  3. 應用程式也會在服務上呼叫,擷取過去 24 小時內使用者最接近的物件相關信息。 該物件會將其色彩變更為綠色。

本課程將教導您如何從 Application Insights 服務取得結果,以 Unity 為基礎的範例應用程式。 您必須將這些概念套用至您可能要建置的自定義應用程式。

裝置支援

課程 HoloLens 沉浸式頭戴裝置
MR 和 Azure 309:Application Insights ✔️ ✔️

注意

雖然本課程主要著重於 Windows Mixed Reality 沉浸式 (VR) 頭戴式裝置,但您也可以將此課程中學到的內容套用至 Microsoft HoloLens。 隨著您遵循課程,您將會看到任何您可能需要採用以支援 HoloLens 變更的附注。 使用 HoloLens 時,您可能會在語音擷取期間注意到一些回應。

必要條件

注意

本教學課程專為具備 Unity 和 C# 基本經驗的開發人員所設計。 另請注意,本檔內的必要條件和書面指示代表在撰寫期間經過測試和驗證的內容(2018 年 7 月)。 您可以自由使用最新的軟體,如安裝工具文章中所列,不過不應該假設本課程中的資訊會完全符合您在較新的軟體中找到的內容,而不是下面所列的內容。

針對此課程,我們建議使用下列硬體和軟體:

在您開始使用 Intune 之前

為避免建置此專案時發生問題,強烈建議您在根資料夾或近根資料夾中建立此教學課程中的專案(長資料夾路徑可能會導致建置時發生問題)。

警告

請注意,前往 Application Insights 的數據需要時間,因此請耐心等候。 如果您想要檢查服務是否已收到您的數據,請參閱 第 14 章,其中會示範如何瀏覽入口網站。

第 1 章 - Azure 入口網站

若要使用 Application Insights,您必須在 Azure 入口網站 中建立及設定 Application Insights 服務

  1. 登入 Azure 入口網站

    注意

    如果您還沒有 Azure 帳戶,則必須建立一個帳戶。 如果您在教室或實驗室情況中遵循本教學課程,請洽詢您的講師或其中一名監看員,以協助設定您的新帳戶。

  2. 登入之後,按兩下左上角的 [新增],然後搜尋Application Insights,然後按兩下 Enter

    注意

    [新增] 一詞可能已取代為在較新的入口網站中建立資源

    顯示 Azure 入口網站的螢幕快照,[所有專案] 窗格中已醒目提示 [深入解析]。

  3. 右側的新頁面將提供 Azure 應用程式 Insights Service 的描述。 在此頁面左下方,選取 [ 建立] 按鈕,以建立此服務的關聯。

    Application Insights 畫面的螢幕快照,其中已醒目提示 [建立]。

  4. 按兩下 [建立]:

    1. 插入此服務實例所需的 名稱

    2. 選取 [一般] 作為 [應用程式類型]。

    3. 選取適當的訂用 帳戶

    4. 選擇資源群組或建立新的群組。 資源群組提供一種方式來監視、控制存取、布建和管理 Azure 資產集合的計費。 建議將所有與單一專案相關聯的 Azure 服務(例如,例如,如這些課程)保留在通用資源群組底下)。

      如果您想要深入瞭解 Azure 資源群組,請 瀏覽資源群組文章

    5. 選取 [位置]

    6. 您也必須確認您已瞭解此服務適用的條款和條件。

    7. 選取 建立

      Application Insights 視窗的螢幕快照。名稱與應用程式類型會反白顯示。

  5. 按兩下 [建立] 之後,您必須等候服務建立,這可能需要一分鐘的時間。

  6. 建立服務實例之後,入口網站中就會顯示通知。

    顯示功能區一部分的螢幕快照,其中已醒目提示通知圖示。

  7. 選取通知以探索新的服務實例。

    顯示 [部署成功] 對話框的螢幕快照,其中已醒目提示 [移至資源]。

  8. 按兩下通知中的 [ 移至資源 ] 按鈕,以探索新的服務實例。 系統會帶您前往新的 Application Insights Service 實例。

    此螢幕快照顯示實例名稱為 MyNewInsight 的 Application Insights Service 實例。

    注意

    保持此網頁開啟且易於存取,您通常會回到這裡查看收集的數據。

    重要

    若要實作 Application Insights,您必須使用三個 (3) 個特定值: 檢測金鑰應用程式識別碼API 密鑰。 您將在下面瞭解如何從服務擷取這些值。 請務必在空白 的 [記事本 ] 頁面上記下這些值,因為您很快就會在程序代碼中使用這些值。

  9. 若要尋找 檢測金鑰,您必須向下卷動服務函式清單,然後選取 [屬性],顯示的索引標籤會顯示 [服務金鑰]。

    顯示服務函式的螢幕快照,[設定] 區段中會醒目提示 [屬性],而 [檢測密鑰] 會醒目提示在主要窗格中。

  10. 在 [屬性] 下方,您會發現需要單擊的 API 存取 右側的面板會提供 您應用程式的 [應用程式識別符 ]。

    顯示服務函式的螢幕快照,其中已醒目提示 P I Access。在主窗格中,會反白顯示 [建立 P I 金鑰] 和 [應用程式識別符]。

  11. [應用程式識別符 ] 面板仍然開啟時,按兩下 [建立 API 金鑰],這會開啟 [ 建立 API 金鑰 ] 面板。

    顯示 [建立 P I 金鑰] 面板的螢幕快照。

  12. 在現在開啟的 [ 建立 API 金鑰 ] 面板中,輸入描述,然後 勾選三個方塊

  13. 按兩下 [ 產生金鑰]。 您的 API 金鑰 將會建立並顯示。

    [建立 P I 金鑰] 面板的螢幕快照,其中顯示新的服務密鑰資訊。

    警告

    這是服務 金鑰 顯示的唯一時間,因此請確定您現在有一份複本。

第 2 章 - 設定 Unity 專案

以下是使用混合實境進行開發的一般設定,因此是其他專案的良好範本。

  1. 開啟 Unity ,然後按兩下 [ 新增]。

    Unity 專案視窗的螢幕快照。未顯示任何項目資訊。

  2. 您現在必須提供 Unity 項目名稱,插入 MR_Azure_Application_Insights。 請確定 [ 範本 ] 設定為 3D。 將 [ 位置 ] 設定為您適當的位置(請記住,更接近根目錄會更好)。 然後按兩下 [ 建立專案]。

    Unity 新專案視窗的螢幕快照,其中顯示項目資訊。

  3. 在 Unity 開啟時,請務必檢查預設 的腳本編輯器 設定為 Visual Studio。 移至 [ 編輯 > 喜好設定 ],然後從新視窗流覽至 [外部工具]。 將外部腳本編輯器變更Visual Studio 2017。 關閉 [喜好設定] 視窗。

    顯示 Visual Studio 設定為外部腳本編輯器的螢幕快照。

  4. 接下來,移至 [檔案>建置設定],然後按兩下 [切換平臺] 按鈕,將平臺切換為 [通用 Windows 平台]。

    [建置設定] 視窗的螢幕快照,其中顯示 [平臺] 選取清單。已選取 通用 Windows 平台。

  5. 移至 [ 檔案 > 建置設定 ],並確定:

    1. 目標裝置 設定為 [任何裝置]

      針對 Microsoft HoloLens,將 [目標裝置] 設定[HoloLens]。

    2. 組建類型 設定為 D3D

    3. SDK 設定為 [最新安裝]

    4. [建置並執行 ] 設定為 [ 本機計算機]

    5. 儲存場景並將它新增至組建。

      1. 選取 [ 新增開啟場景] 來執行此動作。 隨即會出現儲存視窗。

        [建置設定] 視窗的螢幕快照,已選取 [新增開啟場景]。

      2. 為此建立新的資料夾,以及任何未來的場景,然後按兩下 [新增資料夾] 按鈕,以建立新的資料夾 ,並將它命名為 Scenes

        [儲存場景] 視窗的螢幕快照,其中已選取 [場景] 資料夾。

      3. 開啟新建立 的 Scenes 資料夾,然後在 [檔名: 文字] 字段中輸入 ApplicationInsightsScene,然後按兩下 [ 儲存]。

        [儲存場景] 視窗的螢幕快照,其中已輸入檔名。

  6. [建置設定] 中的其餘設定現在應該保留為預設值。

  7. 在 [建置設定] 視窗中,選取 [播放程序設定],這會在 Inspector 所在的空間中開啟相關面板。

    [偵測器] 索引標籤的螢幕快照,其中顯示播放機設定。

  8. 在此面板中,需要驗證一些設定:

    1. 在 [ 其他設定] 索引標籤中:

      1. 腳本運行時間版本應該是實驗性版本 (.NET 4.6 對等專案),這會觸發需要重新啟動編輯器。

      2. 腳本後端 應該是 .NET

      3. API 相容性層級 應該是 .NET 4.6

      [偵測器] 索引標籤的螢幕快照,其中顯示 [其他設定] 區段中的詳細數據。

    2. 在 [發佈設定] 索引標籤的 [功能] 底下,檢查:

      • InternetClient

        已核取 [功能] 列表的螢幕快照,其中已核取因特網用戶端。

    3. 在面板的進一步下,在 [XR 設定] 中,勾選 [支援虛擬實境],確定已新增 Windows Mixed Reality SDK

      已核取 [X R 設定] 區段的螢幕快照,其中已核取 [支援虛擬實境]。

  9. 回到 [建置設定] 中Unity C# 專案不再呈現灰色;勾選此旁邊的複選框。

  10. 關閉 [建置設定] 視窗。

  11. 儲存場景和專案(檔案>儲存場景/檔案>儲存專案)。

第 3 章 - 匯入 Unity 套件

重要

如果您要略過本課程的 Unity 設定元件,並繼續直接進入程式碼,請隨意下載此 Azure-MR-309.unitypackage,將它匯入您的專案做為自定義套件。 這也會包含下一章中的 DLL。 匯入之後,請從 第 6 章繼續進行。

重要

若要在 Unity 中使用 Application Insights,您必須匯入 DLL,以及 Newtonsoft DLL。 Unity 中目前有已知問題,需要在匯入之後重新設定外掛程式。 這些步驟(本節中的 4 - 7)在 Bug 解決之後將不再需要。

若要將 Application Insights 匯入您自己的專案,請確定您已 下載包含外掛程式的 '.unitypackage'。 然後執行下列動作:

  1. 使用 [資產>匯入套件自定義套件>] 功能表選項,將 .unitypackage** 新增至 Unity。

  2. 在快顯的 [ 匯入 Unity 套件] 方塊中,確定已選取 [包含] 外掛程式 底下的所有專案。

    [匯入 Unity 套件] 對話框的螢幕快照,其中顯示已核取的所有專案。

  3. 按兩下 [匯入] 按鈕,將專案新增至您的專案。

  4. 移至 [專案] 檢視中 [外掛程式] 底下的 [Insights] 資料夾,並只選取下列外掛程式

    • Microsoft.ApplicationInsights

    [專案] 面板的螢幕快照,[Insights] 資料夾隨即開啟。

  5. 選取此外掛程式后,請確定未核取 [任何平臺],然後確定 WSAPlayer未核取,然後按兩下 [套用]。 這樣做只是為了確認檔案已正確設定。

    [偵測器] 面板的螢幕快照,其中顯示已核取 [編輯器] 和 [獨立]。

    注意

    將這類外掛程式標示為只用於 Unity 編輯器。 WSA 資料夾中有一組不同的 DLL,將在專案從 Unity 匯出之後使用。

  6. 接下來,您必須在 Insights 資料夾中開啟 WSA 資料夾。 您會看到您所設定之相同檔案的複本。 選取此檔案,然後在偵測器中,確定未核取 [任何平臺],然後確定核取 WSAPlayer。 按一下 套用

    [偵測器] 面板的螢幕快照,其中顯示已核取 W S A 播放機。

  7. 您現在必須遵循 步驟 4-6,但改為針對 Newtonsoft 外掛程式。 如需結果的外觀,請參閱下列螢幕快照。

    [專案] 和 [偵測器] 面板四個檢視的螢幕快照,其中顯示設定 Newtonsoft 資料夾和外掛程式選取項目的結果。

第 4 章 - 設定相機和使用者控制項

在本章中,您將設定相機和控件,讓用戶在場景中查看和移動。

  1. 以滑鼠右鍵按兩下階層面板中的空白區域,然後在 [建立空白] 上按下滑鼠右鍵。>

    [階層] 面板的螢幕快照,已選取 [建立空白]。

  2. 將新的空白 GameObject 重新命名為 Camera Parent

    [階層] 面板的螢幕快照,其中已選取 [相機父系]。偵測器面板

  3. 以滑鼠右鍵按兩下階層面板中的空白區域,然後在3D物件上,然後在Sphere上按下滑鼠右鍵

  4. 將Sphere重新命名為 右手

  5. 右手的轉換比例 設定為 0.1、0.1、0.1

    [階層] 和 [偵測器] 面板的螢幕快照,其中已醒目提示 [偵測器] 面板上的 [轉換] 區段。

  6. 按兩下Sphere碰撞器元件中的齒輪,然後移除元件,以從右手移除Sphere碰撞器元件。

    [偵測器] 面板的螢幕快照,齒輪圖示和 [移除元件] 會在 [Sphere 碰撞器] 區段中醒目提示。

  7. 在 [階層面板] 中,將 主要相機右手 物件拖曳至 Camera Parent 物件。

    [階層] 面板的螢幕快照,其中已選取 [主要相機],[偵測器] 面板會顯示已核取 [主要相機]。

  8. 主相機右手物件的轉換位置設定為 0,0,0

    [階層] 面板的螢幕快照,其中已選取 [主要相機],[轉換] 設定會在 [偵測器] 面板中醒目提示。

    已選取右手的 [階層] 面板螢幕快照,[偵測器] 面板中會醒目提示 [轉換設定]。

第 5 章 - 在 Unity 場景中設定物件

您現在會為場景建立一些基本圖形,讓使用者可以與之互動。

  1. 以滑鼠右鍵按兩下階層面板中的空白區域,然後在3D物件,然後選取[平面]。

  2. 將平面 轉換位置 設定為 0、-1、0

  3. 將平面 轉換比例 設定為 5、1、5

    場景、階層和偵測器面板的螢幕快照。[偵測器] 面板中的 [轉換] 區段會反白顯示。

  4. 建立要與您的 Plane 物件搭配使用的基本材質,讓其他圖形更容易看到。 流覽至您的 專案面板,以滑鼠右鍵按兩下,然後按兩下 [ 建立],後面接著 [資料夾],以建立新的資料夾。 將它命名為 [材質]。

    [專案] 面板的螢幕快照,其中顯示醒目提示 [建立] 和 [資料夾]。 [專案] 面板的螢幕快照。[資產] 窗格中會醒目提示材質。

  5. 開啟 [材質] 資料夾,然後按下滑鼠右鍵,按兩下 [建立],然後按兩下 [材質],以建立新的材質。 將它命名為 藍色

    [專案] 面板的螢幕快照,其中顯示醒目提示 [建立] 和 [材質]。 [專案] 面板的螢幕快照。藍色會在 [材質] 窗格中反白顯示。

  6. 選取新的藍色材質後,查看 Inspector,然後按兩下與 Albedo 並排的矩形視窗。 選取藍色(下圖為 Hex Color:#3592FFFF)。 選擇之後,按兩下 [關閉] 按鈕。

    [偵測器] 面板的螢幕快照。色彩區段會反白顯示。

  7. 將新材質從 [材質] 資料夾拖曳到您新建立的平面、場景中(或將其放在Hierarchy內的 Plane 物件上)。

    [場景] 面板的螢幕快照,其中顯示 [材質] 資料夾中的新材質。

  8. 在階層面板中的空白區域中按下滑鼠右鍵,然後在3D物件,Capsule按兩下滑鼠右鍵。

    • 選取 [膠囊] 后,將其 [轉換位置] 變更為:-10、1、0
  9. 以滑鼠右鍵按兩下階層面板中的空白區域,然後在3D物件 Cube按滑鼠右鍵。

    • 選取 Cube 之後,將其 [轉換位置] 變更為:0、0、10
  10. 以滑鼠右鍵按兩下階層面板中的空白區域,然後在3D物件、Sphere 上按兩下滑鼠右鍵。

    • 選取 [Sphere] 時,將其 [轉換位置] 變更為:10、0、0。

    場景、階層和偵測器面板的螢幕快照。[階層] 面板中已選取 [膠囊]。

    注意

    這些位置值是建議。 您可以隨意將物件的位置設定為您想要的任何位置,但如果對象距離離相機不遠,應用程式的用戶會比較容易。

  11. 當您的應用程式執行時,它必須能夠識別場景中的物件,才能達到此目的,需要標記它們。 選取其中一個對象,然後在 [偵測器] 面板中,按兩下 [新增標籤...],這會將Inspector[標記和圖層] 面板交換

    [偵測器] 面板的螢幕快照,其中顯示醒目提示的 [新增卷標] 選項。 [偵測器] 面板的螢幕快照,其中已醒目提示標記和圖層。

  12. 按兩下 +(加號)符號,然後將標籤名稱輸入為 ObjectInScene

    [偵測器] 面板的螢幕快照,其中已選取 [標記和圖層]。[新增標記名稱] 對話框會反白顯示。

    警告

    如果您針對標記使用不同的名稱,則必須確保此變更也會讓 DataFromAnalyticsObjectTriggerGaze、腳本稍後找到並偵測到您的物件。

  13. 建立標記之後,您現在必須將其套用至所有三個物件。 從 [階層] 中,按住 Shift 鍵,然後按下 [膠囊]、[Cube] 和 [Sphere],然後在 [偵測器] 中,單擊 [捲標] 旁的下拉菜單,然後按兩下您建立的 ObjectInScene 標記。

    [偵測器] 面板的螢幕快照,箭號指向 [標記]。[未標記] 功能表會顯示已核取的 [未標記],並已選取 [ObjectInScene]。 顯示兩個功能表的螢幕快照,其中已醒目提示 [建立] 和 [資料夾]。

第 6 章 - 建立 ApplicationInsightsTracker 類別

您需要建立的第一個腳本是 ApplicationInsightsTracker,負責:

  1. 根據用戶互動建立事件,以提交至 Azure 應用程式 Insights。

  2. 視用戶互動而定,建立適當的事件名稱。

  3. 將事件提交至 Application Insights Service 實例。

若要建立此類別:

  1. 滑鼠右鍵按兩下 [項目面板],然後按兩下 [建立>資料夾]。 將資料夾 命名為文稿

    [專案] 面板的螢幕快照。[資產] 窗格中會反白顯示 [腳本] 資料夾圖示。 顯示已選取 [建立] 和 [C# 腳本] 選項選單選項的螢幕快照。

  2. 建立 [腳本] 資料夾后,按兩下即可開啟。 然後,在該資料夾中,以滑鼠右鍵按兩下 [建立>C# 腳本]。 將腳本 命名為ApplicationInsightsTracker

  3. 按兩下新的 ApplicationInsightsTracker 腳本,以使用 Visual Studio 開啟它。

  4. 更新文稿頂端的命名空間,如下所示:

        using Microsoft.ApplicationInsights;
        using Microsoft.ApplicationInsights.DataContracts;
        using Microsoft.ApplicationInsights.Extensibility;
        using UnityEngine;
    
  5. 在類別內插入下列變數:

        /// <summary>
        /// Allows this class to behavior like a singleton
        /// </summary>
        public static ApplicationInsightsTracker Instance;
    
        /// <summary>
        /// Insert your Instrumentation Key here
        /// </summary>
        internal string instrumentationKey = "Insert Instrumentation Key here";
    
        /// <summary>
        /// Insert your Application Id here
        /// </summary>
        internal string applicationId = "Insert Application Id here";
    
        /// <summary>
        /// Insert your API Key here
        /// </summary>
        internal string API_Key = "Insert API Key here";
    
        /// <summary>
        /// Represent the Analytic Custom Event object
        /// </summary>
        private TelemetryClient telemetryClient;
    
        /// <summary>
        /// Represent the Analytic object able to host gaze duration
        /// </summary>
        private MetricTelemetry metric;
    
  6. 然後新增 Start()Awake() 方法,這會在類別初始化時呼叫:

        /// <summary>
        /// Sets this class instance as a singleton
        /// </summary>
        void Awake()
        {
            Instance = this;
        }
    
        /// <summary>
        /// Use this for initialization
        /// </summary>
        void Start()
        {
            // Instantiate telemetry and metric
            telemetryClient = new TelemetryClient();
    
            metric = new MetricTelemetry();
    
            // Assign the Instrumentation Key to the Event and Metric objects
            TelemetryConfiguration.Active.InstrumentationKey = instrumentationKey;
    
            telemetryClient.InstrumentationKey = instrumentationKey;
        }
    
  7. 新增負責傳送應用程式所註冊事件和計量的方法:

        /// <summary>
        /// Submit the Event to Azure Analytics using the event trigger object
        /// </summary>
        public void RecordProximityEvent(string objectName)
        {
            telemetryClient.TrackEvent(CreateEventName(objectName));
        }
    
        /// <summary>
        /// Uses the name of the object involved in the event to create 
        /// and return an Event Name convention
        /// </summary>
        public string CreateEventName(string name)
        {
            string eventName = $"User near {name}";
            return eventName;
        }
    
        /// <summary>
        /// Submit a Metric to Azure Analytics using the metric gazed object
        /// and the time count of the gaze
        /// </summary>
        public void RecordGazeMetrics(string objectName, int time)
        {
            // Output Console information about gaze.
            Debug.Log($"Finished gazing at {objectName}, which went for <b>{time}</b> second{(time != 1 ? "s" : "")}");
    
            metric.Name = $"Gazed {objectName}";
    
            metric.Value = time;
    
            telemetryClient.TrackMetric(metric);
        }
    
  8. 請務必先在Visual Studio儲存變更,再返回 Unity

第 7 章 - 建立注視腳本

要建立的下一個腳本是 Gaze 腳本。 此腳本負責建立將從主相機向前投影的Raycast,以偵測使用者正在查看的物件。 在此情況下, Raycast 必須識別使用者是否查看具有 ObjectInScene 標記的物件,然後計算使用者 注視 該對象的時間長度。

  1. 按兩下 [文稿] 資料夾,以開啟它。

  2. 在 [腳本] 資料夾內按下滑鼠右鍵,按兩下 [建立>C# 腳本]。 將腳本 命名為 Gaze

  3. 按兩下腳本以使用Visual Studio加以開啟。

  4. 將現有的程式碼取代為下列程式碼:

        using UnityEngine;
    
        public class Gaze : MonoBehaviour
        {
            /// <summary>
            /// Provides Singleton-like behavior to this class.
            /// </summary>
            public static Gaze Instance;
    
            /// <summary>
            /// Provides a reference to the object the user is currently looking at.
            /// </summary>
            public GameObject FocusedGameObject { get; private set; }
    
            /// <summary>
            /// Provides whether an object has been successfully hit by the raycast.
            /// </summary>
            public bool Hit { get; private set; }
    
            /// <summary>
            /// Provides a reference to compare whether the user is still looking at 
            /// the same object (and has not looked away).
            /// </summary>
            private GameObject _oldFocusedObject = null;
    
            /// <summary>
            /// Max Ray Distance
            /// </summary>
            private float _gazeMaxDistance = 300;
    
            /// <summary>
            /// Max Ray Distance
            /// </summary>
            private float _gazeTimeCounter = 0;
    
            /// <summary>
            /// The cursor object will be created when the app is running,
            /// this will store its values. 
            /// </summary>
            private GameObject _cursor;
        }
    
  5. 現在必須新增 Awake()Start() 方法的程式代碼。

        private void Awake()
        {
            // Set this class to behave similar to singleton
            Instance = this;
            _cursor = CreateCursor();
        }
    
        void Start()
        {
            FocusedGameObject = null;
        }
    
        /// <summary>
        /// Create a cursor object, to provide what the user
        /// is looking at.
        /// </summary>
        /// <returns></returns>
        private GameObject CreateCursor()    
        {
            GameObject newCursor = GameObject.CreatePrimitive(PrimitiveType.Sphere);
    
            // Remove the collider, so it does not block raycast.
            Destroy(newCursor.GetComponent<SphereCollider>());
    
            newCursor.transform.localScale = new Vector3(0.1f, 0.1f, 0.1f);
    
            newCursor.GetComponent<MeshRenderer>().material.color = 
            Color.HSVToRGB(0.0223f, 0.7922f, 1.000f);
    
            newCursor.SetActive(false);
            return newCursor;
        }
    
  6. Gaze 類別內,在 Update() 方法中新增下列程式代碼,以投影 Raycast 並偵測目標命中:

        /// <summary>
        /// Called every frame
        /// </summary>
        void Update()
        {
            // Set the old focused gameobject.
            _oldFocusedObject = FocusedGameObject;
    
            RaycastHit hitInfo;
    
            // Initialize Raycasting.
            Hit = Physics.Raycast(Camera.main.transform.position, Camera.main.transform.forward, out hitInfo, _gazeMaxDistance);
    
            // Check whether raycast has hit.
            if (Hit == true)
            {
                // Check whether the hit has a collider.
                if (hitInfo.collider != null)
                {
                    // Set the focused object with what the user just looked at.
                    FocusedGameObject = hitInfo.collider.gameObject;
    
                    // Lerp the cursor to the hit point, which helps to stabilize the gaze.
                    _cursor.transform.position = Vector3.Lerp(_cursor.transform.position, hitInfo.point, 0.6f);
    
                    _cursor.SetActive(true);
                }
                else
                {
                    // Object looked on is not valid, set focused gameobject to null.
                    FocusedGameObject = null;
    
                    _cursor.SetActive(false);
                }
            }
            else
            {
                // No object looked upon, set focused gameobject to null.
                FocusedGameObject = null;
    
                _cursor.SetActive(false);
            }
    
            // Check whether the previous focused object is this same object. If so, reset the focused object.
            if (FocusedGameObject != _oldFocusedObject)
            {
                ResetFocusedObject();
            }
            // If they are the same, but are null, reset the counter. 
            else if (FocusedGameObject == null && _oldFocusedObject == null)
            {
                _gazeTimeCounter = 0;
            }
            // Count whilst the user continues looking at the same object.
            else
            {
                _gazeTimeCounter += Time.deltaTime;
            }
        }
    
  7. 新增 ResetFocusedObject() 方法,以在使用者查看物件時將數據傳送至 Application Insights

        /// <summary>
        /// Reset the old focused object, stop the gaze timer, and send data if it
        /// is greater than one.
        /// </summary>
        public void ResetFocusedObject()
        {
            // Ensure the old focused object is not null.
            if (_oldFocusedObject != null)
            {
                // Only looking for objects with the correct tag.
                if (_oldFocusedObject.CompareTag("ObjectInScene"))
                {
                    // Turn the timer into an int, and ensure that more than zero time has passed.
                    int gazeAsInt = (int)_gazeTimeCounter;
    
                    if (gazeAsInt > 0)
                    {
                        //Record the object gazed and duration of gaze for Analytics
                        ApplicationInsightsTracker.Instance.RecordGazeMetrics(_oldFocusedObject.name, gazeAsInt);
                    }
                    //Reset timer
                    _gazeTimeCounter = 0;
                }
            }
        }
    
  8. 您現在 已完成注視 文稿。 在 Visual Studio儲存變更,再返回 Unity

第 8 章 - 建立 ObjectTrigger 類別

您需要建立的下一個腳本是 ObjectTrigger,負責:

  • 將衝突所需的元件新增至主相機。
  • 偵測相機是否靠近標記為 ObjectInScene 的物件。

若要建立文稿:

  1. 按兩下 [文稿] 資料夾,以開啟它。

  2. 在 [腳本] 資料夾內按下滑鼠右鍵,按兩下 [建立>C# 腳本]。 將腳本 命名為 ObjectTrigger

  3. 按兩下腳本以使用Visual Studio加以開啟。 將現有的程式碼取代為下列程式碼:

        using UnityEngine;
    
        public class ObjectTrigger : MonoBehaviour
        {
            private void Start()
            {
                // Add the Collider and Rigidbody components, 
                // and set their respective settings. This allows for collision.
                gameObject.AddComponent<SphereCollider>().radius = 1.5f;
    
                gameObject.AddComponent<Rigidbody>().useGravity = false;
            }
    
            /// <summary>
            /// Triggered when an object with a collider enters this objects trigger collider.
            /// </summary>
            /// <param name="collision">Collided object</param>
            private void OnCollisionEnter(Collision collision)
            {
                CompareTriggerEvent(collision, true);
            }
    
            /// <summary>
            /// Triggered when an object with a collider exits this objects trigger collider.
            /// </summary>
            /// <param name="collision">Collided object</param>
            private void OnCollisionExit(Collision collision)
            {
                CompareTriggerEvent(collision, false);
            }
    
            /// <summary>
            /// Method for providing debug message, and sending event information to InsightsTracker.
            /// </summary>
            /// <param name="other">Collided object</param>
            /// <param name="enter">Enter = true, Exit = False</param>
            private void CompareTriggerEvent(Collision other, bool enter)
            {
                if (other.collider.CompareTag("ObjectInScene"))
                {
                    string message = $"User is{(enter == true ? " " : " no longer ")}near <b>{other.gameObject.name}</b>";
    
                    if (enter == true)
                    {
                        ApplicationInsightsTracker.Instance.RecordProximityEvent(other.gameObject.name);
                    }
                    Debug.Log(message);
                }
            }
        }
    
  4. 請務必先在Visual Studio儲存變更,再返回 Unity

第 9 章 - 建立 DataFromAnalytics 類別

您現在必須建立 DataFromAnalytics 腳本,其負責:

  • 擷取相機最接近物件的分析數據。
  • 使用服務金鑰,可讓您與 Azure 應用程式 Insights Service 實例通訊。
  • 根據場景中具有最高事件計數的物件排序。
  • 將最接近對象的材質色彩變更為 綠色

若要建立文稿:

  1. 按兩下 [文稿] 資料夾,以開啟它。

  2. 在 [腳本] 資料夾內按下滑鼠右鍵,按兩下 [建立>C# 腳本]。 將腳本 命名為 DataFromAnalytics

  3. 按兩下腳本以使用Visual Studio加以開啟。

  4. 插入下列命名空間:

        using Newtonsoft.Json;
        using System;
        using System.Collections;
        using System.Collections.Generic;
        using System.Linq;
        using UnityEngine;
        using UnityEngine.Networking;
    
  5. 在文稿中,插入下列專案:

        /// <summary>
        /// Number of most recent events to be queried
        /// </summary>
        private int _quantityOfEventsQueried = 10;
    
        /// <summary>
        /// The timespan with which to query. Needs to be in hours.
        /// </summary>
        private int _timepspanAsHours = 24;
    
        /// <summary>
        /// A list of the objects in the scene
        /// </summary>
        private List<GameObject> _listOfGameObjectsInScene;
    
        /// <summary>
        /// Number of queries which have returned, after being sent.
        /// </summary>
        private int _queriesReturned = 0;
    
        /// <summary>
        /// List of GameObjects, as the Key, with their event count, as the Value.
        /// </summary>
        private List<KeyValuePair<GameObject, int>> _pairedObjectsWithEventCount = new List<KeyValuePair<GameObject, int>>();
    
        // Use this for initialization
        void Start()
        {
            // Find all objects in scene which have the ObjectInScene tag (as there may be other GameObjects in the scene which you do not want).
            _listOfGameObjectsInScene = GameObject.FindGameObjectsWithTag("ObjectInScene").ToList();
    
            FetchAnalytics();
        }
    
  6. 在 DataFromAnalytics 類別的 Start() 方法後面,新增下列稱為 FetchAnalytics() 的方法。 此方法負責填入機碼值組的清單,其中包含 GameObject 和佔位元符事件計數編號。 然後, 它會初始化 GetWebRequest() 協同程式。 您也可以在此方法中找到對 Application Insights 呼叫的查詢結構,做為查詢 URL 端點。

        private void FetchAnalytics()
        {
            // Iterate through the objects in the list
            for (int i = 0; i < _listOfGameObjectsInScene.Count; i++)
            {
                // The current event number is not known, so set it to zero.
                int eventCount = 0;
    
                // Add new pair to list, as placeholder, until eventCount is known.
                _pairedObjectsWithEventCount.Add(new KeyValuePair<GameObject, int>(_listOfGameObjectsInScene[i], eventCount));
    
                // Set the renderer of the object to the default color, white
                _listOfGameObjectsInScene[i].GetComponent<Renderer>().material.color = Color.white;
    
                // Create the appropriate object name using Insights structure
                string objectName = _listOfGameObjectsInScene[i].name;
    
     		    // Build the queryUrl for this object.
     		    string queryUrl = Uri.EscapeUriString(string.Format(
                    "https://api.applicationinsights.io/v1/apps/{0}/events/$all?timespan=PT{1}H&$search={2}&$select=customMetric/name&$top={3}&$count=true",
     			    ApplicationInsightsTracker.Instance.applicationId, _timepspanAsHours, "Gazed " + objectName, _quantityOfEventsQueried));
    
    
                // Send this object away within the WebRequest Coroutine, to determine it is event count.
                StartCoroutine("GetWebRequest", new KeyValuePair<string, int>(queryUrl, i));
            }
        }
    
  7. 在 FetchAnalytics() 方法正下方,新增名為 GetWebRequest()的方法,它會傳回 IEnumerator 此方法負責要求與特定 GameObject 對應的事件在 Application Insights呼叫的次數。 傳回所有傳送的查詢時, 會呼叫 DetermineWinner() 方法。

        /// <summary>
        /// Requests the data count for number of events, according to the
        /// input query URL.
        /// </summary>
        /// <param name="webQueryPair">Query URL and the list number count.</param>
        /// <returns></returns>
        private IEnumerator GetWebRequest(KeyValuePair<string, int> webQueryPair)
        {
            // Set the URL and count as their own variables (for readability).
            string url = webQueryPair.Key;
            int currentCount = webQueryPair.Value;
    
            using (UnityWebRequest unityWebRequest = UnityWebRequest.Get(url))
            {
                DownloadHandlerBuffer handlerBuffer = new DownloadHandlerBuffer();
    
                unityWebRequest.downloadHandler = handlerBuffer;
    
                unityWebRequest.SetRequestHeader("host", "api.applicationinsights.io");
    
                unityWebRequest.SetRequestHeader("x-api-key", ApplicationInsightsTracker.Instance.API_Key);
    
                yield return unityWebRequest.SendWebRequest();
    
                if (unityWebRequest.isNetworkError)
                {
                    // Failure with web request.
                    Debug.Log("<color=red>Error Sending:</color> " + unityWebRequest.error);
                }
                else
                {
                    // This query has returned, so add to the current count.
                    _queriesReturned++;
    
                    // Initialize event count integer.
                    int eventCount = 0;
    
                    // Deserialize the response with the custom Analytics class.
                    Analytics welcome = JsonConvert.DeserializeObject<Analytics>(unityWebRequest.downloadHandler.text);
    
                    // Get and return the count for the Event
                    if (int.TryParse(welcome.OdataCount, out eventCount) == false)
                    {
                        // Parsing failed. Can sometimes mean that the Query URL was incorrect.
                        Debug.Log("<color=red>Failure to Parse Data Results. Check Query URL for issues.</color>");
                    }
                    else
                    {
                        // Overwrite the current pair, with its actual values, now that the event count is known.
                        _pairedObjectsWithEventCount[currentCount] = new KeyValuePair<GameObject, int>(_pairedObjectsWithEventCount[currentCount].Key, eventCount);
                    }
    
                    // If all queries (compared with the number which was sent away) have 
                    // returned, then run the determine winner method. 
                    if (_queriesReturned == _pairedObjectsWithEventCount.Count)
                    {
                        DetermineWinner();
                    }
                }
            }
        }
    
  8. 下一個方法是 DetermineWinner(),它會根據最高的事件計數來排序 GameObjectInt 配對的清單。 然後,它會將 GameObject 的材質色彩變更為綠色(作為其最高計數的意見反應)。 這會顯示含有分析結果的訊息。

        /// <summary>
        /// Call to determine the keyValue pair, within the objects list, 
        /// with the highest event count.
        /// </summary>
        private void DetermineWinner()
        {
            // Sort the values within the list of pairs.
            _pairedObjectsWithEventCount.Sort((x, y) => y.Value.CompareTo(x.Value));
    
            // Change its colour to green
            _pairedObjectsWithEventCount.First().Key.GetComponent<Renderer>().material.color = Color.green;
    
            // Provide the winner, and other results, within the console window. 
            string message = $"<b>Analytics Results:</b>\n " +
                $"<i>{_pairedObjectsWithEventCount.First().Key.name}</i> has the highest event count, " +
                $"with <i>{_pairedObjectsWithEventCount.First().Value.ToString()}</i>.\nFollowed by: ";
    
            for (int i = 1; i < _pairedObjectsWithEventCount.Count; i++)
            {
                message += $"{_pairedObjectsWithEventCount[i].Key.name}, " +
                    $"with {_pairedObjectsWithEventCount[i].Value.ToString()} events.\n";
            }
    
            Debug.Log(message);
        }
    
  9. 新增類別結構,此結構將用來還原串行化從 Application Insights 接收的 JSON 物件。 在類別定義之外於 DataFromAnalytics 類別檔案的底部新增這些類別。

        /// <summary>
        /// These classes represent the structure of the JSON response from Azure Insight
        /// </summary>
        [Serializable]
        public class Analytics
        {
            [JsonProperty("@odata.context")]
            public string OdataContext { get; set; }
    
            [JsonProperty("@odata.count")]
            public string OdataCount { get; set; }
    
            [JsonProperty("value")]
            public Value[] Value { get; set; }
        }
    
        [Serializable]
        public class Value
        {
            [JsonProperty("customMetric")]
            public CustomMetric CustomMetric { get; set; }
        }
    
        [Serializable]
        public class CustomMetric
        {
            [JsonProperty("name")]
            public string Name { get; set; }
        }
    
  10. 請務必先在Visual Studio儲存變更,再返回 Unity

第 10 章 - 建立行動類別

移動腳本是您需要建立的下一個腳本。 下列為其負責的工作:

  • 根據相機往往的方向移動主相機。
  • 將所有其他腳本新增至場景物件。

若要建立文稿:

  1. 按兩下 [文稿] 資料夾,以開啟它。

  2. 在 [腳本] 資料夾內按下滑鼠右鍵,按兩下 [建立>C# 腳本]。 將腳本 命名為 [移動]。

  3. 按兩下腳本以使用 Visual Studio加以開啟。

  4. 將現有的程式碼取代為下列程式碼:

        using UnityEngine;
        using UnityEngine.XR.WSA.Input;
    
        public class Movement : MonoBehaviour
        {
            /// <summary>
            /// The rendered object representing the right controller.
            /// </summary>
            public GameObject Controller;
    
            /// <summary>
            /// The movement speed of the user.
            /// </summary>
            public float UserSpeed;
    
            /// <summary>
            /// Provides whether source updates have been registered.
            /// </summary>
            private bool _isAttached = false;
    
            /// <summary>
            /// The chosen controller hand to use. 
            /// </summary>
            private InteractionSourceHandedness _handness = InteractionSourceHandedness.Right;
    
            /// <summary>
            /// Used to calculate and proposes movement translation.
            /// </summary>
            private Vector3 _playerMovementTranslation;
    
            private void Start()
            {
                // You are now adding components dynamically 
                // to ensure they are existing on the correct object  
    
                // Add all camera related scripts to the camera. 
                Camera.main.gameObject.AddComponent<Gaze>();
                Camera.main.gameObject.AddComponent<ObjectTrigger>();
    
                // Add all other scripts to this object.
                gameObject.AddComponent<ApplicationInsightsTracker>();
                gameObject.AddComponent<DataFromAnalytics>();
            }
    
            // Update is called once per frame
            void Update()
            {
    
            }
        }
    
  5. Movement類別的空白Update() 方法下方,插入下列方法,讓使用者使用手部控制器在虛擬空間中移動:

        /// <summary>
        /// Used for tracking the current position and rotation of the controller.
        /// </summary>
        private void UpdateControllerState()
        {
    #if UNITY_WSA && UNITY_2017_2_OR_NEWER
            // Check for current connected controllers, only if WSA.
            string message = string.Empty;
    
            if (InteractionManager.GetCurrentReading().Length > 0)
            {
                foreach (var sourceState in InteractionManager.GetCurrentReading())
                {
                    if (sourceState.source.kind == InteractionSourceKind.Controller && sourceState.source.handedness == _handness)
                    {
                        // If a controller source is found, which matches the selected handness, 
                        // check whether interaction source updated events have been registered. 
                        if (_isAttached == false)
                        {
                            // Register events, as not yet registered.
                            message = "<color=green>Source Found: Registering Controller Source Events</color>";
                            _isAttached = true;
    
                            InteractionManager.InteractionSourceUpdated += InteractionManager_InteractionSourceUpdated;
                        }
    
                        // Update the position and rotation information for the controller.
                        Vector3 newPosition;
                        if (sourceState.sourcePose.TryGetPosition(out newPosition, InteractionSourceNode.Pointer) && ValidPosition(newPosition))
                        {
                            Controller.transform.localPosition = newPosition;
                        }
    
                        Quaternion newRotation;
    
                        if (sourceState.sourcePose.TryGetRotation(out newRotation, InteractionSourceNode.Pointer) && ValidRotation(newRotation))
                        {
                            Controller.transform.localRotation = newRotation;
                        }
                    }
                }
            }
            else
            {
                // Controller source not detected. 
                message = "<color=blue>Trying to detect controller source</color>";
    
                if (_isAttached == true)
                {
                    // A source was previously connected, however, has been lost. Disconnected
                    // all registered events. 
    
                    _isAttached = false;
    
                    InteractionManager.InteractionSourceUpdated -= InteractionManager_InteractionSourceUpdated;
    
                    message = "<color=red>Source Lost: Detaching Controller Source Events</color>";
                }
            }
    
            if(message != string.Empty)
            {
                Debug.Log(message);
            }
    #endif
        }
    
        /// <summary>
        /// This registered event is triggered when a source state has been updated.
        /// </summary>
        /// <param name="obj"></param>
        private void InteractionManager_InteractionSourceUpdated(InteractionSourceUpdatedEventArgs obj)
        {
            if (obj.state.source.handedness == _handness)
            {
                if(obj.state.thumbstickPosition.magnitude > 0.2f)
                {
                    float thumbstickY = obj.state.thumbstickPosition.y;
    
                    // Vertical Input.
                    if (thumbstickY > 0.3f || thumbstickY < -0.3f)
                    {
                        _playerMovementTranslation = Camera.main.transform.forward;
                        _playerMovementTranslation.y = 0;
                        transform.Translate(_playerMovementTranslation * UserSpeed * Time.deltaTime * thumbstickY, Space.World);
                    }
                }
            }
        }
    
        /// <summary>
        /// Check that controller position is valid. 
        /// </summary>
        /// <param name="inputVector3">The Vector3 to check</param>
        /// <returns>The position is valid</returns>
        private bool ValidPosition(Vector3 inputVector3)
        {
            return !float.IsNaN(inputVector3.x) && !float.IsNaN(inputVector3.y) && !float.IsNaN(inputVector3.z) && !float.IsInfinity(inputVector3.x) && !float.IsInfinity(inputVector3.y) && !float.IsInfinity(inputVector3.z);
        }
    
        /// <summary>
        /// Check that controller rotation is valid. 
        /// </summary>
        /// <param name="inputQuaternion">The Quaternion to check</param>
        /// <returns>The rotation is valid</returns>
        private bool ValidRotation(Quaternion inputQuaternion)
        {
            return !float.IsNaN(inputQuaternion.x) && !float.IsNaN(inputQuaternion.y) && !float.IsNaN(inputQuaternion.z) && !float.IsNaN(inputQuaternion.w) && !float.IsInfinity(inputQuaternion.x) && !float.IsInfinity(inputQuaternion.y) && !float.IsInfinity(inputQuaternion.z) && !float.IsInfinity(inputQuaternion.w);
        }   
    
  6. 最後,在 Update() 方法內新增方法呼叫。

        // Update is called once per frame
        void Update()
        {
            UpdateControllerState();
        }
    
  7. 請務必先在Visual Studio儲存變更,再返回 Unity

第 11 章 - 設定文本參考

在本章中,您需要將 移動 腳本 放在相機父 系上,並設定其參考目標。 然後,該腳本會處理放置其他腳本所需的位置。

  1. 從 [項目面板] 中的 [腳本] 資料夾,將 [移動] 腳本拖曳至 [階層面板] 中的 Camera Parent 物件。

    [專案] 和 [階層] 面板的螢幕快照。已醒目提示移動。

  2. 按兩下相機 父系。 在 [階層面板] 中,將 [右手] 物件從 [階層面板] 拖曳至 [偵測器面板] 中的參考目標 Controller。 將 [ 使用者速度 ] 設定為 5,如下圖所示。

    顯示 [階層] 和 [偵測器] 面板的螢幕快照。這兩個面板上的右手連接線。

第 12 章 - 建置 Unity 專案

此專案 Unity 區段所需的所有項目現在都已完成,因此是時候從 Unity 建置它了。

  1. 流覽至 [建置設定]、[檔案>建置設定]。

  2. 從 [ 建置設定] 視窗中,按兩下 [ 置]。

    [建置設定] 視窗的螢幕快照,其中顯示 [建置中的場景]。

  3. 隨即會顯示 檔案總管 視窗,提示您輸入組建的位置。 建立新資料夾(按兩下 左上角的[新增資料夾 ],並將它命名為 BUILDS

    檔案總管 的螢幕快照,其中顯示醒目提示的 [組建] 資料夾。

    1. 開啟新的 BUILDS 資料夾,然後建立另一個資料夾(再次使用資料夾),並將它命名為MR_Azure_Application_Insights

      [檔案總管] 的螢幕快照,其中顯示MR_Azure_Insights資料夾。

    2. 選取 MR_Azure_Application_Insights 資料夾后,按兩下 [ 選取資料夾]。 專案需要一分鐘左右的時間才能建置。

  4. 在 [建置] 之後檔案總管 會顯示新專案的位置。

第 13 章 - 將MR_Azure_Application_Insights應用程式部署至您的電腦

若要在 本機電腦上部署MR_Azure_Application_Insights 應用程式:

  1. 在 Visual Studio 中開啟MR_Azure_Application_Insights應用程式的解決方案檔案

  2. 在 [解決方案平臺] 中,選取 [x86] [本機計算機]。

  3. 在 [解決方案組態] 中,選取 [偵錯]。

    Visual Studio 解決方案組態畫面的螢幕快照,其中顯示功能表欄中的 [偵錯]。

  4. 移至 [ 建置] 功能表 ,然後按下 [ 部署解決方案 ] 將應用程式側載至您的電腦。

  5. 您的應用程式現在應該會出現在已安裝的應用程式清單中,準備好啟動。

  6. 啟動混合實境應用程式。

  7. 在場景周圍移動、接近物件並查看對象,當 Azure Insight Service 收集到足夠的事件數據時,它會將已接近最綠色的物件設定為綠色。

重要

雖然服務收集事件和計量的平均等候時間大約需要 15 分鐘,但在某些情況下,最多可能需要 1 小時的時間。

第 14 章 - Application Insights 服務入口網站

一旦您在場景周圍漫遊並注視數個物件,您就可以在Application Insights服務入口網站中看到收集的數據。

  1. 返回 Application Insights 服務入口網站。

  2. 選取 [計量總管]。

    顯示選項清單的 MyNewInsight 面板螢幕快照。計量總管列在 [調查] 區段中。

  3. 它會在索引標籤中開啟,其中包含圖形,代表 與應用程式相關的事件和計量 。 如上所述,數據可能需要一些時間(最多 1 小時)才會顯示在圖形中

    [計量總管] 的螢幕快照,其中顯示事件和計量圖表。

  4. 選取 [依應用程式版本排序的事件總數] 中的 [事件] 列,以查看事件名稱的詳細明細。

    [搜尋] 面板的螢幕快照,其中顯示自定義事件篩選的結果。

您已完成 Application Insights Service 應用程式

恭喜,您建置了混合實境應用程式,利用 Application Insights Service 來監視應用程式內的用戶活動。

課程歡迎畫面。

Bonus 練習

練習 1

請嘗試繁衍,而不是手動建立 ObjectInScene 物件,並在腳本內的平面上設定其座標。 如此一來,您可以詢問 Azure 最受歡迎對象是什麼(從注視或鄰近結果中),並繁衍其中一個 額外的 物件。

練習 2

依時間排序 Application Insights 結果,以便取得最相關的數據,並在應用程式中實作該時間敏感數據。