HoloLens(第一代)和 Azure 308:跨设备通知


注意

混合现实学院教程在制作时考虑到了 HoloLens(第一代)和混合现实沉浸式头戴显示设备。 因此,对于仍在寻求这些设备的开发指导的开发人员而言,我们觉得很有必要保留这些教程。 我们不会在这些教程中更新 HoloLens 2 所用的最新工具集或集成相关的内容。 我们将维护这些教程,使之持续适用于支持的设备。 将来会发布一系列演示如何针对 HoloLens 2 进行开发的新教程。 此通知将在教程发布时通过指向这些教程的链接进行更新。


最终产品 - 开始

本课程将介绍如何使用 Azure 通知中心、Azure 表和 Azure Functions 将通知中心功能添加到混合现实应用程序。

Azure 通知中心是一项 Microsoft 服务,它允许开发人员将个性化定向推送通知发送到云中支持的任何平台。 借助此服务,开发人员能够有效地与最终用户通信,甚至在各种应用程序之间通信(具体取决于方案)。 有关详细信息,请访问 Azure 通知中心

Azure Functions 是一项 Microsoft 服务,它允许开发人员在 Azure 中运行小段代码“函数”。 这提供了一种将工作委托给云(而不是本地应用程序)的方法,这种方法会有很多优势。 Azure Functions 支持多种开发语言,包括 C#、F#、Node.js、Java 和 PHP。 有关详细信息,请访问 Azure Functions

Azure 表是一项 Microsoft 云服务,它允许开发人员在云中存储结构化的非 SQL 数据,使其在任何位置都可轻松访问。 此服务采用无架构设计,允许根据需要演变表,因此非常灵活。 有关详细信息,请访问“Azure 表”

完成本课程后,你将拥有一个混合现实沉浸式头戴显示设备应用程序和一个桌面电脑应用程序,它们将能够执行以下操作:

  1. 桌面电脑应用将允许用户使用鼠标在 2D 空间(X 和 Y)中移动对象。

  2. 使用 JSON 并以字符串形式将电脑应用中的对象移动(包含对象 ID、类型和转换信息(X、Y 坐标))发送到云。

  3. 混合现实应用(具有与桌面应用相同的场景)将从通知中心服务(刚由桌面电脑应用更新)接收有关对象移动的通知。

  4. 接收到通知(包含对象 ID、类型和转换信息)后,混合现实应用将接收的信息应用到自己的场景中。

在应用程序中,由你决定结果与设计的集成方式。 本课程旨在教授如何将 Azure 服务与 Unity 项目集成。 你的任务是运用从本课程中学到的知识来增强混合现实应用程序。 本课程是独立教程,不直接涉及任何其他混合现实实验室。

设备支持

课程 HoloLens 沉浸式头戴显示设备
MR 和 Azure 308:跨设备通知 ✔️ ✔️

注意

尽管本课程主要重点介绍 Windows Mixed Reality 沉浸式 (VR) 头戴显示设备,但你也可以将本课程中学到的内容应用到 Microsoft HoloLens。 随着课程的进行,你将看到有关支持 HoloLens 可能需要进行的任何更改的说明。 使用 HoloLens 时,你可能会在语音捕获过程中注意到某些回声。

先决条件

注意

本教程专为具有 Unity 和 C# 基本经验的开发人员设计。 另请注意,本文档中的先决条件和书面说明在编写时(2018 年 5 月)已经过测试和验证。 可随意使用最新软件(如安装工具一文所列的软件),但不应假设本课程中的信息会与你在比下列版本更高的软件中找到的内容完全一致。

建议在本课程中使用以下硬件和软件:

开始之前

  • 为了避免在生成此项目时遇到问题,强烈建议在根文件夹或接近根的文件夹中创建本教程中提到的项目(长文件夹路径会在生成时导致问题)。
  • 你必须是 Microsoft 开发人员门户和应用程序注册门户的所有者,否则无权访问第 2 章中的应用。

第 1 章 - 在 Microsoft 开发人员门户中创建应用程序

若要使用 Azure 通知中心服务,需要在 Microsoft 开发人员门户中创建应用程序,因为应用程序需要注册才能发送和接收通知。

  1. 登录到 Microsoft 开发人员门户

    需要登录到 Microsoft 帐户。

  2. 在仪表板中,单击“创建新应用”

    创建应用

  3. 随即将出现一个弹出窗口,你需要在其中为新应用保留名称。 在文本框中插入适当的名称;如果所选名称可用,文本框右侧将显示一个对勾。 插入可用名称后,单击弹出窗口左下角的“保留产品名称”按钮。

    保留名称

  4. 应用现已创建完成,可即刻进入下一章。

第 2 章 - 检索新应用凭据

登录应用程序注册门户(新应用将列于其中),并检索用于在 Azure 门户中设置通知中心服务的凭据。

  1. 导航到应用程序注册门户

    应用程序注册门户

    警告

    需要使用 Microsoft 帐户进行登录。
    它必须是上一章中在 Windows Store 开发人员门户中使用的 Microsoft 帐户

  2. 你将在“我的应用程序”部分下找到你的应用。 找到它后,单击它,随即将进入一个新页面,其中显示应用名称注册。

    新注册的应用

  3. 向下滚动注册页,找到应用的“应用程序机密”部分和“包 SID”。 复制这两者,以在下一章中设置 Azure 通知中心服务时使用。

    应用程序机密

第 3 章 - 设置 Azure 门户:创建通知中心服务

检索到应用凭据后,需要转到 Azure 门户,在其中创建 Azure 通知中心服务。

  1. 登录到 Azure 门户

    注意

    如果你没有 Azure 帐户,需要创建一个。 如果你在课堂或实验室场景中跟着本教程学习,请让讲师或监督人员帮助设置你的新帐户。

  2. 登录后,单击左上角的“新建”,搜索“通知中心”,并单击“Enter”键

    搜索通知中心

    注意

    在更新的门户中,“新建”一词可能已替换为“创建资源”

  3. 新页面将提供“通知中心”服务的说明。 在该提示的左下角,选择“创建”按钮,以创建与此服务的关联

    创建通知中心实例

  4. 单击“创建”后

    1. 插入此服务实例的所需名称。

    2. 提供能与此应用关联的命名空间。

    3. 选择“位置” 。

    4. 选择一个资源组或创建一个新资源组。 通过资源组,可监视和预配 Azure 资产集合、控制其访问权限并管理其计费。 建议保留与单个项目(例如通用资源组下的这些实验室)相关联的所有 Azure 服务。

      若要详细了解 Azure 资源组,请单击此链接了解如何管理资源组

    5. 选择相应的订阅

    6. 还需要确认了解应用于此服务的条款和条件。

    7. 选择创建

      填写服务详细信息

  5. 单击“创建”后,必须等待服务创建完成,这可能需要一分钟时间

  6. 创建服务实例后,门户中将显示一条通知。

    通知

  7. 单击通知中的“转到资源”按钮,浏览新的服务实例。 你将访问新的通知中心服务实例。

    显示通知窗口中突出显示的“转到资源”按钮的屏幕截图。

  8. 在概述页的中间位置单击“Windows (WNS)”。右侧面板将更改为显示两个文本字段,需要提供之前设置的应用中的包 SID 和安全密钥。

    新创建的中心服务

  9. 将详细信息复制到正确的字段后,单击“保存”,成功更新通知中心后,会收到一条通知。

    将安全详细信息复制过来

第 4 章 - 设置 Azure 门户:创建表服务

