HoloLens (第 1 代) 和 Azure 311 - Microsoft Graph

注意

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

在此課程中,您將瞭解如何使用 Microsoft Graph ,在混合實境應用程式中使用安全驗證登入您的 Microsoft 帳戶。 接著,您會在應用程式介面中擷取並顯示排程的會議。

顯示應用程式介面中已排程會議的螢幕快照。

Microsoft Graph 是一組 API,其設計目的是要能夠存取許多 Microsoft 服務。 Microsoft 將 Microsoft Graph 描述為依關聯性連線的資源矩陣,這表示它可讓應用程式存取各種連線的用戶數據。 如需詳細資訊,請流覽 Microsoft Graph 頁面

開發將包含建立應用程式,其中會指示使用者注視,然後點選球體,提示使用者安全地登入 Microsoft 帳戶。 登入其帳戶之後,用戶就能夠看到排程當天的會議清單。

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

  1. 使用點選手勢,點選物件,提示使用者登入 Microsoft 帳戶 (移出應用程式以登入,然後再次回到應用程式) 。
  2. 檢視排定當天的會議清單。

在您的應用程式中,您可以瞭解如何將結果與設計整合。 本課程旨在教導您如何將 Azure 服務與您的 Unity 專案整合。 您的工作是使用您從本課程獲得的知識來增強混合實境應用程式。

裝置支援

課程 HoloLens 沉浸式頭戴裝置
MR 和 Azure 311:Microsoft Graph ✔️

必要條件

注意

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

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

開始之前

  1. 為了避免建置此專案時發生問題,強烈建議您在根資料夾或近根資料夾中建立本教學課程中所提及的專案, (長資料夾路徑可能會導致建置時間) 的問題。
  2. 設定及測試 HoloLens。 如果您需要設定 HoloLens 的支援, 請務必造訪 HoloLens 設定文章
  3. 開始開發新的 HoloLens 應用程式時,最好執行校正和感測器微調 (有時有助於為每個使用者執行這些工作) 。

如需校正的說明,請遵循此 連結至 HoloLens 校正文章

如需感測器微調的說明,請遵循此 連結至 HoloLens 感測器微調文章

第 1 章 - 在應用程式註冊入口網站中建立您的應用程式

首先,您必須在 應用程式註冊入口網站中建立和註冊您的應用程式。

在本章中,您也會找到服務密鑰,可讓您呼叫 Microsoft Graph 來存取您的帳戶內容。

  1. 流覽至 Microsoft 應用程式註冊入口網站 ,並使用您的 Microsoft 帳戶登入。 登入之後,系統會將您重新導向至 應用程式註冊入口網站

  2. 在 [ 我的應用程式 ] 區段中,按兩下 [ 新增應用程式] 按鈕。

    顯示選取 [新增應用程式的位置] 的螢幕快照。

    重要

    應用程式註冊入口網站看起來可能會有所不同,視您先前是否曾使用過 Microsoft Graph 而定。 下列螢幕快照顯示這些不同版本。

  3. 新增應用程式的名稱,然後按兩下 [ 建立]。

    顯示新增應用程式名稱位置的螢幕快照。

  4. 建立應用程式之後,系統會將您重新導向至應用程式主頁面。 複製 應用程式識別碼 ,並確定在安全的地方記下此值,您很快就會在程式碼中使用它。

    顯示檢視應用程式識別碼位置的螢幕快照。

  5. 在 [ 平臺 ] 區段中,確定 [ 原生應用程式 ] 已顯示。 如果未按兩下 [新增平臺],然後選取 [原生應用程式]。

    醒目提示 [原生應用程式] 區段的螢幕快照。

  6. 在相同的頁面中向下捲動,並在名為 Microsoft Graph 許可權 的區段中,您必須為應用程式新增其他許可權。 按兩下 [委派的許可權] 旁的 [新增]。

    此螢幕快照顯示 [委派的許可權] 旁要選取 [新增] 的位置。

  7. 由於您想要讓應用程式存取使用者的 [行事歷],請核取名為 Calendars.Read 的方塊,然後按兩下 [ 確定]。

    顯示 [Calendars.Read] 複選框的螢幕快照。

  8. 捲動至底部,然後按兩下 [ 儲存] 按鈕。

    顯示選取 [儲存位置] 的螢幕快照。

  9. 系統會確認您的儲存,而且您可以從 應用程式註冊入口網站註銷。

