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

注意

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

The Mixed Reality Academy tutorial welcome screen.

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

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

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

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

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

裝置支援

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

注意

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

必要條件

注意

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

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

在您開始使用 Intune 之前

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

警告

請注意,移至應用程式的資料Insights需要一點時間,因此請耐心等候。 如果您想要檢查服務是否已收到您的資料,請參閱 第 14 章,其中會示範如何流覽入口網站。

第 1 章 - Azure 入口網站

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

  1. 登入 Azure 入口網站

    注意

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

  2. 登入之後,按一下左上角的 [新增],然後搜尋[應用程式Insights],然後按一下Enter

    注意

    在較新的入口網站中, [新增 ] 這個字可能已取代為 建立資源

    Screenshot showing the Azure Portal, Insight is highlighted in the Everything pane.

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

    Screenshot of the Application Insights screen, Create is highlighted.

  4. 按一下 [ 建立]:

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

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

    3. 選取適當的 訂用帳戶

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

      如果您想要深入瞭解 Azure 資源群組,請 造訪資源群組一文

    5. 選取 [位置] 。

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

    7. 選取 [建立]。

      Screenshot of the Application Insights window. Name and application type are highlighted.

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

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

    Screenshot showing a portion of the menu ribbon, the notification icon is highlighted.

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

    Screenshot showing the Deployment succeeded dialog, Go to resource is highlighted.

  8. 按一下通知中的 [ 移至資源 ] 按鈕,以探索新的服務實例。 系統會將您帶至新的應用程式Insights服務實例。

    Screenshot showing the Application Insights Service instance where the instance name is MyNewInsight.

    注意

    讓此網頁保持開啟且容易存取,您通常會回到這裡以查看收集的資料。

    重要

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

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

    Screenshot showing service functions, Properties is highlighted in the Configure section and Instrumentation Key is highlighted in the main pane.

  10. 在 [ 屬性] 下方,您會看到需要按一下的 API 存取。 右側的面板會提供您應用程式的 [應用程式識別碼 ]。

    Screenshot showing service functions, A P I Access is highlighted. Create A P I Key and Application I D are highlighted in the main pane.

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

    Screenshot showing the Create A P I key panel.

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

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

    Screenshot of the Create A P I key panel showing the new service key information.

    警告

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

第 2 章 - 設定 Unity 專案

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

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

    Screenshot of the Unity projects window. No project information is shown.

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

    Screenshot of the Unity new projects window, showing project information.

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

    Screenshot showing Visual Studio is set up as the external script editor.

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

    Screenshot of the Build Settings window, showing the Platform selection list. Universal Windows Platform is selected.

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

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

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

    2. 組建類型 設定為 D3D

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

    4. [建置並執行 ] 設定為 [本機電腦]

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

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

        Screenshot of the Build Settings window, Add Open Scenes is selected.

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

        Screenshot of the Save Scene window, the Scenes folder is selected.

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

        Screenshot of the Save Scene window with the file name entered.

  6. 組建設定中的其餘設定現在應保留為預設值。

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

    Screenshot of the Inspector tab showing Player Settings.

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

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

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

      2. 腳本後端 應該是 .NET

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

      Screenshot of the Inspector tab showing details in the configuration section of Other Settings.

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

      • InternetClient

        Screenshot of the Capabilities list, Internet client is checked.

    3. 在面板下方,在XR 設定 (中找到發佈設定) ,勾選[支援虛擬實境],確定已新增Windows Mixed Reality SDK

      Screenshot of the X R Settings section, Virtual Reality Supported is checked.

  9. 回到[建置設定],Unity C# 專案不再呈現灰色;請勾選此旁邊的核取方塊。

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

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

第 3 章 - 匯入 Unity 套件

重要

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

重要

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

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

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

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

    Screenshot of the Import Unity Package dialog box showing all items checked.

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

  4. 移至[Project檢視] 中 [外掛程式] 底下的 [Insights] 資料夾,然後只選取下列外掛程式:

    • Microsoft.ApplicationInsights

    Screenshot of the Project panel, the Insights folder is open.

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

    Screenshot of the Inspector panel showing Editor and Standalone checked.

    注意

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

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

    Screenshot of the Inspector panel showing W S A Player checked.

  7. 您現在必須遵循 步驟 4-6,但改為針對 Newtonsoft 外掛程式。 請參閱下列螢幕擷取畫面,以瞭解結果的外觀。

    Screenshot of four views of the Project and Inspector panels showing the results of setting up the Newtonsoft folder and plugin selections.

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