创建通知中心服务实例后,导航回 Azure 门户,在其中创建存储资源来创建 Azure 表服务。

  1. 如果尚未登录,请登录到 Azure 门户

  2. 登录后,单击左上角的“新建”,搜索“存储帐户”,并单击“Enter”键

    注意

    在更新的门户中,“新建”一词可能已替换为“创建资源”

  3. 选择列表中的“存储帐户 - Blob、文件、表、队列”

    搜索存储帐户

  4. 新页面将提供“存储帐户”服务的说明。 在该提示的左下角,选择“创建”按钮,创建此服务的实例

    创建存储实例

  5. 单击“创建”后,随即显示一个面板

    1. 插入此服务实例的所需名称(必须全部小写)。

    2. 对于“部署模型”,请单击“资源管理器”

    3. 对于“帐户类型”,请使用下拉菜单并选择“存储(常规用途 v1)”

    4. 选择相应的位置

    5. 对于“复制”下拉菜单,请选择“读取访问异地冗余存储(RA-GRS)”

    6. 对于“性能”,请单击“标准”。

    7. 在“需要安全传输”部分中,选择“禁用”

    8. 在“订阅”下拉菜单中,选择相应的订阅。

    9. 选择一个资源组或创建一个新资源组。 通过资源组,可监视和预配 Azure 资产集合、控制其访问权限并管理其计费。 建议保留与单个项目(例如通用资源组下的这些实验室)相关联的所有 Azure 服务。

      若要详细了解 Azure 资源组,请单击此链接了解如何管理资源组

    10. 将“虚拟网络”保留为“禁用”状态(如果有此选项)。

    11. 单击 “创建”

      填充存储详细信息

  6. 单击“创建”后,必须等待服务创建完成,这可能需要一分钟时间

  7. 创建服务实例后,门户中将显示一条通知。 单击通知以浏览新的服务实例。

    新的存储通知

  8. 单击通知中的“转到资源”按钮,浏览新的服务实例。 你将转到新的“存储服务”实例概述页。

    显示“部署成功”窗口中突出显示的“转到资源”按钮的屏幕截图。

  9. 在概述页中,单击右侧的“表”

    显示选择表的位置的屏幕截图。

  10. 右侧面板将更改为显示“表服务”信息,其中需要添加新表。 为此,请单击左上角的+“表格”按钮。

    打开表

  11. 随即显示一个新页面,其中需要输入表名称。 这是稍后几章中用于引用应用程序数据的名称。 插入适当的名称,然后单击“确定”

    创建新表

  12. 创建新表后,可以在“表服务”页(底部)看到此表。

    创建的新表

第 5 章 - 在 Visual Studio 中完成 Azure 表

设置表服务存储帐户后,可以向其添加数据,之后将用于存储和检索信息。 可以通过 Visual Studio 来编辑表。

  1. 打开“Visual Studio”。

  2. 从菜单中,单击“查看”>“Cloud Explorer”。

    打开 Cloud Explorer

  3. “Cloud Explorer”将以停靠项方式打开(请耐心等待,加载可能需要一些时间)。

    注意

    如果用于创建存储帐户的订阅不可见,请确保:

    • 已登录的帐户与用于 Azure 门户的帐户相同。

    • 从“帐户管理”页选择了订阅(可能需要在帐户设置中应用筛选器):

      查找订阅

  4. 随即将显示 Azure 云服务。 查找存储帐户,并单击其左侧的箭头以展开帐户

    打开存储帐户

  5. 展开后,新创建的存储帐户应可用。 单击存储左侧的箭头,然后在展开后,查找“表”并单击表旁边的箭头,以显示在上一章中创建的表。 双击表

    打开场景对象表

  6. 表将在 Visual Studio 窗口的中央位置打开。 单击带有 +(加号)的表图标。

    添加新表

  7. 随即显示一个窗口,提示“添加实体”。 一共将创建三个实体,每个实体具有多个属性。 会注意到已提供“PartitionKey”和“RowKey”,因为表使用这些属性来查找数据

    分区键和行键

  8. 按如下所示更新“PartitionKey”和“RowKey”的值(请记住为添加的每个行属性执行此操作,但每次递增 RowKey):

    添加正确的值

  9. 单击“添加属性”以添加额外的数据行。 第一个空表要与下表匹配。

  10. 完成后,单击确定

    完成时单击“确定”

    警告

    确保将“X”、“Y”和“Z”条目的“类型”更改为“双精度”。

  11. 现在你会注意到,你的表具有一行数据。 再次单击 +(加号)图标以添加其他实体。

    首行

  12. 创建其他属性,然后设置新实体的值以与下面显示的值相匹配。

    添加立方体

  13. 重复执行上一步操作以添加其他实体。 将此实体的值设置为下面显示的值。

    添加圆柱体

  14. 现在,表应如下所示。

    表完成

  15. 你已完成本章。 请务必保存。

第 6 章 - 创建 Azure 函数应用

创建 Azure 函数应用,桌面应用程序将调用该应用来更新表服务,并通过通知中心发送通知。