第 2 章 - 設定 Unity 專案

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

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

    顯示 Unity 介面的螢幕快照。

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

    顯示選取 [建立專案] 位置的螢幕快照。

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

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

  4. 移至 [檔案>建置設定],然後選取 [通用 Windows 平台],然後按兩下 [切換平臺] 按鈕以套用您的選擇。

    顯示選取 [切換平臺] 位置的螢幕快照。

  5. 仍在 [檔案>建置設定] 中,請確定:

    1. 目標裝置 設定為 HoloLens

    2. 組建類型 設定為 D3D

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

    4. Visual Studio 版本 設定為 [最新安裝]

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

    6. 儲存場景,並將其新增至組建。

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

        顯示選取 [新增開啟場景] 位置的螢幕快照。

      2. 為此和任何未來場景建立新資料夾。 選取 [ 新增資料夾 ] 按鈕,以建立新的資料夾,並將其命名為 Scenes

        顯示命名新資料夾位置的螢幕快照。

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

        顯示輸入檔名位置的螢幕快照。

        重要

        請注意,您必須將 Unity 場景儲存在 Assets 資料夾中,因為它們必須與 Unity 專案相關聯。 建立場景資料夾 (和其他類似的資料夾) 是建構 Unity 專案的一般方式。

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

  6. 在 [ 建置設定 ] 視窗中,按兩下 [ 播放程序設定 ] 按鈕,這會在 Inspector 所在的空間中開啟相關的面板。

    顯示 [播放程序設定] 對話框的螢幕快照。

  7. 在此面板中,必須驗證一些設定:

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

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

      2. 腳本後端 應該是 .NET

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

        顯示檢查 API 相容性層級位置的螢幕快照。

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

      • InternetClient

        顯示選取 InternetClient 選項位置的螢幕快照。

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

      顯示新增 Windows Mixed Reality SDK 位置的螢幕快照。

  8. 回到 [建置設定], Unity C# 專案 不再呈現灰色;核取此旁的複選框。

  9. 關閉 [組建設定] 視窗。

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

第 3 章 - 在 Unity 中匯入連結庫

重要

如果您想要略過本課程的 Unity 設定 元件,並直接進入程式碼,請隨意下載此 Azure-MR-311.unitypackage,將其匯入您的專案作為 自定義套件,然後從 第 5 章繼續進行。

若要在 Unity 中使用 Microsoft Graph ,您必須使用 Microsoft.Identity.Client DLL。 不過,您可以在建置 Unity 專案之後新增 NuGet 套件 (,這表示在建置後) 編輯項目之後,可以使用 Microsoft Graph SDK。 將所需的 DLL 直接匯入 Unity 會比較簡單。

注意

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

若要將 Microsoft Graph 匯入您自己的專案, 請下載 MSGraph_LabPlugins.zip 檔案。 此套件是使用已測試的連結庫版本所建立。

如果您想要深入瞭解如何將自定義 DLL 新增至 Unity 專案, 請遵循此連結

若要匯入套件:

  1. 使用 [ 資產>匯入套件>自定義套件 ] 功能表選項,將 Unity 套件新增至 Unity。 選取您剛才下載的套件。

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

    顯示 [外掛程式] 底下所選組態參數的螢幕快照。

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

  4. 移至 [項目面板] 中 [外掛程式] 底下的 MSGraph 資料夾,然後選取名為 Microsoft.Identity.Client 的外掛程式。

    顯示 Microsoft.Identity.Client 外掛程式的螢幕快照。

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

    顯示確認未核取任何平臺和 WSAPlayer 位置的螢幕快照。

    注意

    標記這些外掛程式會將這些外掛程式設定為只在 Unity 編輯器中使用。 WSA 資料夾中有一組不同的 DLL,將在專案從 Unity 匯出為通用 Windows 應用程式之後使用。

  6. 接下來,您必須在 MSGraph 資料夾中開啟 WSA 資料夾。 您會看到您剛才設定的相同檔案複本。 選取檔案,然後在偵測器中:

    • 確定未核取任何平臺,而且只會檢查WSAPlayer

    • 確定 SDK 已設定為 UWP,並將 腳本後端 設定為 Dot Net

    • 確定已核取[不要處理]。

      顯示已選取 [不要處理] 的螢幕快照。

  7. 按一下 [套用]。