在本章中,您將設定相機和控制項,讓使用者在場景中查看和移動。

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

    Screenshot of the Hierarchy panel, Create Empty is selected.

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

    Screenshot of the Hierarchy panel with Camera Parent selected. The Inspector panel

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

  4. 將球體重新命名為 右手

  5. 將右側的 轉換小數位數 設定為 0.1、0.1、0.1

    Screenshot of the Hierarchy and Inspector panels, the Transform section on the Inspector panel is highlighted.

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

    Screenshot of the Inspector panel, the gear icon and Remove Component are highlighted in the Sphere Collider section.

  7. 在 [階層] 面板中,將Main 相機Right Hand物件拖曳至相機 Parent物件。

    Screenshot of the Hierarchy panel with Main Camera selected, the Inspector panel shows Main Camera checked.

  8. Main 相機Right Hand物件的[轉換位置] 設定為0、0、0

    Screenshot of the Hierarchy panel with Main Camera selected, Transform settings are highlighted in the Inspector panel.

    Screenshot of the Hierarchy panel with Right Hand selected, Transform settings are highlighted in the Inspector panel.

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

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

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

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

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

    Screenshot of the Scene, Hierarchy, and Inspector panels. The Transform section in the Inspector panel is highlighted.

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

    Screenshot of the Project panel showing Create and Folder highlighted.Screenshot of the Project panel. Materials is highlighted in the Assets pane.

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

    Screenshot of the Project panel showing Create and Material highlighted.Screenshot of the Project panel. Blue is highlighted in the Materials pane.

  6. 選取新的 Blue 材質後,請查看 Inspector,然後按一下矩形視窗與 Albedo。 選取藍色 (下圖的 十六進位色彩:#3592FFFF) 。 選擇之後,按一下 [關閉] 按鈕。

    Screenshot of the Inspector panel. The color section is highlighted.

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

    Screenshot of the Scene panel showing the new material from the Materials folder.

  8. 以滑鼠右鍵按一下 [階層面板] 中的空白區域,然後在[3D 物件] 上按一下 [內文]。

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

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

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

    Screenshot of the Scene, Hierarchy, and Inspector panels. Capsule is selected in the Hierarchy panel.

    注意

    這些位置值是建議。 您可以自由地將物件的位置設定為您想要的任何位置,不過,如果物件距離與相機距離太遠,應用程式的使用者會比較容易。

  11. 當您的應用程式執行時,它必須能夠識別場景中的物件,才能達成此目的,必須加以標記。 選取其中一個物件,然後在 [ 偵測器 ] 面板中按一下 [ 新增標籤...],這會交換 Inspector 與 [ 標記 & 層 ] 面板。

    Screenshot of the Inspector panel showing the Add Tag option highlighted.Screenshot of the Inspector panel showing Tags and Layers highlighted.

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

    Screenshot of the Inspector panel with Tags and Layers selected. The New Tag Name dialog is highlighted.

    警告

    如果您針對標籤使用不同的名稱,則必須確保此變更也會在場景中建立 DataFromAnalyticsObjectTriggerGaze、腳本,以便在您的場景中找到及偵測到您的物件。

  13. 建立標記之後,您現在必須將它套用至這三個物件。 從 [階層],按住 Shift 鍵,然後按一下 [封裝]、[ Cube] 和 [ Sphere] 物件,然後在 [偵測器] 中,按一下 [ 標籤] 旁的下拉式功能表,然後按一下您建立的 ObjectInScene 標籤。

    Screenshot of the Inspector panel, an arrow points to Tag. The Untagged menu shows Untagged checked and ObjectInScene is selected.Screenshot showing two menus with Create and Folder highlighted.

第 6 章 - 建立 ApplicationInsightsTracker 類別

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

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

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

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

若要建立此類別:

  1. 以滑鼠右鍵按一下[Project面板],然後按一下 [建立>資料夾]。 將資料夾命名為 Scripts

    Screenshot of the Projects panel. The Scripts folder icon is highlighted in the Assets pane.Screenshot showing menu options where the options, Create and C# Script are selected.

  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;
    

    注意

    使用 Azure 入口網站中的服務金鑰,適當地設定instrumentationKey、applicationId 和API_Key值,如步驟 9 第 9所述。

  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 章 - 建立注視腳本

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

  1. 按兩下 [腳本 ] 資料夾,將其開啟。

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

  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,負責:

  • 將衝突所需的元件新增至 Main 相機。
  • 偵測相機是否接近標記為 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服務實例通訊。
  • 根據場景中具有最高事件計數的物件來排序物件。
  • 將最接近物件的材質色彩變更為 綠色

建立指令碼:

  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。 這個方法負責要求在Application Insights內呼叫與特定GameObject對應的事件次數。 當所有傳送的查詢都傳回時,會呼叫 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 章 - 建立 Movement 類別

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

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

建立指令碼:

  1. 按兩下 [腳本 ] 資料夾,將其開啟。

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

  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 章 - 設定腳本參考

在本章中,您必須將移動腳本放在相機 Parent 上,並設定其參考目標。 該腳本接著會處理放置其他腳本所需的位置。

  1. [Project面板] 的 [腳本] 資料夾中,將[移動] 腳本拖曳至位於 [階層面板] 中的相機 Parent物件。

    Screenshot of the Project and Hierarchy panels. Movement is highlighted.

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

    Screenshot showing the Hierarchy and Inspector panels. A line connects Right Hand on both panels.

第 12 章 - 建置 Unity 專案

此專案的 Unity 區段所需的所有專案現在都已完成,因此現在可以從 Unity 建置它。

  1. 流覽至組建設定, (檔案>建置設定) 。

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

    Screenshot of the Build Settings window showing Scenes In Build.

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

    Screenshot of File Explorer showing the Builds folder highlighted.

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

      Screenshot of File explorer showing the MR_Azure_Insights folder.

    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. [方案組態] 中,選取 [ 偵錯]。

    Screenshot of the the Visual Studio Solution Configuration screen showing Debug in the menu bar.

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

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

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

  7. 在場景四處移動、接近物件並查看它們時, 當 Azure Insight Service 收集足夠的事件資料時,它會將已接近最綠色的物件設定為綠色。

重要

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

第 14 章 - 應用程式Insights服務入口網站

一旦您在場景中漫遊並注視數個物件,您就可以在應用程式Insights服務入口網站中看到收集的資料。

  1. 返回至您的應用程式Insights服務入口網站。

  2. 選取 [計量瀏覽器]

    Screenshot of the MyNewInsight panel showing the list of options. Metrics Explorer is listed in the Investigate section.

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

    Screenshot of the Metrics Explorer showing the events and metrics graph.

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

    Screenshot of the Search panel showing the results of a custom event filter.

您已完成應用程式Insights服務應用程式

恭喜,您已建置混合實境應用程式,利用應用程式Insights服務來監視應用程式內的使用者活動。

Course welcome screen.

Bonus 練習

練習 1

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

練習 2

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