首先,需要创建一个文件,该文件允许 Azure 函数加载所需的库。

  1. 打开“记事本”(按 Windows 键,然后键入“记事本”)。

    打开记事本

  2. 打开“记事本”后,将下面的 JSON 结构插入其中。 完成此操作后,将其以 project.json 形式保存在桌面上。 命名务必正确:确保它没有 .txt 文件扩展名。 此文件定义函数将使用的库,如果你使用过 NuGet,你会很熟悉它。

    {
    "frameworks": {
        "net46":{
        "dependencies": {
            "WindowsAzure.Storage": "7.0.0",
            "Microsoft.Azure.NotificationHubs" : "1.0.9",
            "Microsoft.Azure.WebJobs.Extensions.NotificationHubs" :"1.1.0"
        }
        }
    }
    }
    
  3. 登录到 Azure 门户

  4. 登录后,单击左上角的“新建”,搜索“函数应用”,并按“Enter”键

    搜索函数应用

    注意

    在更新的门户中,“新建”一词可能已替换为“创建资源”

  5. 新页面将提供“函数应用”服务的说明。 在该提示的左下角,选择“创建”按钮,以创建与此服务的关联

    函数应用实例

  6. 单击“创建”后,请填写以下内容

    1. 对于“应用名称”,请插入此服务实例的所需名称

    2. 选择一个“订阅” 。

    3. 选择适合的定价层,如果这是第一次创建函数应用服务,则应可使用免费层

    4. 选择一个资源组或创建一个新资源组。 通过资源组,可监视和预配 Azure 资产集合、控制其访问权限并管理其计费。 建议保留与单个项目(例如通用资源组下的这些实验室)相关联的所有 Azure 服务。

      若要详细了解 Azure 资源组,请单击此链接了解如何管理资源组

    5. 对于“OS”,请单击“Windows”,因为这是目标平台

    6. 选择一种托管计划(本教程使用“消耗计划”)。

    7. 选择位置(选择与在上一步中生成的存储相同的位置)

    8. 对于“存储”部分,必须选择在上一步中创建的存储服务。

    9. 此应用中无需 Application Insights,因此可根据需要将其保留为“关闭”状态

    10. 单击 “创建”

      新建实例

  7. 单击“创建”后,必须等待服务创建完成,这可能需要一分钟时间

  8. 创建服务实例后,门户中将显示一条通知。

    新通知

  9. 单击通知以浏览新的服务实例。

  10. 单击通知中的“转到资源”按钮,浏览新的服务实例

    显示“部署成功”的屏幕截图,其中突出显示了“转到资源”按钮。

  11. 单击“函数”旁的 +(加号)图标,以新建函数

    添加新函数

  12. 在中央面板中,将显示“函数”创建窗口。 忽略面板上半部分中的信息,单击底部附近(如下所示蓝色区域)的“自定义函数”。

    自定义函数

  13. 窗口中的新页面将显示各种函数类型。 向下滚动以查看紫色类型,然后单击“HTTP PUT”元素。

    HTTP PUT 链接

    重要

    但是,查找名为“HTTP PUT”的元素时,可能必须继续向下滚动页面(如果 Azure 门户经过更新,此图看起来可能并不完全相同)。

  14. 随即将显示“HTTP PUT”窗口,需要在其中配置该函数(请参阅下图)。

    1. 对于“语言”,请使用下拉菜单选择“C#”。

    2. 对于“名称”,请输入适当的名称。

    3. 在“身份验证级别”下拉菜单中,选择“函数”

    4. 对于“表名称”部分,需要使用之前用于创建表服务的名称(包括相同的字母大小写)。

    5. 在“存储帐户连接”部分中,使用下拉菜单并从其中选择你的存储帐户。 如果你的存储帐户不在其中,请单击此部分标题旁的“新建”超链接,随即显示另一个面板,你的存储帐户应列于其中。

      显示“存储帐户连接”部分的屏幕截图,其中选择了“新建”超链接。

  15. 单击“创建”,将收到一条通知,指示设置已成功更新。

    创建函数

  16. 单击“创建”后,将会重定向到函数编辑器。

    更新函数代码

  17. 将以下代码插入函数编辑器中(替换函数代码):

    #r "Microsoft.WindowsAzure.Storage"
    
    using System;
    using Microsoft.WindowsAzure.Storage;
    using Microsoft.WindowsAzure.Storage.Table;
    using Microsoft.Azure.NotificationHubs;
    using Newtonsoft.Json;
    
    public static async Task Run(UnityGameObject gameObj, CloudTable table, IAsyncCollector<Notification> notification, TraceWriter log)
    {
        //RowKey of the table object to be changed
        string rowKey = gameObj.RowKey;
    
        //Retrieve the table object by its RowKey
        TableOperation operation = TableOperation.Retrieve<UnityGameObject>("UnityPartitionKey", rowKey); 
    
        TableResult result = table.Execute(operation);
    
        //Create a UnityGameObject so to set its parameters
        UnityGameObject existingGameObj = (UnityGameObject)result.Result; 
    
        existingGameObj.RowKey = rowKey;
        existingGameObj.X = gameObj.X;
        existingGameObj.Y = gameObj.Y;
        existingGameObj.Z = gameObj.Z;
    
        //Replace the table appropriate table Entity with the value of the UnityGameObject
        operation = TableOperation.Replace(existingGameObj); 
    
        table.Execute(operation);
    
        log.Verbose($"Updated object position");
    
        //Serialize the UnityGameObject
        string wnsNotificationPayload = JsonConvert.SerializeObject(existingGameObj);
    
        log.Info($"{wnsNotificationPayload}");
    
        var headers = new Dictionary<string, string>();
    
        headers["X-WNS-Type"] = @"wns/raw";
    
        //Send the raw notification to subscribed devices
        await notification.AddAsync(new WindowsNotification(wnsNotificationPayload, headers)); 
    
        log.Verbose($"Sent notification");
    }
    
    // This UnityGameObject represent a Table Entity
    public class UnityGameObject : TableEntity
    {
        public string Type { get; set; }
        public double X { get; set; }
        public double Y { get; set; }
        public double Z { get; set; }
        public string RowKey { get; set; }
    }
    

    注意

    该函数使用包含的库接收在 Unity 场景中移动的对象(称为 UnityGameObject 的 C# 对象)的名称和位置。 然后,使用此对象更新已创建的表中的对象参数。 在此之后,该函数调用所创建的通知中心服务,服务将通知所有已订阅的应用程序。

  18. 代码准备就绪后,单击“保存”

  19. 接下来,单击页面右侧的 <(箭头)图标。

    打开上传面板

  20. 随即一个面板从右侧滑入。 在该面板中,单击“上传”,随即显示“文件浏览器”。

  21. 导航到之前在记事本中创建的 project.json 文件并单击该文件,然后单击“打开”按钮。 此文件定义函数将使用的库。

    上传 json

  22. 文件上传后,它将显示在右侧的面板中。 单击它会在“函数”编辑器中打开它。 它必须与下图(步骤 23 下方)完全相同。

  23. 然后,在左侧面板的“函数”下,单击“集成”链接。

    集成函数

  24. 在下一页的右上角,单击“高级编辑器”(如下所示)。

    打开高级编辑器

  25. 随即在面板中间打开一个 function.json 文件,需要将其替换为以下代码片段。 它用于定义生成的函数以及传递到该函数的参数。

    {
    "bindings": [
        {
        "authLevel": "function",
        "type": "httpTrigger",
        "methods": [
            "get",
            "post"
        ],
        "name": "gameObj",
        "direction": "in"
        },
        {
        "type": "table",
        "name": "table",
        "tableName": "SceneObjectsTable",
        "connection": "mrnothubstorage_STORAGE",
        "direction": "in"
        },
        {
        "type": "notificationHub",
        "direction": "out",
        "name": "notification",
        "hubName": "MR_NotHub_ServiceInstance",
        "connection": "MRNotHubNS_DefaultFullSharedAccessSignature_NH",
        "platform": "wns"
        }
    ]
    }
    
  26. 编辑器现应如下图所示:

    返回到标准编辑器

  27. 你可能会注意到,你刚插入的输入参数可能与表和存储详细信息不匹配,因此需要使用你的信息进行更新。 请勿在此处执行此操作,接下来我们会讲到这一点。 只需单击页面右上角的“标准编辑器”链接,返回即可。

  28. 返回到“标准编辑器”,单击“输入”下的“Azure 表存储(表)”

    表输入

  29. 确保以下各项与你的信息匹配,因为它们可能不同(以下步骤的下方提供了一张图):

    1. 表名称:在 Azure 存储表服务中创建的表的名称。

    2. 存储帐户连接:单击下拉菜单旁边显示的“新建”,随即会在窗口的右侧显示一个面板。

      显示存储帐户窗口的屏幕截图,其中窗口右侧的面板中突出显示了“新建”。

      1. 选择之前创建的用于托管函数应用的存储帐户

      2. 你会注意到,存储帐户连接值已创建。

      3. 完成后,请务必按“保存”

    3. “输入”页现应与以下内容匹配,其中显示了你的信息。

      输入完成

  30. 接下来,单击“输出”下的“Azure 通知中心(通知)”。 确保以下各项与你的信息匹配,因为它们可能不同(以下步骤的下方提供了一张图):

    1. 通知中心名称:这是之前创建的通知中心服务实例的名称。

    2. 通知中心命名空间连接:单击下拉菜单旁边显示的“新建”

      检查输出

    3. 随即将显示“连接”弹出窗口(见下图),需要在其中选择之前设置的通知中心的命名空间。

    4. 从中间的下拉菜单中选择通知中心名称。

    5. 将“策略”下拉菜单设置为“DefaultFullSharedAccessSignature”

    6. 单击“选择”按钮以返回。

      输出更新

  31. “输入”页现应与以下内容匹配,但其中改为显示你的信息。 请务必按“保存”

警告

请勿直接编辑通知中心名称(如果已正确执行前面的步骤,则应全部使用“高级编辑器”来完成)。

显示包含常规信息的“输出”页的屏幕截图。

  1. 此时,应测试函数,确保它正常工作。 要执行此操作:

    1. 再次导航到函数页:

      显示“函数”页的屏幕截图,其中突出显示了新创建的函数。

    2. 返回函数页,单击页面最右侧的“测试”选项卡,以打开“测试”边栏选项卡:

      函数页的屏幕截图,右侧突出显示了“测试”。

    3. 在边栏选项卡的“请求正文”文本框中,粘贴以下代码:

      {  
          "Type":null,
          "X":3,
          "Y":0,
          "Z":1,
          "PartitionKey":null,
          "RowKey":"Obj2",
          "Timestamp":"0001-01-01T00:00:00+00:00",
          "ETag":null
      }
      
    4. 测试代码准备就绪后,单击右下角的“运行”按钮,将运行测试。 测试的输出日志将显示在函数代码下方的控制台区域中。

      显示控制台区域中测试的输出日志的屏幕截图。

    警告

    如果上述测试失败,则需要仔细检查是否完全遵照上述步骤执行操作,尤其是集成面板中的设置

第 7 章 - 设置桌面 Unity 项目

重要

现在创建的桌面应用程序将无法在 Unity 编辑器中运行。 需要在应用程序生成后,使用 Visual Studio(或已部署的应用程序)在编辑器外部运行。

下面是用于使用 Unity 和混合现实进行开发的典型设置,因此,这对其他项目来说是一个不错的模板。

设置并测试混合现实沉浸式头戴显示设备。

注意

本课程不需要运动控制器。 如果在设置沉浸式头戴显示设备方面需要支持,请点击此链接了解如何设置 Windows Mixed Reality

  1. 打开 Unity,单击“新建”

    Unity 项目窗口的屏幕截图,右上角突出显示了“新建”项目图标。

  2. 需要提供 Unity 项目名称,并插入 UnityDesktopNotifHub。 确保将项目类型设置为“3D”。 将“位置”设置为适合你的位置(请记住,越接近根目录越好)。 然后,单击“创建项目”

    创建项目

  3. 当 Unity 处于打开状态时,有必要检查默认“脚本编辑器”是否设置为“Visual Studio”。 转到“编辑”>“首选项”,然后在新窗口中导航到“外部工具”。 将外部脚本编辑器更改为 Visual Studio 2017。 关闭“首选项”窗口。

    设置外部 VS 工具

  4. 接下来转到“文件”>“生成设置”,选择“通用 Windows 平台”,然后单击“切换平台”按钮以应用你的选择

    切换平台

  5. 仍处于“文件”>“生成设置”中时,请确保

    1. 将“目标设备”设置为“任何设备”

      此应用程序将在桌面上使用,因此必须是“任何设备”

    2. 将“生成类型”设置为“D3D”

    3. 将“SDK”设置为“最新安装的版本”

    4. 将“Visual Studio 版本”设置为“最新安装的版本”

    5. 将“生成并运行”设置为“本地计算机”

    6. 此时应该保存该场景,并将其添加到生成中。

      1. 通过选择“添加开放场景”来执行此操作。 将出现一个保存窗口。

        显示右上角突出显示的“添加打开场景”的屏幕截图。

      2. 为此创建新文件夹,并为将来的任何场景创建一个新文件夹,然后选择“新建文件夹”按钮以创建新文件夹,将其命名为“场景”

        显示新建的 Scenes 文件夹的屏幕截图,其中突出显示了左上角的“新建文件夹”。

      3. 打开新创建的“场景”文件夹,然后在“文件名:”文本字段中,键入 NH_Desktop_Scene,然后按“保存”

        新建 NH_Desktop_Scene

    7. 在“生成设置”中,其余设置目前应保留为默认值

  6. 在同一个窗口中,单击“播放器设置”按钮,这会在检查器所在的空间中打开相关面板

  7. 在此面板中,需要验证一些设置:

    1. 在“其他设置”选项卡中

      1. “脚本运行时版本”应为“实验性(.NET 4.6 等效版本)”

      2. “脚本后端”应为 “.NET”

      3. “API 兼容性级别”应为“.NET 4.6”

        4.6 .NET 版本

    2. 在“发布设置”选项卡的“功能”下,检查以下内容

      • InternetClient

        显示“功能”下选择了“InternetClient”的屏幕截图。

  8. 返回生成设置 Unity C# 项目不再灰显;勾选此框旁边的复选框。

  9. 关闭“生成设置”窗口 。

  10. 保存场景和项目(“文件”>“保存场景/文件”>“保存项目”)

    重要

    如果要跳过本项目(桌面应用)的“Unity 设置”组件,直接继续学习代码,请根据需要下载此 .unitypackage,并将其作为自定义包导入项目中,然后从第 9 章继续。 仍需添加脚本组件。

第 8 章 - 在 Unity 中导入 DLL

你将使用适用于 Unity 的 Azure 存储(它本身利用的是适用于 Azure 的 .Net SDK)。 有关详细信息,请点击此链接了解适用于 Unity 的 Azure 存储

Unity 中当前存在一个已知问题,即需要在导入后重新配置插件。 解决 bug 后不再需要执行这些步骤(本部分的 4-7)。

若要将 SDK 导入到自己的项目中,请确保已从 GitHub 下载最新的 .unitypackage。 然后执行以下操作:

  1. 通过使用“资产”>“导入包”>“自定义包”菜单选项,将“.unitypackage”添加到 Unity

  2. 在弹出的“导入 Unity 包”框中,可以选择“插件”>“存储”下的所有内容。 取消选中其他所有内容,因为本课程不需要这些内容。

    导入到包

  3. 单击“导入”按钮,将项添加到项目

  4. 转到“插件”下的“存储”文件夹中,在“项目”视图中,仅选择以下插件

    • Microsoft.Data.Edm
    • Microsoft.Data.OData
    • Microsoft.WindowsAzure.Storage
    • Newtonsoft.Json
    • System.Spatial

取消选中任何平台

  1. 选中这些特定插件后,取消选中“任何平台”,然后取消选中“WSAPlayer”,然后单击“应用”。

    应用平台 dll

    注意

    我们正在将这些特定插件标记为仅在 Unity 编辑器中使用。 这是因为,在从 Unity 导出项目后,将使用 WSA 文件夹中的相同插件的不同版本。

  2. 在“存储”插件文件夹中,仅选择

    • Microsoft.Data.Services.Client

      为 dll 设置不处理

  3. 选中“平台设置”下的“不处理”框,然后单击“应用”

    应用不处理

    注意

    我们正在将此插件标记为“不处理”,因为 Unity 程序集修补程序在处理此插件时遇到困难。 即使未处理插件,该插件仍可正常工作。

第 9 章 - 在桌面 Unity 项目中创建 TableToScene 类

现在,需要创建包含代码的脚本来运行此应用程序。

需要创建的第一个脚本是 TableToScene,它负责:

  • 读取 Azure 表中的实体。
  • 使用表数据确定要生成的对象以及对象的生成位置。

需要创建的第二个脚本是 CloudScene,它负责:

  • 注册左键单击事件,使用户能够在场景周围拖动对象。
  • 序列化 Unity 场景中的对象数据,并将其发送到 Azure 函数应用。

若要创建此类,请执行以下操作:

  1. 右键单击“资产”文件夹,该文件夹位于“项目”面板中的“创建”>“文件夹”下。 将文件夹命名为“脚本”

    创建脚本文件夹

    创建脚本文件夹 2

  2. 双击刚刚创建的文件夹,打开它。

  3. 右键单击“脚本”文件夹,然后单击“创建”>“C# 脚本”。 将脚本命名为“TableToScene”

    显示如何创建新的“TableToScene”脚本的屏幕截图。TableToScene 重命名

  4. 双击该脚本以在 Visual Studio 2017 中打开它。

  5. 添加以下命名空间:

    using Microsoft.WindowsAzure.Storage;
    using Microsoft.WindowsAzure.Storage.Auth;
    using Microsoft.WindowsAzure.Storage.Table;
    using UnityEngine;
    
  6. 在类中插入以下变量:

        /// <summary>    
        /// allows this class to behave like a singleton
        /// </summary>    
        public static TableToScene instance;
    
        /// <summary>    
        /// Insert here you Azure Storage name     
        /// </summary>    
        private string accountName = " -- Insert your Azure Storage name -- ";
    
        /// <summary>    
        /// Insert here you Azure Storage key    
        /// </summary>    
        private string accountKey = " -- Insert your Azure Storage key -- ";
    

    注意

    将 accountName 值替换为 Azure 存储服务名称,并将 accountKey 值替换为在 Azure 门户的 Azure 存储服务中找到的密钥值(请参阅下图)。

    提取帐户密钥

  7. 现在,添加 Start() 和 Awake() 方法来初始化类。

        /// <summary>
        /// Triggers before initialization
        /// </summary>
        void Awake()
        {
            // static instance of this class
            instance = this;
        }
    
        /// <summary>
        /// Use this for initialization
        /// </summary>
        void Start()
        {  
            // Call method to populate the scene with new objects as 
            // pecified in the Azure Table
            PopulateSceneFromTableAsync();
        }
    
  8. TableToScene 类中,添加将检索 Azure 表中的值并使用这些值在场景中生成相应基元的方法。

        /// <summary>    
        /// Populate the scene with new objects as specified in the Azure Table    
        /// </summary>    
        private async void PopulateSceneFromTableAsync()
        {
            // Obtain credentials for the Azure Storage
            StorageCredentials creds = new StorageCredentials(accountName, accountKey);
    
            // Storage account
            CloudStorageAccount account = new CloudStorageAccount(creds, useHttps: true);
    
            // Storage client
            CloudTableClient client = account.CreateCloudTableClient(); 
    
            // Table reference
            CloudTable table = client.GetTableReference("SceneObjectsTable");
    
            TableContinuationToken token = null;
    
            // Query the table for every existing Entity
            do
            {
                // Queries the whole table by breaking it into segments
                // (would happen only if the table had huge number of Entities)
                TableQuerySegment<AzureTableEntity> queryResult = await table.ExecuteQuerySegmentedAsync(new TableQuery<AzureTableEntity>(), token); 
    
                foreach (AzureTableEntity entity in queryResult.Results)
                {
                    GameObject newSceneGameObject = null;
                    Color newColor;
    
                    // check for the Entity Type and spawn in the scene the appropriate Primitive
                    switch (entity.Type)
                    {
                        case "Cube":
                            // Create a Cube in the scene
                            newSceneGameObject = GameObject.CreatePrimitive(PrimitiveType.Cube);
                            newColor = Color.blue;
                            break;
    
                        case "Sphere":
                            // Create a Sphere in the scene
                            newSceneGameObject = GameObject.CreatePrimitive(PrimitiveType.Sphere);
                            newColor = Color.red;
                            break;
    
                        case "Cylinder":
                            // Create a Cylinder in the scene
                            newSceneGameObject = GameObject.CreatePrimitive(PrimitiveType.Cylinder);
                            newColor = Color.yellow;
                            break;
                        default:
                            newColor = Color.white;
                            break;
                    }
    
                    newSceneGameObject.name = entity.RowKey;
    
                    newSceneGameObject.GetComponent<MeshRenderer>().material = new Material(Shader.Find("Diffuse"))
                    {
                        color = newColor
                    };
    
                    //check for the Entity X,Y,Z and move the Primitive at those coordinates
                    newSceneGameObject.transform.position = new Vector3((float)entity.X, (float)entity.Y, (float)entity.Z);
                }
    
                // if the token is null, it means there are no more segments left to query
                token = queryResult.ContinuationToken;
            }
    
            while (token != null);
        }
    
  9. TableToScene 类之外,需要定义应用程序用于序列化和反序列化表实体的类。

        /// <summary>
        /// This objects is used to serialize and deserialize the Azure Table Entity
        /// </summary>
        [System.Serializable]
        public class AzureTableEntity : TableEntity
        {
            public AzureTableEntity(string partitionKey, string rowKey)
                : base(partitionKey, rowKey) { }
    
            public AzureTableEntity() { }
            public string Type { get; set; }
            public double X { get; set; }
            public double Y { get; set; }
            public double Z { get; set; }
        }
    
  10. 请务必在返回 Unity 编辑器之前保存

  11. 在“层次结构”面板中单击“主摄像头”,以便在“检查器”中显示其属性

  12. 在打开的“脚本”文件夹中,选择脚本 TableToScene 文件,并将其拖动到“主摄像头”上。 结果应如下所示:

    将脚本添加到主摄像头

第 10 章 - 在桌面 Unity 项目中创建 CloudScene 类

需要创建的第二个脚本是 CloudScene,它负责:

  • 注册左键单击事件,使用户能够在场景周围拖动对象。

  • 序列化 Unity 场景中的对象数据,并将其发送到 Azure 函数应用。

若要创建第二个脚本:

  1. 在“脚本”文件夹内右键单击,然后依次单击“创建”、“C# 脚本”。 将脚本命名为“CloudScene”

    显示如何创建新的“CloudScene”脚本的屏幕截图。重命名 CloudScene

  2. 添加以下命名空间:

    using Newtonsoft.Json;
    using System.Collections;
    using System.Text;
    using System.Threading.Tasks;
    using UnityEngine;
    using UnityEngine.Networking;
    
  3. 插入以下变量:

        /// <summary>
        /// Allows this class to behave like a singleton
        /// </summary>
        public static CloudScene instance;
    
        /// <summary>
        /// Insert here you Azure Function Url
        /// </summary>
        private string azureFunctionEndpoint = "--Insert here you Azure Function Endpoint--";
    
        /// <summary>
        /// Flag for object being moved
        /// </summary>
        private bool gameObjHasMoved;
    
        /// <summary>
        /// Transform of the object being dragged by the mouse
        /// </summary>
        private Transform gameObjHeld;
    
        /// <summary>
        /// Class hosted in the TableToScene script
        /// </summary>
        private AzureTableEntity azureTableEntity;
    
  4. 将 azureFunctionEndpoint 值替换为在 Azure 门户的 Azure 函数应用服务中找到的 Azure 函数应用 URL,如下图所示:

    获取函数 URL

  5. 现在,添加 Start() 和 Awake() 方法来初始化类。

        /// <summary>
        /// Triggers before initialization
        /// </summary>
        void Awake()
        {
            // static instance of this class
            instance = this;
        }
    
        /// <summary>
        /// Use this for initialization
        /// </summary>
        void Start()
        {
            // initialise an AzureTableEntity
            azureTableEntity = new AzureTableEntity();
        }
    
  6. 在 Update() 方法中,添加以下代码,该代码将检测用于在场景中移动 GameObject 的鼠标输入和拖动。 如果用户拖放了一个对象,它会将该对象的名称和坐标传递给 UpdateCloudScene() 方法,该方法将调用 Azure 函数应用服务,随后该服务会更新 Azure 表并触发通知。

        /// <summary>
        /// Update is called once per frame
        /// </summary>
        void Update()
        {
            //Enable Drag if button is held down
            if (Input.GetMouseButton(0))
            {
                // Get the mouse position
                Vector3 mousePosition = new Vector3(Input.mousePosition.x, Input.mousePosition.y, 10);
    
                Vector3 objPos = Camera.main.ScreenToWorldPoint(mousePosition);
    
                Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
    
                RaycastHit hit;
    
                // Raycast from the current mouse position to the object overlapped by the mouse
                if (Physics.Raycast(ray, out hit))
                {
                    // update the position of the object "hit" by the mouse
                    hit.transform.position = objPos;
    
                    gameObjHasMoved = true;
    
                    gameObjHeld = hit.transform;
                }
            }
    
            // check if the left button mouse is released while holding an object
            if (Input.GetMouseButtonUp(0) && gameObjHasMoved)
            {
                gameObjHasMoved = false;
    
                // Call the Azure Function that will update the appropriate Entity in the Azure Table
                // and send a Notification to all subscribed Apps
                Debug.Log("Calling Azure Function");
    
                StartCoroutine(UpdateCloudScene(gameObjHeld.name, gameObjHeld.position.x, gameObjHeld.position.y, gameObjHeld.position.z));
            }
        }
    
  7. 现在,按如下所示添加 UpdateCloudScene() 方法:

        private IEnumerator UpdateCloudScene(string objName, double xPos, double yPos, double zPos)
        {
            WWWForm form = new WWWForm();
    
            // set the properties of the AzureTableEntity
            azureTableEntity.RowKey = objName;
    
            azureTableEntity.X = xPos;
    
            azureTableEntity.Y = yPos;
    
            azureTableEntity.Z = zPos;
    
            // Serialize the AzureTableEntity object to be sent to Azure
            string jsonObject = JsonConvert.SerializeObject(azureTableEntity);
    
            using (UnityWebRequest www = UnityWebRequest.Post(azureFunctionEndpoint, jsonObject))
            {
                byte[] jsonToSend = new System.Text.UTF8Encoding().GetBytes(jsonObject);
    
                www.uploadHandler = new UploadHandlerRaw(jsonToSend);
    
                www.uploadHandler.contentType = "application/json";
    
                www.downloadHandler = new DownloadHandlerBuffer();
    
                www.SetRequestHeader("Content-Type", "application/json");
    
                yield return www.SendWebRequest();
    
                string response = www.responseCode.ToString();
            }
        }
    
  8. 保存代码并返回到 Unity

  9. 将 CloudScene 脚本拖动到“主摄像头”上。

    1. 在“层次结构”面板中单击“主摄像头”,以便在“检查器”中显示其属性

    2. 在打开的“脚本”文件夹中,选择 CloudScene 脚本,并将其拖动到“主摄像头”上。 结果应如下所示:

      将云脚本拖动到主摄像头上

第 11 章 - 将桌面项目生成到 UWP

此项目的 Unity 部分所需的一切现已完成。

  1. 导航到“生成设置”(“文件”>“生成设置”)

  2. 在“生成设置”窗口中,单击“生成”

    显示“生成设置”窗口的屏幕截图,其中已选中通用 Windows 平台,右下角突出显示了“生成”按钮。

  3. 随即将弹出一个“文件资源管理器”窗口,提示选择生成的位置。 创建一个新的文件夹(单击左上角的“新建文件夹”)并将其命名为“生成”

    为生成新建文件夹

    1. 打开新的“生成”文件夹,然后创建另一个文件夹(再次使用“新建文件夹”)并将其命名为“NH_Desktop_App”

      文件夹名称 NH_Desktop_App

    2. 选中“NH_Desktop_App”。 单击“选择文件夹”。 生成项目大约需要一分钟时间。

  4. 生成后,随即出现“文件资源管理器”并显示新项目的位置。 但是无需打开它,因为在接下来的几章中,需要先创建另一个 Unity 项目。

第 12 章 - 设置混合现实 Unity 项目

下面是用于使用混合现实进行开发的典型设置,因此对其他项目来说,这是一个不错的模板。

  1. 打开 Unity,单击“新建”

    显示“Unity 项目”窗口的屏幕截图,其中右上角突出显示了“新建”。

  2. 现在,需要提供 Unity 项目名称,并插入 UnityMRNotifHub。 确保将项目类型设置为“3D”。 将“位置”设置为适合你的位置(请记住,越接近根目录越好)。 然后,单击“创建项目”

    命名 UnityMRNotifHub

  3. 当 Unity 处于打开状态时,有必要检查默认“脚本编辑器”是否设置为“Visual Studio”。 转到“编辑”>“首选项”,然后在新窗口中导航到“外部工具”。 将外部脚本编辑器更改为 Visual Studio 2017。 关闭“首选项”窗口。

    将外部编辑器设置为 VS

  4. 接下来,转到“文件”>“生成设置”,并通过单击“切换平台”按钮将平台切换到“通用 Windows 平台”

    将平台切换为 UWP

  5. 转到“文件”>“生成设置”,并确保

    1. 将“目标设备”设置为“任何设备”

      对于 Microsoft HoloLens,请将“目标设备”设置为“HoloLens”

    2. 将“生成类型”设置为“D3D”

    3. 将“SDK”设置为“最新安装的版本”

    4. 将“Visual Studio 版本”设置为“最新安装的版本”

    5. 将“生成并运行”设置为“本地计算机”

    6. 此时应该保存该场景,并将其添加到生成中。

      1. 通过选择“添加开放场景”来执行此操作。 将出现一个保存窗口。

        显示“生成设置”窗口的屏幕截图,其中右上角突出显示了“添加打开场景”按钮。

      2. 为此创建新文件夹,并为将来的任何场景创建一个新文件夹,然后选择“新建文件夹”按钮以创建新文件夹,将其命名为“场景”

        显示“保存场景”窗口左上角突出显示的“新建文件夹”的屏幕截图。

      3. 打开新创建的“场景”文件夹,然后在“文件名:”文本字段中,键入 NH_MR_Scene,然后按“保存”

        新建场景 - NH_MR_Scene

    7. 在“生成设置”中,其余设置目前应保留为默认值

  6. 在同一个窗口中,单击“播放器设置”按钮,这会在检查器所在的空间中打开相关面板

    打开播放器设置

  7. 在此面板中,需要验证一些设置:

    1. 在“其他设置”选项卡中

      1. “脚本运行时版本”应为“实验性(.NET 4.6 等效版本)”

      2. “脚本后端”应为 “.NET”

      3. “API 兼容性级别”应为“.NET 4.6”

        API 兼容性

    2. 再往下滑面板,在“XR 设置”(在“发布设置”下方)中,勾选“支持的虚拟现实”,确保已添加“Windows Mixed Reality SDK”

      更新 XR 设置

    3. 在“发布设置”选项卡的“功能”下,检查以下内容

      • InternetClient

        显示已选中 InternetClient 的“发布设置”选项卡的屏幕截图。

  8. 返回生成设置,此时 Unity C# 项目不再灰显:勾选此内容旁边的复选框。

  9. 完成这些更改后,关闭“生成设置”窗口。

  10. 保存场景和项目(“文件”>“保存场景/文件”>“保存项目”)

    重要

    如果要跳过本项目(混合现实应用)的“Unity 设置”组件,直接继续学习代码,请根据需要下载此 .unitypackage,并将其作为自定义包导入项目中,然后从第 14 章继续。 仍需添加脚本组件。

第 13 章 - 在混合现实 Unity 项目中导入 DLL

你将使用适用于 Unity 库(它使用适用于 Azure 的 .Net SDK)的 Azure 存储。 请单击此链接,了解如何将 Azure 存储与 Unity 一起使用。 Unity 中当前存在一个已知问题,即需要在导入后重新配置插件。 解决 bug 后不再需要执行这些步骤(本部分的 4-7)。

若要将 SDK 导入到自己的项目中,请确保已下载最新的 .unitypackage。 然后执行以下操作:

  1. 通过使用“资产”>“导入包”>“自定义包”菜单选项,将以上下载的 .unitypackage 添加到 Unity

  2. 在弹出的“导入 Unity 包”框中,可以选择“插件”>“存储”下的所有内容

    导入包

  3. 单击“导入”按钮,将项添加到项目

  4. 转到“插件”下的“存储”文件夹中,在“项目”视图中,仅选择以下插件

    • Microsoft.Data.Edm
    • Microsoft.Data.OData
    • Microsoft.WindowsAzure.Storage
    • Newtonsoft.Json
    • System.Spatial

    选择插件

  5. 选中这些特定插件后,取消选中“任何平台”,然后取消选中“WSAPlayer”,然后单击“应用”。

    应用平台更改

    注意

    你正在将这些特定插件标记为仅在 Unity 编辑器中使用。 这是因为,在从 Unity 导出项目后,将使用 WSA 文件夹中的相同插件的不同版本。

  6. 在“存储”插件文件夹中,仅选择

    • Microsoft.Data.Services.Client

      选择数据服务客户端

  7. 选中“平台设置”下的“不处理”框,然后单击“应用”

    不处理

    注意

    你正在将此插件标记为“不处理”,因为 Unity 程序集修补程序在处理此插件时遇到困难。 即使未处理插件,该插件仍可正常工作。

第 14 章 - 在混合现实 Unity 项目中创建 TableToScene 类

TableToScene 类与第 9 章中所介绍的类完全相同。 遵循第 9 章所介绍的相同过程,在混合现实 Unity 项目中创建相同的类。

完成本章后,两个 Unity 项目的“主摄像头”上均将设置好此类。

第 15 章 - 在混合现实 Unity 项目中创建 NotificationReceiver 类

需要创建的第二个脚本是 NotificationReceiver,它负责:

  • 在初始化时向通知中心注册应用。
  • 侦听来自通知中心的通知。
  • 反序列化收到的通知中的对象数据。
  • 基于反序列化的数据移动场景中的 Gameobject。

若要创建 NotificationReceiver 脚本:

  1. 在“脚本”文件夹内右键单击,然后依次单击“创建”、“C# 脚本”。 将脚本命名为“NotificationReceiver”

    创建新的 c# 脚本将其命名为 NotificationReceiver

  2. 双击脚本将其打开。

  3. 添加以下命名空间:

    //using Microsoft.WindowsAzure.Messaging;
    using Newtonsoft.Json;
    using System;
    using System.Collections;
    using UnityEngine;
    
    #if UNITY_WSA_10_0 && !UNITY_EDITOR
    using Windows.Networking.PushNotifications;
    #endif
    
  4. 插入以下变量:

        /// <summary>
        /// allows this class to behave like a singleton
        /// </summary>
        public static NotificationReceiver instance;
    
        /// <summary>
        /// Value set by the notification, new object position
        /// </summary>
        Vector3 newObjPosition;
    
        /// <summary>
        /// Value set by the notification, object name
        /// </summary>
        string gameObjectName;
    
        /// <summary>
        /// Value set by the notification, new object position
        /// </summary>
        bool notifReceived;
    
        /// <summary>
        /// Insert here your Notification Hub Service name 
        /// </summary>
        private string hubName = " -- Insert the name of your service -- ";
    
        /// <summary>
        /// Insert here your Notification Hub Service "Listen endpoint"
        /// </summary>
        private string hubListenEndpoint = "-Insert your Notification Hub Service Listen endpoint-";
    
  5. 将 hubName 值替换为通知中心服务名称,并将 hubListenEndpoint 值替换为 Azure 门户的 Azure 通知中心服务的“访问策略”选项卡中找到的终结点值(请参阅下图)。

    插入通知中心策略终结点

  6. 现在,添加 Start() 和 Awake() 方法来初始化类。

        /// <summary>
        /// Triggers before initialization
        /// </summary>
        void Awake()
        {
            // static instance of this class
            instance = this;
        }
    
        /// <summary>
        /// Use this for initialization
        /// </summary>
        void Start()
        {
            // Register the App at launch
            InitNotificationsAsync();
    
            // Begin listening for notifications
            StartCoroutine(WaitForNotification());
        }
    
  7. 添加 WaitForNotification 方法,使应用能够在不与主线程发生冲突的情况下从通知中心库接收通知:

        /// <summary>
        /// This notification listener is necessary to avoid clashes 
        /// between the notification hub and the main thread   
        /// </summary>
        private IEnumerator WaitForNotification()
        {
            while (true)
            {
                // Checks for notifications each second
                yield return new WaitForSeconds(1f);
    
                if (notifReceived)
                {
                    // If a notification is arrived, moved the appropriate object to the new position
                    GameObject.Find(gameObjectName).transform.position = newObjPosition;
    
                    // Reset the flag
                    notifReceived = false;
                }
            }
        }
    
  8. 方法 InitNotificationAsync() 将在初始化时向通知中心服务注册应用程序。 代码会注释掉,因为 Unity 将无法生成项目。 在 Visual Studio 中导入 Azure 消息 Nuget 包时,你将删除注释。

        /// <summary>
        /// Register this application to the Notification Hub Service
        /// </summary>
        private async void InitNotificationsAsync()
        {
            // PushNotificationChannel channel = await PushNotificationChannelManager.CreatePushNotificationChannelForApplicationAsync();
    
            // NotificationHub hub = new NotificationHub(hubName, hubListenEndpoint);
    
            // Registration result = await hub.RegisterNativeAsync(channel.Uri);
    
            // If registration was successful, subscribe to Push Notifications
            // if (result.RegistrationId != null)
            // {
            //     Debug.Log($"Registration Successful: {result.RegistrationId}");
            //     channel.PushNotificationReceived += Channel_PushNotificationReceived;
            // }
        }
    
  9. 每次收到通知时,都会触发处理程序 Channel_PushNotificationReceived()。 它将反序列化通知(即桌面应用程序上已移动的 Azure 表实体),然后将 MR 场景中的相应 GameObject 移动到相同的位置。

    重要

    代码会注释掉,因为代码会引用 Azure 消息库,而在 Visual Studio 中使用 Nuget 包管理器生成 Unity 项目后会添加该消息库。 因此,除非注释掉此代码,否则 Unity 项目将无法生成。请注意,如果生成项目,并想要返回到 Unity,则需要重新注释此代码。

        ///// <summary>
        ///// Handler called when a Push Notification is received
        ///// </summary>
        //private void Channel_PushNotificationReceived(PushNotificationChannel sender, PushNotificationReceivedEventArgs args)    
        //{
        //    Debug.Log("New Push Notification Received");
        //
        //    if (args.NotificationType == PushNotificationType.Raw)
        //    {
        //        //  Raw content of the Notification
        //        string jsonContent = args.RawNotification.Content;
        //
        //        // Deserialise the Raw content into an AzureTableEntity object
        //        AzureTableEntity ate = JsonConvert.DeserializeObject<AzureTableEntity>(jsonContent);
        //
        //        // The name of the Game Object to be moved
        //        gameObjectName = ate.RowKey;          
        //
        //        // The position where the Game Object has to be moved
        //        newObjPosition = new Vector3((float)ate.X, (float)ate.Y, (float)ate.Z);
        //
        //        // Flag thats a notification has been received
        //        notifReceived = true;
        //    }
        //}
    
  10. 请记得在返回到 Unity 编辑器之前保存更改。

  11. 在“层次结构”面板中单击“主摄像头”,以便在“检查器”中显示其属性

  12. 在打开的“脚本”文件夹中,选择 NotificationReceiver 脚本,并将其拖动到“主摄像头”上。 结果应如下所示:

    将 NotificationReceiver 脚本拖动到摄像头

    注意

    如果针对 Microsoft HoloLens 开发,则需要更新主摄像头的摄像头组件,以使:

    • “清除标志”:“纯色”
    • 背景色:黑色

第 16 章 - 将混合现实项目生成到 UWP

本章介绍的生成过程与前一个项目完全相同。 此项目的 Unity 部分所需的一切现已完成,接下来要从 Unity 生成它。

  1. 导航到“生成设置”(“文件”>“生成设置”)

  2. 在“生成设置”菜单中,确保已勾选“Unity C# 项目”(这样便可以在生成后编辑项目中的脚本)。

  3. 完成此操作后,单击“生成”

    显示“生成设置”窗口的屏幕截图,其中右下角突出显示了“生成”按钮。

  4. 随即将弹出一个“文件资源管理器”窗口,提示选择生成的位置。 创建一个新的文件夹(单击左上角的“新建文件夹”)并将其命名为“生成”

    创建生成文件夹

    1. 打开新的“生成”文件夹,然后创建另一个文件夹(再次使用“新建文件夹”)并将其命名为“NH_MR_App”

      创建 NH_MR_Apps 文件夹

    2. 选中“NH_MR_App”。 单击“选择文件夹”。 生成项目大约需要一分钟时间。

  5. 生成后,随即会在新项目的位置打开一个“文件资源管理器”窗口。

第 17 章 - 将 NuGet 包添加到 UnityMRNotifHub 解决方案

警告

请记住,添加以下 NuGet 包(并在下一章中取消注释代码)后,在 Unity 项目中重新打开代码时,代码会出现错误。 如果要返回并继续在 Unity 编辑器中编辑,则需要注释该错误代码,然后在稍后回到 Visual Studio 时重新取消注释代码。

完成混合现实生成后,导航到生成的混合现实项目,然后双击该文件夹中的解决方案 (.sln) 文件,以使用 Visual Studio 2017 打开解决方案。 现在需要添加 WindowsAzure.Messaging.managed NuGet包;这是用于从通知中心接收通知的库。

若要导入 NuGet 包:

  1. 在“解决方案资源管理器”中,右键单击解决方案

  2. 单击“管理 NuGet 包”

    打开 NuGet 管理器

  3. 选择“浏览”选项卡并搜索“WindowsAzure.Messaging.managed”

    查找 Windows Azure 消息包

  4. 选择结果(如下所示),并在右侧窗口中选中“项目”旁边的复选框。 此时会勾选“项目”旁边的复选框,以及“Assembly-CSharp”和“UnityMRNotifHub”项目旁边的复选框

    勾选所有项目

  5. 最初提供的版本可能不兼容此项目。 因此,请单击“版本”旁边的下拉菜单,并单击“版本 0.1.7.9”,然后单击“安装”

  6. 现在已经完成 NuGet 包安装。 请在 NotificationReceiver 类中找到输入的已注释代码,并删除注释。

第 18 章 - 编辑 UnityMRNotifHub 应用程序的 NotificationReceiver 类

添加 NuGet包后,需要在 NotificationReceiver 类中取消注释一些代码。

这包括:

  1. 顶部的命名空间:

    using Microsoft.WindowsAzure.Messaging;
    
  2. InitNotificationsAsync() 方法内的所有代码:

        /// <summary>
        /// Register this application to the Notification Hub Service
        /// </summary>
        private async void InitNotificationsAsync()
        {
            PushNotificationChannel channel = await PushNotificationChannelManager.CreatePushNotificationChannelForApplicationAsync();
    
            NotificationHub hub = new NotificationHub(hubName, hubListenEndpoint);
    
            Registration result = await hub.RegisterNativeAsync(channel.Uri);
    
            // If registration was successful, subscribe to Push Notifications
            if (result.RegistrationId != null)
            {
                Debug.Log($"Registration Successful: {result.RegistrationId}");
                channel.PushNotificationReceived += Channel_PushNotificationReceived;
            }
        }
    

警告

以上代码包含注释:请确保未意外取消注释某个注释(因为如果意外取消注释,则代码将不会编译!)。

  1. 最后是 Channel_PushNotificationReceived 事件:

        /// <summary>
        /// Handler called when a Push Notification is received
        /// </summary>
        private void Channel_PushNotificationReceived(PushNotificationChannel sender, PushNotificationReceivedEventArgs args)
        {
            Debug.Log("New Push Notification Received");
    
            if (args.NotificationType == PushNotificationType.Raw)
            {
                //  Raw content of the Notification
                string jsonContent = args.RawNotification.Content;
    
                // Deserialize the Raw content into an AzureTableEntity object
                AzureTableEntity ate = JsonConvert.DeserializeObject<AzureTableEntity>(jsonContent);
    
                // The name of the Game Object to be moved
                gameObjectName = ate.RowKey;
    
                // The position where the Game Object has to be moved
                newObjPosition = new Vector3((float)ate.X, (float)ate.Y, (float)ate.Z);
    
                // Flag thats a notification has been received
                notifReceived = true;
            }
        }
    

取消注释代码后,请务必保存,然后继续下一章。

第 19 章 - 将混合现实项目关联到“应用商店”应用

现在,需要将混合现实项目关联到在实验开始时创建的“应用商店”应用。

  1. 打开解决方案。

  2. 右键单击“解决方案资源管理器”面板中的 UWP 应用项目,转到“应用商店”、“将应用与应用商店关联...”

    打开应用商店关联

  3. 随即显示一个名为“将应用与 Windows Store 关联”的新窗口。 单击 “下一步”

    转到下一屏幕

  4. 它将加载与已登录帐户关联的所有应用程序。 如果未登录到帐户,可以在此页面上登录。

  5. 找到在本教程开始时创建的应用商店应用名称,然后选择它。 然后单击“下一步”

    查找并选择应用商店名称

  6. 单击“关联”。

    关联应用

  7. 现在已将你的应用与应用商店应用关联。 这是启用通知所必需的。

第 20 章 - 部署 UnityMRNotifHub 和 UnityDesktopNotifHub 应用程序

由两人来完成本章内容可能更轻松,因为结果将包括两个运行的应用,一个运行在计算机桌面上,另一个运行在沉浸式头戴显示设备中。

沉浸式头戴显示设备应用等待接收场景更改(本地 GameObject 的位置更改),而桌面应用将对其会共享给 MR 应用的本地场景进行更改(位置更改)。 最好首先部署 MR 应用,然后部署桌面应用,以便接收器可以开始侦听。

若要在本地计算机上部署 UnityMRNotifHub 应用:

  1. 在 Visual Studio 2017 中打开 UnityMRNotifHub 应用的解决方案文件

  2. 在“解决方案平台”中,选择“x86,本地计算机”

  3. 在“解决方案配置”中,选择“调试”

    显示工具栏中“调试”的解决方案配置设置的屏幕截图。

  4. 转到“生成”菜单,并单击“部署解决方案”,将应用程序旁加载到计算机

  5. 应用现在应显示在已安装的应用列表中,随时可以启动。

若要在本地计算机上部署 UnityDesktopNotifHub 应用:

  1. 在 Visual Studio 2017 中打开 UnityDesktopNotifHub 应用的解决方案文件

  2. 在“解决方案平台”中,选择“x86,本地计算机”

  3. 在“解决方案配置”中,选择“调试”

    显示“解决方案配置”设置为“调试”的屏幕截图。

  4. 转到“生成”菜单,并单击“部署解决方案”,将应用程序旁加载到计算机

  5. 应用现在应显示在已安装的应用列表中,随时可以启动。

  6. 启动混合现实应用程序,然后启动桌面应用程序。

两个应用程序运行后,移动桌面场景中的对象(使用鼠标左键)。 将在本地进行这些位置更改,将其序列化并发送到函数应用服务。 然后,函数应用服务将更新表以及通知中心。 收到更新后,通知中心会直接将更新后的数据发送到所有已注册的应用程序(本例中为沉浸式头戴显示设备应用),应用程序随后反序列化传入的数据,并将新的位置数据应用到本地对象(在场景中移动它们)。

你已完成 Azure 通知中心应用程序

祝贺你,你生成了一个混合现实应用,此应用利用 Azure 通知中心服务并支持应用之间的通信。

最终产品 - 结束

额外练习

练习 1

你能否想出如何更改 GameObject 的颜色并将该通知发送到查看场景的其他应用?

练习 2

你能否将 GameObject 的移动添加到 MR 应用,并在桌面应用中查看更新的场景?