共用方式為


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 月)。 您可以自由使用最新的軟體,如安裝工具文章中所列,不過不應該假設本課程中的資訊會完全符合您在較新的軟體中找到的內容,而不是下面所列的內容。

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

在您開始使用 Intune 之前

  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。 不過,您可以使用 Microsoft Graph SDK,但是,在建置 Unity 項目之後,它需要新增 NuGet 套件(這表示在建置後編輯專案)。 將必要的 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 對象必須命名為主相機(請注意拼字!)

    2. 主相機 標記 必須設定為 MainCamera (請注意拼字!

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

    4. 將清除旗標設定純色

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

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

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

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

第 5 章 - 建立 MeetingsUI 類別

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

若要建立此類別:

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

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

  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. 新增負責建立 會議 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. 刪除 Update() 方法,並在返回 Unity 之前,先將變更儲存在 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
    

    重要

    您會發現此腳本中的程式碼部分會包裝在 Precompile 指示詞周圍,這是為了避免建置 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 章 - 建立 Interactions 類別

您現在必須建立 互動 文稿,該腳本負責:

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

  • 在場景中建立登入 「button」 物件,讓使用者可以與其互動。

若要建立文稿:

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

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

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

  4. 插入下列命名空間:

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

    public class Interactions : MonoBehaviour

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

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

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

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

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

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

第 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 將會開始將您的專案建置至 App 資料夾。

  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] [遠端計算機]。 系統會提示您插入 遠端裝置的IP位址 (在此案例中為您注意到的 HoloLens)。

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

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

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

您的 Microsoft Graph HoloLens 應用程式

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

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

額外練習

練習 1

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

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

練習 1

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