第 4 章 - 相機設定

在本章中,您將設定場景的主要相機:

  1. 在 [ 階層面板] 中,選取 [主要相機]。

  2. 選取之後,您將能夠在 [偵測器] 面板中看到主要相機的所有元件。

    1. Camera 對象必須命名為 Main Camera (記下拼字!)

    2. 主要相機 標籤 必須設定為 MainCamera (記下拼字!)

    3. 確定 [轉換位置 ] 設定為 0、0、0

    4. 清除旗標 設定為 純色

    5. 將相機元件 的背景色彩 設定為 黑色、Alpha 0 (十六進位代碼: #0000000000)

      醒目提示設定背景色彩位置的螢幕快照。

  3. 階層面板中的最後一個對象結構應該像下圖所示:

    顯示階層面板中最終對象結構的螢幕快照。

第 5 章 - 建立 MeetingsUI 類別

您需要建立的第一個腳本是 MeetingsUI,負責裝載和填入應用程式 UI (歡迎訊息、指示和會議詳細數據) 。

若要建立此類別:

  1. 以滑鼠右鍵按兩下 [項目面板] 中的 [資產] 資料夾,然後選取 [建立>資料夾]。 將資料夾命名為 Scripts

    顯示 [資產] 資料夾位置的螢幕快照。顯示建立 [腳稿] 資料夾位置的螢幕快照。

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

    顯示建立 MeetingsUI 資料夾位置的螢幕快照。

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

  4. 插入下列命名空間:

    using System;
    using UnityEngine;
    
  5. 在類別內插入下列變數:

        /// <summary>
        /// Allows this class to behave like a singleton
        /// </summary>
        public static MeetingsUI Instance;
    
        /// <summary>
        /// The 3D text of the scene
        /// </summary>
        private TextMesh _meetingDisplayTextMesh;
    
  6. 然後取代 Start () 方法,並新增 Awake () 方法。 當 類別初始化時,將會呼叫下列專案:

        /// <summary>
        /// Called on initialization
        /// </summary>
        void Awake()
        {
            Instance = this;
        }
    
        /// <summary>
        /// Called on initialization, after Awake
        /// </summary>
        void Start ()
        {
            // Creating the text mesh within the scene
            _meetingDisplayTextMesh = CreateMeetingsDisplay();
        }
    
  7. 新增負責建立 Meetings UI 的方法,並在要求時填入目前的會議:

        /// <summary>
        /// Set the welcome message for the user
        /// </summary>
        internal void WelcomeUser(string userName)
        {
            if(!string.IsNullOrEmpty(userName))
            {
                _meetingDisplayTextMesh.text = $"Welcome {userName}";
            }
            else 
            {
                _meetingDisplayTextMesh.text = "Welcome";
            }
        }
    
        /// <summary>
        /// Set up the parameters for the UI text
        /// </summary>
        /// <returns>Returns the 3D text in the scene</returns>
        private TextMesh CreateMeetingsDisplay()
        {
            GameObject display = new GameObject();
            display.transform.localScale = new Vector3(0.03f, 0.03f, 0.03f);
            display.transform.position = new Vector3(-3.5f, 2f, 9f);
            TextMesh textMesh = display.AddComponent<TextMesh>();
            textMesh.anchor = TextAnchor.MiddleLeft;
            textMesh.alignment = TextAlignment.Left;
            textMesh.fontSize = 80;
            textMesh.text = "Welcome! \nPlease gaze at the button" +
                "\nand use the Tap Gesture to display your meetings";
    
            return textMesh;
        }
    
        /// <summary>
        /// Adds a new Meeting in the UI by chaining the existing UI text
        /// </summary>
        internal void AddMeeting(string subject, DateTime dateTime, string location)
        {
            string newText = $"\n{_meetingDisplayTextMesh.text}\n\n Meeting,\nSubject: {subject},\nToday at {dateTime},\nLocation: {location}";
    
            _meetingDisplayTextMesh.text = newText;
        }
    
  8. 在返回 Unity 之前,請先刪除 Update () 方法,並將 變更儲存 在 Visual Studio 中。

第 6 章 - 建立 Graph 類別

要建立的下一個腳本是 Graph 腳本。 此腳本負責進行呼叫,以驗證使用者,並從使用者的行事歷擷取當天的排程會議。

若要建立此類別:

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

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

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

  4. 插入下列命名空間:

    using System.Collections.Generic;
    using UnityEngine;
    using Microsoft.Identity.Client;
    using System;
    using System.Threading.Tasks;
    
    #if !UNITY_EDITOR && UNITY_WSA
    using System.Net.Http;
    using System.Net.Http.Headers;
    using Windows.Storage;
    #endif
    

    重要

    您會注意到此腳本中的程式碼部分會包裝在 先行編譯指示詞周圍,這是為了避免在建置Visual Studio解決方案時發生連結庫的問題。

  5. 刪除 Start () Update () 方法,因為不會使用這些方法。

  6. Graph 類別之外,插入下列物件,這是還原串行化代表每日排程會議的 JSON 物件所需的物件:

    /// <summary>
    /// The object hosting the scheduled meetings
    /// </summary>
    [Serializable]
    public class Rootobject
    {
        public List<Value> value;
    }
    
    [Serializable]
    public class Value
    {
        public string subject { get; set; }
        public StartTime start { get; set; }
        public Location location { get; set; }
    }
    
    [Serializable]
    public class StartTime
    {
        public string dateTime;
    
        private DateTime? _startDateTime;
        public DateTime StartDateTime
        {
            get
            {
                if (_startDateTime != null)
                    return _startDateTime.Value;
                DateTime dt;
                DateTime.TryParse(dateTime, out dt);
                _startDateTime = dt;
                return _startDateTime.Value;
            }
        }
    }
    
    [Serializable]
    public class Location
    {
        public string displayName { get; set; }
    }
    
  7. Graph 類別內,新增下列變數:

        /// <summary>
        /// Insert your Application Id here
        /// </summary>
        private string _appId = "-- Insert your Application Id here --";
    
        /// <summary>
        /// Application scopes, determine Microsoft Graph accessibility level to user account
        /// </summary>
        private IEnumerable<string> _scopes = new List<string>() { "User.Read", "Calendars.Read" };
    
        /// <summary>
        /// Microsoft Graph API, user reference
        /// </summary>
        private PublicClientApplication _client;
    
        /// <summary>
        /// Microsoft Graph API, authentication
        /// </summary>
        private AuthenticationResult _authResult;
    
    

    注意

    appId 值變更為您在步驟 4 第 1 章中記下的應用程式識別碼。 此值應該與應用程式 註冊 入口網站中應用程式註冊頁面中顯示的值相同。

  8. Graph 類別中,新增 SignInAsync () AquireTokenAsync () 方法,以提示使用者插入登入認證。

        /// <summary>
        /// Begin the Sign In process using Microsoft Graph Library
        /// </summary>
        internal async void SignInAsync()
        {
    #if !UNITY_EDITOR && UNITY_WSA
            // Set up Grap user settings, determine if needs auth
            ApplicationDataContainer localSettings = ApplicationData.Current.LocalSettings;
            string userId = localSettings.Values["UserId"] as string;
            _client = new PublicClientApplication(_appId);
    
            // Attempt authentication
            _authResult = await AcquireTokenAsync(_client, _scopes, userId);
    
            // If authentication is successful, retrieve the meetings
            if (!string.IsNullOrEmpty(_authResult.AccessToken))
            {
                // Once Auth as been completed, find the meetings for the day
                await ListMeetingsAsync(_authResult.AccessToken);
            }
    #endif
        }
    
        /// <summary>
        /// Attempt to retrieve the Access Token by either retrieving
        /// previously stored credentials or by prompting user to Login
        /// </summary>
        private async Task<AuthenticationResult> AcquireTokenAsync(
            IPublicClientApplication app, IEnumerable<string> scopes, string userId)
        {
            IUser user = !string.IsNullOrEmpty(userId) ? app.GetUser(userId) : null;
            string userName = user != null ? user.Name : "null";
    
            // Once the User name is found, display it as a welcome message
            MeetingsUI.Instance.WelcomeUser(userName);
    
            // Attempt to Log In the user with a pre-stored token. Only happens
            // in case the user Logged In with this app on this device previously
            try
            {
                _authResult = await app.AcquireTokenSilentAsync(scopes, user);
            }
            catch (MsalUiRequiredException)
            {
                // Pre-stored token not found, prompt the user to log-in 
                try
                {
                    _authResult = await app.AcquireTokenAsync(scopes);
                }
                catch (MsalException msalex)
                {
                    Debug.Log($"Error Acquiring Token: {msalex.Message}");
                    return _authResult;
                }
            }
    
            MeetingsUI.Instance.WelcomeUser(_authResult.User.Name);
    
    #if !UNITY_EDITOR && UNITY_WSA
            ApplicationData.Current.LocalSettings.Values["UserId"] = 
            _authResult.User.Identifier;
    #endif
            return _authResult;
        }
    
  9. 新增下列兩種方法:

    1. BuildTodayCalendarEndpoint () ,它會建置指定日期和時間範圍的 URI,以擷取排程的會議。

    2. ListMeetingsAsync () ,其會向 Microsoft Graph 要求排程的會議。

        /// <summary>
        /// Build the endpoint to retrieve the meetings for the current day.
        /// </summary>
        /// <returns>Returns the Calendar Endpoint</returns>
        public string BuildTodayCalendarEndpoint()
        {
            DateTime startOfTheDay = DateTime.Today.AddDays(0);
            DateTime endOfTheDay = DateTime.Today.AddDays(1);
            DateTime startOfTheDayUTC = startOfTheDay.ToUniversalTime();
            DateTime endOfTheDayUTC = endOfTheDay.ToUniversalTime();
    
            string todayDate = startOfTheDayUTC.ToString("o");
            string tomorrowDate = endOfTheDayUTC.ToString("o");
            string todayCalendarEndpoint = string.Format(
                "https://graph.microsoft.com/v1.0/me/calendarview?startdatetime={0}&enddatetime={1}",
                todayDate,
                tomorrowDate);
    
            return todayCalendarEndpoint;
        }
    
        /// <summary>
        /// Request all the scheduled meetings for the current day.
        /// </summary>
        private async Task ListMeetingsAsync(string accessToken)
        {
    #if !UNITY_EDITOR && UNITY_WSA
            var http = new HttpClient();
    
            http.DefaultRequestHeaders.Authorization = 
            new AuthenticationHeaderValue("Bearer", accessToken);
            var response = await http.GetAsync(BuildTodayCalendarEndpoint());
    
            var jsonResponse = await response.Content.ReadAsStringAsync();
    
            Rootobject rootObject = new Rootobject();
            try
            {
                // Parse the JSON response.
                rootObject = JsonUtility.FromJson<Rootobject>(jsonResponse);
    
                // Sort the meeting list by starting time.
                rootObject.value.Sort((x, y) => DateTime.Compare(x.start.StartDateTime, y.start.StartDateTime));
    
                // Populate the UI with the meetings.
                for (int i = 0; i < rootObject.value.Count; i++)
                {
                    MeetingsUI.Instance.AddMeeting(rootObject.value[i].subject,
                                                rootObject.value[i].start.StartDateTime.ToLocalTime(),
                                                rootObject.value[i].location.displayName);
                }
            }
            catch (Exception ex)
            {
                Debug.Log($"Error = {ex.Message}");
                return;
            }
    #endif
        }
    
  10. 您現在已完成 Graph 文稿。 在 Visual Studio 中儲存變更,再返回 Unity。

第 7 章 - 建立 GazeInput 腳本

您現在將會建立 GazeInput。 這個類別會使用來自主要相機Raycast,向前投影,處理並持續追蹤使用者的注視。

建立指令碼:

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

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

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

  4. 變更命名空間程式代碼以符合下列命名空間程序代碼,以及在 GazeInput 類別上方新增 '[System.Serializable]' 卷標,以便串行化:

    using UnityEngine;
    
    /// <summary>
    /// Class responsible for the User's Gaze interactions
    /// </summary>
    [System.Serializable]
    public class GazeInput : MonoBehaviour
    {
    
  5. GazeInput 類別內,新增下列變數:

        [Tooltip("Used to compare whether an object is to be interacted with.")]
        internal string InteractibleTag = "SignInButton";
    
        /// <summary>
        /// Length of the gaze
        /// </summary>
        internal float GazeMaxDistance = 300;
    
        /// <summary>
        /// Object currently gazed
        /// </summary>
        internal GameObject FocusedObject { get; private set; }
    
        internal GameObject oldFocusedObject { get; private set; }
    
        internal RaycastHit HitInfo { get; private set; }
    
        /// <summary>
        /// Cursor object visible in the scene
        /// </summary>
        internal GameObject Cursor { get; private set; }
    
        internal bool Hit { get; private set; }
    
        internal Vector3 Position { get; private set; }
    
        internal Vector3 Normal { get; private set; }
    
        private Vector3 _gazeOrigin;
    
        private Vector3 _gazeDirection;
    
  6. 新增 CreateCursor () 方法,以在場景中建立 HoloLens 數據指標,並從 Start () 方法呼叫 方法:

        /// <summary>
        /// Start method used upon initialisation.
        /// </summary>
        internal virtual void Start()
        {
            FocusedObject = null;
            Cursor = CreateCursor();
        }
    
        /// <summary>
        /// Method to create a cursor object.
        /// </summary>
        internal GameObject CreateCursor()
        {
            GameObject newCursor = GameObject.CreatePrimitive(PrimitiveType.Sphere);
            newCursor.SetActive(false);
            // Remove the collider, so it doesn't block raycast.
            Destroy(newCursor.GetComponent<SphereCollider>());
            newCursor.transform.localScale = new Vector3(0.05f, 0.05f, 0.05f);
            Material mat = new Material(Shader.Find("Diffuse"));
            newCursor.GetComponent<MeshRenderer>().material = mat;
            mat.color = Color.HSVToRGB(0.0223f, 0.7922f, 1.000f);
            newCursor.SetActive(true);
    
            return newCursor;
        }
    
  7. 下列方法會啟用注視 Raycast,並追蹤焦點物件。

    /// <summary>
    /// Called every frame
    /// </summary>
    internal virtual void Update()
    {
        _gazeOrigin = Camera.main.transform.position;
    
        _gazeDirection = Camera.main.transform.forward;
    
        UpdateRaycast();
    }
    /// <summary>
    /// Reset the old focused object, stop the gaze timer, and send data if it
    /// is greater than one.
    /// </summary>
    private void ResetFocusedObject()
    {
        // Ensure the old focused object is not null.
        if (oldFocusedObject != null)
        {
            if (oldFocusedObject.CompareTag(InteractibleTag))
            {
                // Provide the 'Gaze Exited' event.
                oldFocusedObject.SendMessage("OnGazeExited", SendMessageOptions.DontRequireReceiver);
            }
        }
    }
    
        private void UpdateRaycast()
        {
            // Set the old focused gameobject.
            oldFocusedObject = FocusedObject;
            RaycastHit hitInfo;
    
            // Initialise Raycasting.
            Hit = Physics.Raycast(_gazeOrigin,
                _gazeDirection,
                out hitInfo,
                GazeMaxDistance);
                HitInfo = hitInfo;
    
            // Check whether raycast has hit.
            if (Hit == true)
            {
                Position = hitInfo.point;
                Normal = hitInfo.normal;
    
                // Check whether the hit has a collider.
                if (hitInfo.collider != null)
                {
                    // Set the focused object with what the user just looked at.
                    FocusedObject = hitInfo.collider.gameObject;
                }
                else
                {
                    // Object looked on is not valid, set focused gameobject to null.
                    FocusedObject = null;
                }
            }
            else
            {
                // No object looked upon, set focused gameobject to null.
                FocusedObject = null;
    
                // Provide default position for cursor.
                Position = _gazeOrigin + (_gazeDirection * GazeMaxDistance);
    
                // Provide a default normal.
                Normal = _gazeDirection;
            }
    
            // Lerp the cursor to the given position, which helps to stabilize the gaze.
            Cursor.transform.position = Vector3.Lerp(Cursor.transform.position, Position, 0.6f);
    
            // Check whether the previous focused object is this same. If so, reset the focused object.
            if (FocusedObject != oldFocusedObject)
            {
                ResetFocusedObject();
                if (FocusedObject != null)
                {
                    if (FocusedObject.CompareTag(InteractibleTag))
                    {
                        // Provide the 'Gaze Entered' event.
                        FocusedObject.SendMessage("OnGazeEntered", 
                            SendMessageOptions.DontRequireReceiver);
                    }
                }
            }
        }
    
  8. 在 Visual Studio 中儲存變更,再返回 Unity。

第 8 章 - 建立互動類別

您現在必須建立負責下列項目的 互動 文稿:

  • 處理 選互動和 相機注視,讓用戶能夠與場景中的登入「按鈕」互動。

  • 在場景中建立登入 「button」 物件,讓用戶能夠與其互動。

建立指令碼:

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

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

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

  4. 插入下列命名空間:

    using UnityEngine;
    using UnityEngine.XR.WSA.Input;
    
  5. Interaction 類別的繼承從 MonoBehaviour 變更為 GazeInput

    public 類別互動:MonoBehaviour

    public class Interactions : GazeInput
    
  6. Interaction 類別內插入下列變數:

        /// <summary>
        /// Allows input recognition with the HoloLens
        /// </summary>
        private GestureRecognizer _gestureRecognizer;
    
  7. 取代 Start 方法;請注意,它是覆寫方法,它會呼叫 'base' Gaze 類別方法。 啟動 () 會在類別初始化時呼叫、註冊輸入辨識,並在場景中建立登入 按鈕

        /// <summary>
        /// Called on initialization, after Awake
        /// </summary>
        internal override void Start()
        {
            base.Start();
    
            // Register the application to recognize HoloLens user inputs
            _gestureRecognizer = new GestureRecognizer();
            _gestureRecognizer.SetRecognizableGestures(GestureSettings.Tap);
            _gestureRecognizer.Tapped += GestureRecognizer_Tapped;
            _gestureRecognizer.StartCapturingGestures();
    
            // Add the Graph script to this object
            gameObject.AddComponent<MeetingsUI>();
            CreateSignInButton();
        }
    
  8. 新增 CreateSignInButton () 方法,此方法會在場景中具現化登入 按鈕 ,並設定其屬性:

        /// <summary>
        /// Create the sign in button object in the scene
        /// and sets its properties
        /// </summary>
        void CreateSignInButton()
        {
            GameObject signInButton = GameObject.CreatePrimitive(PrimitiveType.Sphere);
    
            Material mat = new Material(Shader.Find("Diffuse"));
            signInButton.GetComponent<Renderer>().material = mat;
            mat.color = Color.blue;
    
            signInButton.transform.position = new Vector3(3.5f, 2f, 9f);
            signInButton.tag = "SignInButton";
            signInButton.AddComponent<Graph>();
        }
    
  9. 新增 GestureRecognizer_Tapped () 方法,以回應 Tap 使用者事件。

        /// <summary>
        /// Detects the User Tap Input
        /// </summary>
        private void GestureRecognizer_Tapped(TappedEventArgs obj)
        {
            if(base.FocusedObject != null)
            {
                Debug.Log($"TAP on {base.FocusedObject.name}");
                base.FocusedObject.SendMessage("SignInAsync", SendMessageOptions.RequireReceiver);
            }
        }
    
  10. 刪除Update () 方法,然後儲存 Visual Studio 中的 變更 ,再返回 Unity。

第 9 章 - 設定文本參考

在本章中,您需要將 互動 腳本放在 主要相機上。 然後,該腳本會處理放置其他腳本的位置。

  • [項目面板] 的 [腳本] 資料夾中,將腳本 [互動] 拖曳至 [主要相機] 物件,如下圖所示。

    顯示要拖曳互動腳本位置的螢幕快照。

第 10 章 - 設定標記

處理注視的程式代碼會使用Tag SignInButton 來識別使用者將與其互動以登入 Microsoft Graph 的物件。

若要建立標籤:

  1. 在 Unity 編輯器中,按兩下階層面板中的主要相機

  2. [偵測器面板 ] 中,按兩下 MainCamera標籤 以開啟下拉式清單。 按兩下 [ 新增標籤...

    醒目提示 [新增標記... ] 的螢幕快照選項。

  3. 按一下 []+ 按鈕。

    顯示 [+] 按鈕的螢幕快照。

  4. 將標籤名稱寫入 為 SignInButton ,然後按兩下 [儲存]。

    顯示新增 SignInButton 標籤名稱位置的螢幕快照。

第 11 章 - 將 Unity 專案建置至 UWP

此專案的 Unity 區段一切都已完成,因此是時候從 Unity 建置它。

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

    顯示 [建置設定] 對話框的螢幕快照。

  2. 如果尚未這麼做,請勾選 Unity C# 專案

  3. 按一下 [建置]。 Unity 會啟動 檔案總管 視窗,您需要在其中建立,然後選取要建置應用程式的資料夾。 立即建立該資料夾,並將它命名為 [應用程式]。 然後選取 [ 應用程式 ] 資料夾後,按兩下 [ 選取資料夾]。

  4. Unity 會開始將專案建置至 [應用程式 ] 資料夾。

  5. 一旦 Unity 完成建置 (可能需要一些時間) ,它會在組建 (檢查工作欄的位置開啟 檔案總管 視窗,因為它可能不一定會出現在您的視窗上方,但會通知您新增視窗) 。

第 12 章 - 部署至 HoloLens

若要在 HoloLens 上部署:

  1. 您需要適用於遠端部署) 的 HoloLens (IP 位址,並確保 HoloLens 處於 開發人員模式。 若要這樣做:

    1. 在戴上 HoloLens 時,開啟 [ 設定]。

    2. 移至 網路 & 因特網>Wi-Fi>進階選項

    3. 請注意 IPv4 位址。

    4. 接下來,流覽回 [設定],然後流覽至 [更新開發人員的安全性 & 安全性>]

    5. 設定 [開啟開發人員模式]。

  2. 流覽至新的 Unity 組建, (應用程式 資料夾) ,並使用 Visual Studio 開啟方案檔。

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

  4. [解決方案平臺] 中,選取 [x86]、[遠端計算機]。 在此案例中,系統會提示您在 HoloLens (插入遠端裝置的 IP 位址 ,在此案例中,系統會提示您) 。

    顯示選取 x86 和遠端電腦位置的螢幕快照。

  5. 移至 [建置 ] 功能表,然後按兩下 [ 部署解決方案 ] 以側載應用程式至 HoloLens。

  6. 您的應用程式現在應該會出現在您的 HoloLens 上安裝的應用程式清單中,準備好啟動!

您的 Microsoft Graph HoloLens 應用程式

恭喜,您建置了利用 Microsoft Graph 的混合實境應用程式,以讀取及顯示使用者行事曆數據。

顯示已完成混合實境應用程式的螢幕快照。

額外練習

練習 1

使用 Microsoft Graph 顯示使用者的其他資訊

  • 用戶電子郵件/電話號碼/配置檔圖片

練習 1

實作語音控件以巡覽 Microsoft Graph UI。