HoloLens(第一代)和 Azure 312:机器人集成

注意

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

在本课程中,你将了解如何使用 Microsoft Bot Framework V4 创建和部署机器人,并通过 Windows Mixed Reality 应用程序与它进行通信。

Screenshot that shows communication through a Windows Mixed Reality application.

Microsoft Bot Framework V4 是一组 API,旨在向开发人员提供用于生成可扩展、可缩放机器人应用程序的工具。 有关详细信息,请访问 Microsoft Bot Framework 页V4 Git 存储库

完成本课程后,你将生成一个 Windows Mixed Reality 应用程序,该应用程序将能够执行以下操作:

  1. 使用“点击手势”启动机器人来侦听用户语音
  2. 如果用户有所表达,机器人将尝试提供响应。
  3. 在 Unity 场景中,在机器人附近将机器人的回复将显示为文本。

在应用程序中,由你决定结果与设计的集成方式。 本课程旨在介绍如何将 Azure 服务与 Unity 项目集成。 你的任务是运用从本课程中学到的知识来增强混合现实应用程序。

设备支持

课程 HoloLens 沉浸式头戴显示设备
MR 和 Azure 312:机器人集成

注意

尽管本课程重点介绍 HoloLens,但你也可以将本课程中学到的知识运用到 Windows Mixed Reality 沉浸式 (VR) 头戴显示设备。 由于沉浸式 (VR) 头戴显示设备没有可用的摄像头,因此你需要将外部摄像头连接到电脑。 随着课程的进行,你将看到有关支持沉浸式 (VR) 头戴显示设备可能需要进行的任何更改的说明。

先决条件

注意

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

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

开始之前

  1. 为了避免在生成此项目时遇到问题,强烈建议在根文件夹或接近根的文件夹中创建本教程中提到的项目(长文件夹路径会在生成时导致问题)。
  2. 设置并测试 HoloLens。 如需有关设置 HoloLens 的支持,请确保参阅“HoloLens 设置”一文
  3. 在开始开发新的 HoloLens 应用时,最好执行校准和传感器优化(有时 HoloLens 应用可以帮助为每个用户执行这些任务)。

有关校准的帮助信息,请单击此链接访问“HoloLens 校准”一文

有关传感器优化的帮助信息,请单击此链接访问“HoloLens 传感器优化”一文

第 1 章 - 创建机器人应用程序

第一步是将机器人创建为本地 ASP.Net Core Web 应用程序。 完成应用程序并进行测试后,将其发布到 Azure 门户。

  1. 打开 Visual Studio。 创建一个新项目,选择“ASP NET Core Web 应用程序”作为项目类型(你将在 .NET Core 小节找到它),并将其命名为“MyBot”。 单击“确定”。

  2. 在显示的窗口中选择“空”。 此外,请确保将目标设置为“ASP NET Core 2.0”,并将身份验证设置为“无身份验证”。 单击“确定”。

    Screenshot that shows the New A S P dot N E T Core Web Application window.

  3. 解决方案现在将打开。 在“解决方案资源管理器”中右键单击解决方案“Mybot”,然后单击“管理解决方案的 NuGet 包”

    Screenshot that shows the opened solution with 'MyBot' and 'Manage NuGet Packages for Solution' highlighted.

  4. 在“浏览”选项卡中,搜索“Microsoft.Bot.Builder.Integration.AspNet.Core”(确保已选中“包含预发行版”)。 选择包版本 4.0.1-预览,并勾选项目框。 然后单击“安装”。 你现在已安装了 Bot Framework v4 所需的库。 关闭 NuGet 页。

    Screenshot that shows the Nu-Get Solution manager.

  5. 在“解决方案资源管理器”中右键单击“项目”上的“MyBot”,然后单击“添加”|“类”

    Screenshot that shows the process to add a new class to MyBot.

  6. 将类命名为“MyBot”,然后单击“添加”

    Screenshot that shows the new class creation 'MyBot'.

  7. 重复上述步骤,以创建名为“ConversationContext”的另一个类

  8. 在“解决方案资源管理器”中右键单击“wwwroot”,然后单击“添加”|“新建项”。 选择“HTML 页”(可以在 Web 小节下找到它)。 将文件命名为“default.html”。 单击“添加” 。

    Screenshot that shows the creation of a new H T M L page from within the Solution Explorer window.

  9. “解决方案资源管理器”中的类/对象列表应类似于下图

    Screenshot of the Solution Explorer window with list of classes.

  10. 双击“ConversationContext”类。 此类负责保存机器人用于维护对话上下文的变量。 这些对话上下文值在此类的实例中维护,因为每次收到活动时,MyBot 类的任何实例都将刷新。 将以下代码添加到类:

    namespace MyBot
    {
        public static class ConversationContext
        {
            internal static string userName;
    
            internal static string userMsg;
        }
    }
    
  11. 双击“MyBot”类。 此类将托管来自客户的任何传入活动所调用的处理程序。 在此类中,你将添加用于生成机器人与客户对话的代码。 如前文所述,每次收到活动时都会初始化此类的实例。 将以下代码添加到此类:

    using Microsoft.Bot;
    using Microsoft.Bot.Builder;
    using Microsoft.Bot.Schema;
    using System.Threading.Tasks;
    
    namespace MyBot
    {
        public class MyBot : IBot
        {       
            public async Task OnTurn(ITurnContext context)
            {
                ConversationContext.userMsg = context.Activity.Text;
    
                if (context.Activity.Type is ActivityTypes.Message)
                {
                    if (string.IsNullOrEmpty(ConversationContext.userName))
                    {
                        ConversationContext.userName = ConversationContext.userMsg;
                        await context.SendActivity($"Hello {ConversationContext.userName}. Looks like today it is going to rain. \nLuckily I have umbrellas and waterproof jackets to sell!");
                    }
                    else
                    {
                        if (ConversationContext.userMsg.Contains("how much"))
                        {
                            if (ConversationContext.userMsg.Contains("umbrella")) await context.SendActivity($"Umbrellas are $13.");
                            else if (ConversationContext.userMsg.Contains("jacket")) await context.SendActivity($"Waterproof jackets are $30.");
                            else await context.SendActivity($"Umbrellas are $13. \nWaterproof jackets are $30.");
                        }
                        else if (ConversationContext.userMsg.Contains("color") || ConversationContext.userMsg.Contains("colour"))
                        {
                            await context.SendActivity($"Umbrellas are black. \nWaterproof jackets are yellow.");
                        }
                        else
                        {
                            await context.SendActivity($"Sorry {ConversationContext.userName}. I did not understand the question");
                        }
                    }
                }
                else
                {
    
                    ConversationContext.userMsg = string.Empty;
                    ConversationContext.userName = string.Empty;
                    await context.SendActivity($"Welcome! \nI am the Weather Shop Bot \nWhat is your name?");
                }
    
            }
        }
    }
    
  12. 双击“Startup”类。 此类将初始化机器人。 将以下代码添加到类:

    using Microsoft.AspNetCore.Builder;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.Bot.Builder.BotFramework;
    using Microsoft.Bot.Builder.Integration.AspNet.Core;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.DependencyInjection;
    
    namespace MyBot
    {
    public class Startup
        {
            public IConfiguration Configuration { get; }
    
            public Startup(IHostingEnvironment env)
            {
                var builder = new ConfigurationBuilder()
                    .SetBasePath(env.ContentRootPath)
                    .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                    .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
                    .AddEnvironmentVariables();
                Configuration = builder.Build();
            }
    
            // This method gets called by the runtime. Use this method to add services to the container.
            public void ConfigureServices(IServiceCollection services)
            {
                services.AddSingleton(_ => Configuration);
                services.AddBot<MyBot>(options =>
                {
                    options.CredentialProvider = new ConfigurationCredentialProvider(Configuration);
                });
            }
    
            // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
            public void Configure(IApplicationBuilder app, IHostingEnvironment env)
            {
                if (env.IsDevelopment())
                {
                    app.UseDeveloperExceptionPage();
                }
    
                app.UseDefaultFiles();
                app.UseStaticFiles();
                app.UseBotFramework();
            }
        }
    }
    
  13. 打开“Program”类文件,验证其中的代码是否与以下代码相同

    using Microsoft.AspNetCore;
    using Microsoft.AspNetCore.Hosting;
    
    namespace MyBot
    {
        public class Program
        {
            public static void Main(string[] args)
            {
                BuildWebHost(args).Run();
            }
    
            public static IWebHost BuildWebHost(string[] args) =>
                WebHost.CreateDefaultBuilder(args)
                    .UseStartup<Startup>()
                    .Build();
        }
    }
    
  14. 请记住保存所做的更改,若要执行此操作,请从 Visual Studio 顶部的工具栏中转到“文件”>“全部保存”

第 2 章 - 创建 Azure 机器人服务

既然已经为机器人生成了代码,必须将其发布到 Azure 门户上的“Web 应用机器人”服务的实例。 本章将展示如何在 Azure 上创建和配置机器人服务,然后将代码发布到该服务。

  1. 首先,登录到 Azure 门户 (https://portal.azure.com)。

    1. 如果你没有 Azure 帐户,需要创建一个。 如果你在课堂或实验室场景中跟着本教程学习,请让讲师或监督人员帮助设置你的新帐户。
  2. 登录后,单击左上角的“创建资源”,然后搜索“Web 应用机器人”,并单击“Enter”键

    Screenshot of the Microsoft Azure dashboard with 'Create a resource' highlighted in the upper left.

  3. 新页面将提供“Web 应用机器人”服务的说明。 在此页面的左下角,选择“创建”按钮,以创建与此服务的关联

    Screenshot of the Web App Bot page and the 'Create' button in the bottom left.

  4. 单击“创建”后

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

    2. 选择一个“订阅” 。

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

      若要详细了解 Azure 资源组,请点击此链接

    4. 确定资源组的位置(如果正在创建新的资源组)。 理想情况下,此位置在运行应用程序的区域中。 某些 Azure 资产仅在特定区域可用。

    5. 选择合适的“定价层”,如果这是你首次创建 Web 应用机器人服务,则会向你提供免费层(名为 F0)

    6. “应用名称”可以与机器人名称相同

    7. 将“机器人模板”保留为“基本 (C#)”

    8. “应用服务计划/位置”将由帐户自动填充

    9. 设置要用于托管机器人的“Azure 存储”。 如果还没有,可以在此处创建。

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

    11. 单击 “创建” 。

      Screenshot that shows the required fields needed to create the new Service.

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

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

    Screenshot that shows the notification icon highlighted after the Service instance is created.

  7. 单击通知可浏览新的服务实例。

    Screenshot that shows deployment succeeded and the 'Go to resources' button.

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

    Screenshot of the Resources windows after clicking the 'Go to resources' button in the previous window.

  9. 此时,需要设置名为“Direct Line”的功能,以允许客户端应用程序与此机器人服务通信。 单击“通道”,然后在“添加精选通道”部分中,单击“配置 Direct Line 通道”

    Screenshot that shows the Configure Direct Line channel highlighted in MyHoloLensBot.

  10. 在此页中,你将找到允许客户端应用向机器人进行身份验证的密钥。 单击“显示”按钮,并复制其中一个显示的密钥,因为稍后在项目中需要此密钥

    Screenshot of the secret keys highlighted in MyHoloLensBot Configure Direct Line channel.

第 3 章 - 将机器人发布到 Azure Web 应用机器人服务

服务准备就绪后,需要将之前生成的机器人代码发布到新创建的 Web 应用机器人服务。

注意

每次更改机器人解决方案/代码时,都必须将机器人发布到 Azure 服务。

  1. 返回到之前创建的 Visual Studio 解决方案。

  2. 在“解决方案资源管理器”中右键单击“MyBot”项目,然后单击“发布”

    Screenshot that shows the 'MyBot' project drop down menu after a right-click.

  3. 在“选取发布目标”页上,单击“应用服务”,然后单击“选择现有”,最后单击“创建配置文件”(如果此按钮不可见,可能需要单击“发布”按钮旁边的下拉箭头)

    Screenshot that shows the Pick a publish target page with 'App Service', 'Select Existing', and 'Create Profile' highlighted.

  4. 如果尚未登录到 Microsoft 帐户,必须在此处登录。

  5. 在“发布”页上,你会发现必须设置与创建 Web 应用机器人服务时使用的相同订阅。 然后将“视图”设置为“资源组”,在下拉文件夹结构中选择之前创建的“资源组”。 单击“确定”。

    Screenshot that shows the App Service window with the same Subscription used for the Web App Bot Service creation selected.

  6. 现在,单击“发布”按钮,等待机器人发布(可能需要几分钟)

    Screenshot that shows the Publish window with Publish button.

第 4 章 - 设置 Unity 项目

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

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

    Screenshot that shows the Unity Projects window with the 'New' project icon highlighted in the upper right.

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

    Screenshot that shows the new Unity Project name field highlighted.

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

    Screenshot that shows the Unity Preferences window with the required settings.

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

    Screenshot that shows the Build Settings window with the 'Switch Platform' button highlighted.

  5. 仍在“文件”>“生成设置”中,确保

    1. 将“目标设备”设置为“HoloLens”

      对于沉浸式头戴显示设备,将“目标设备”设置为“任何设备”

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

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

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

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

    6. 保存场景并将其添加到生成。

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

        Screenshot that shows the Build Settings window with the 'Add Open Scenes' button highlighted.

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

        Screenshot that shows the creation of a new 'Scenes' folder.

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

        Screenshot of the Scenes folder and the newly created file being saved.

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

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

    Screenshot that shows the Build Settings window in the Inspector tab.

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

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

      1. 脚本运行时版本应为试验版(等效于 NET 4.6),这将导致需要重启编辑器

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

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

        Screenshot that shows the Other Settings tab with the required settings.

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

      • InternetClient

      • Microphone

        Screenshot that shows 'InternetClient' and 'Microphone' enabled in the Publishing Settings tab.

    3. 在面板再靠下部分,在“发布设置”下的“XR 设置”中,勾选“支持虚拟现实”,确保已添加“Windows Mixed Reality SDK”

      Screenshot that shows Virtual Reality Supported enabled and Windows Mixed Reality S D K added.

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

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

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

第 5 章 - 摄像头设置

重要

如果要跳过本课程的“Unity 设置”部分,并继续直接编写代码,请根据需要下载此 Azure-MR-312-Package.unitypackage,并将其作为自定义包导入项目中,然后从第 7 章继续

  1. 在“层次结构”面板中选择“主摄像头”

  2. 选择后,你将能够在“检查器面板”中查看“主摄像头”的所有组件

    1. 摄像头对象必须命名为“主摄像头”(注意拼写)
    2. “主摄像头”标记必须设置为“MainCamera”(注意拼写)
    3. 请确保将“转换位置”设置为“0, 0, 0”
    4. 将“清除标志”设置为“纯色”
    5. 将摄像头组件的背景色设置为“黑色,Alpha 0 (十六进制代码: #00000000)”

    Screenshot that shows all the components of the Main Camera in the Inspector panel.

第 6 章 - 导入 Newtonsoft 库

为了帮助你反序列化和序列化接收并发送到机器人服务的对象,需要下载 Newtonsoft 库。 可在此处找到已使用正确的 Unity 文件夹结构组织的兼容版本

若要将 Newtonsoft 库导入项目,请使用本课程提供的 Unity 包。

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

    Screenshot that shows the Assets drop down menu with 'Import Package' then 'Custom Package' selected.

  2. 在弹出的“导入 Unity 包”框中,确保“插件”下面的所有内容(包括插件)都被选中

    Screenshot of the Import Unity Package popup box with'Plugins' selected.

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

  4. 转到项目视图中“插件”下的“Newtonsoft”文件夹,然后选择 Newtonsoft 插件

    Screenshot that shows the Newtonsoft folder in the project view.

  5. 选中 Newtonsoft 插件后,请确保未选中“任何平台”,然后确保“WSAPlayer”也未选中,然后单击“应用”。 这样做只是为了确认文件配置正确。

    Screenshot that shows the correct selections for the Newtonsoft plugin.

    注意

    标记这些插件,会将它们配置为仅在 Unity 编辑器中使用。 WSA 文件夹中还有一组不同的插件,从 Unity 导出项目后,将使用它们。

  6. 接下来,需要打开 Newtonsoft 文件夹中的 WSA 文件夹。 你将看到刚刚配置的同一文件的副本。 选择该文件,然后在检查器中确保

    • “任何平台”处于未选中状态
    • 仅选中“WSAPlayer”
    • “不处理”处于选中状态

    Screenshot that shows the correct selections for the Newtonsoft plugin in the WSA folder.

第 7 章 - 创建 BotTag

  1. 创建名为 BotTag 的新标记对象。 在场景中选择“主摄像头”。 单击“检查器”面板中的“标记”下拉菜单。 单击“添加标记”

    Screenshot of the Main Camera Tag drop down menu in the Inspector panel with 'Add Tag' highlighted.

  2. 单击 + 符号。 将新标记命名为 BotTag,然后单击“保存”

    Screenshot of the Inspector panel with the new BotTag name, plus symbol, and Save button.

警告

不要将 BotTag 应用到“主摄像头”。 如果意外执行了此操作,请确保将主摄像头标记更改回 MainCamera

第 8 章 - 创建 BotObjects 类

需要创建的第一个脚本是 BotObjects 类,这是一个空类,创建用于确保一系列其他类对象可以存储在同一脚本中,并可通过场景中的其他脚本访问

此类的创建纯粹是一种体系结构选择,这些对象可以托管在机器人脚本中,本课程稍后将创建该脚本。

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

  1. 右键单击“项目”面板,然后单击“创建”>“文件夹”。 将文件夹命名为“脚本”

    Create scripts folder.

  2. 双击“脚本”文件夹,将其打开。 然后在该文件夹中,右键单击并选择“创建”>“C# 脚本”。 将脚本命名为“BotObjects”

  3. 双击新的 BotObjects 脚本,使用 Visual Studio 将其打开

  4. 删除脚本内容,并将其替换为以下代码:

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class BotObjects : MonoBehaviour{}
    
    /// <summary>
    /// Object received when first opening a conversation
    /// </summary>
    [Serializable]
    public class ConversationObject
    {
        public string ConversationId;
        public string token;
        public string expires_in;
        public string streamUrl;
        public string referenceGrammarId;
    }
    
    /// <summary>
    /// Object including all Activities
    /// </summary>
    [Serializable]
    public class ActivitiesRootObject
    {
        public List<Activity> activities { get; set; }
        public string watermark { get; set; }
    }
    [Serializable]
    public class Conversation
    {
        public string id { get; set; }
    }
    [Serializable]
    public class From
    {
        public string id { get; set; }
        public string name { get; set; }
    }
    [Serializable]
    public class Activity
    {
        public string type { get; set; }
        public string channelId { get; set; }
        public Conversation conversation { get; set; }
        public string id { get; set; }
        public From from { get; set; }
        public string text { get; set; }
        public string textFormat { get; set; }
        public DateTime timestamp { get; set; }
        public string serviceUrl { get; set; }
    }
    
  5. 返回到 Unity 之前,请务必在 Visual Studio 中保存所做的更改

第 9 章 - 创建 GazeInput 类

要创建的下一个类是 GazeInput 类。 此类负责执行以下操作:

  • 创建一个表示玩家凝视的光标
  • 检测被玩家凝视的对象,并保存对检测到的对象的引用。

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

  1. 转到之前创建的“脚本”文件夹

  2. 右键单击文件夹并选择“创建”>“C# 脚本”。 将脚本命名为“GazeInput”

  3. 双击新的 GazeInput 脚本,通过 Visual Studio 将其打开

  4. 在类名的正上方插入以下行:

    /// <summary>
    /// Class responsible for the User's gaze interactions
    /// </summary>
    [System.Serializable]
    public class GazeInput : MonoBehaviour
    
  5. 然后在 GazeInput 类中的 Start() 方法上方添加以下变量

        [Tooltip("Used to compare whether an object is to be interacted with.")]
        internal string InteractibleTag = "BotTag";
    
        /// <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. 应添加 Start() 方法的代码。 此方法将在类初始化时调用:

        /// <summary>
        /// Start method used upon initialization.
        /// </summary>
        internal virtual void Start()
        {
            FocusedObject = null;
            Cursor = CreateCursor();
        }
    
  7. 实现将实例化并设置凝视光标的方法:

        /// <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 does not 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;
        }
    
  8. 实现从主摄像头设置 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 OnGazeExited event.
                    _oldFocusedObject.SendMessage("OnGazeExited", 
                        SendMessageOptions.DontRequireReceiver);
                }
            }
        }
    
    
        private void UpdateRaycast()
        {
            // Set the old focused gameobject.
            _oldFocusedObject = FocusedObject;
            RaycastHit hitInfo;
    
            // Initialize 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 OnGazeEntered event.
                        FocusedObject.SendMessage("OnGazeEntered",
                            SendMessageOptions.DontRequireReceiver);
                    }
                }
            }
        }
    
  9. 返回到 Unity 之前,请务必在 Visual Studio 中保存所做的更改

第 10 章 - 创建 Bot 类

现在要创建的脚本称为“Bot”。 这是应用程序的核心类,它将存储:

  • Web 应用机器人凭据
  • 收集用户语音命令的方法
  • 启动与 Web 应用机器人的对话所需的方法
  • 将消息发送到 Web 应用机器人所需的方法

若要将消息发送到机器人服务,SendMessageToBot() 协同例程将生成一个活动,该活动是一个对象,Bot Framework 会将其识别为用户发送的数据

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

  1. 双击“脚本”文件夹,将其打开

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

  3. 双击此新脚本,通过 Visual Studio 将其打开。

  4. 在 Bot 类的顶部,将命名空间更新为与以下内容相同

    using Newtonsoft.Json;
    using System.Collections;
    using System.Text;
    using UnityEngine;
    using UnityEngine.Networking;
    using UnityEngine.Windows.Speech;
    
  5. 在 Bot 类中,添加以下变量

        /// <summary>
        /// Static instance of this class
        /// </summary>
        public static Bot Instance;
    
        /// <summary>
        /// Material of the sphere representing the Bot in the scene
        /// </summary>
        internal Material botMaterial;
    
        /// <summary>
        /// Speech recognizer class reference, which will convert speech to text.
        /// </summary>
        private DictationRecognizer dictationRecognizer;
    
        /// <summary>
        /// Use this variable to identify the Bot Id
        /// Can be any value
        /// </summary>
        private string botId = "MRBotId";
    
        /// <summary>
        /// Use this variable to identify the Bot Name
        /// Can be any value
        /// </summary>
        private string botName = "MRBotName";
    
        /// <summary>
        /// The Bot Secret key found on the Web App Bot Service on the Azure Portal
        /// </summary>
        private string botSecret = "-- Add your Secret Key here --"; 
    
        /// <summary>
        /// Bot Endpoint, v4 Framework uses v3 endpoint at this point in time
        /// </summary>
        private string botEndpoint = "https://directline.botframework.com/v3/directline";
    
        /// <summary>
        /// The conversation object reference
        /// </summary>
        private ConversationObject conversation;
    
        /// <summary>
        /// Bot states to regulate the application flow
        /// </summary>
        internal enum BotState {ReadyToListen, Listening, Processing}
    
        /// <summary>
        /// Flag for the Bot state
        /// </summary>
        internal BotState botState;
    
        /// <summary>
        /// Flag for the conversation status
        /// </summary>
        internal bool conversationStarted = false;
    

    注意

    确保将机器人密钥插入 botSecret 变量。 在本课程开始时,你将在第 2 章步骤 10 中注意到机器人密钥

  6. 现在需要添加 Awake() 和 Start() 的代码

        /// <summary>
        /// Called on Initialization
        /// </summary>
        void Awake()
        {
            Instance = this;
        }
    
        /// <summary>
        /// Called immediately after Awake method
        /// </summary>
        void Start()
        {
            botState = BotState.ReadyToListen;
        }
    
  7. 添加语音捕获开始和结束时由语音库调用的两个处理程序。 用户停止说话后,DictationRecognizer 将自动停止捕获用户语音

        /// <summary>
        /// Start microphone capture.
        /// </summary>
        public void StartCapturingAudio()
        {
            botState = BotState.Listening;
            botMaterial.color = Color.red;
    
            // Start dictation
            dictationRecognizer = new DictationRecognizer();
            dictationRecognizer.DictationResult += DictationRecognizer_DictationResult;
            dictationRecognizer.Start();
        }
    
    
        /// <summary>
        /// Stop microphone capture.
        /// </summary>
        public void StopCapturingAudio()
        {
            botState = BotState.Processing;
            dictationRecognizer.Stop();
        }
    
    
  8. 下面的处理程序将收集用户语音输入的结果,并调用负责将消息发送到 Web 应用机器人服务的协同例程。

        /// <summary>
        /// This handler is called every time the Dictation detects a pause in the speech. 
        /// </summary>
        private void DictationRecognizer_DictationResult(string text, ConfidenceLevel confidence)
        {
            // Update UI with dictation captured
            Debug.Log($"User just said: {text}");      
    
            // Send dictation to Bot
            StartCoroutine(SendMessageToBot(text, botId, botName, "message"));
            StopCapturingAudio();
        }     
    
  9. 调用以下协同例程来开始与机器人的对话。 你会注意到,完成会话调用后,它将通过传递一系列参数(这些参数将活动设置为以空消息形式发送到机器人服务)来调用 SendMessageToCoroutine()。 这样做是为了提示机器人服务启动对话。

        /// <summary>
        /// Request a conversation with the Bot Service
        /// </summary>
        internal IEnumerator StartConversation()
        {
            string conversationEndpoint = string.Format("{0}/conversations", botEndpoint);
    
            WWWForm webForm = new WWWForm();
    
            using (UnityWebRequest unityWebRequest = UnityWebRequest.Post(conversationEndpoint, webForm))
            {
                unityWebRequest.SetRequestHeader("Authorization", "Bearer " + botSecret);
                unityWebRequest.downloadHandler = new DownloadHandlerBuffer();
    
                yield return unityWebRequest.SendWebRequest();
                string jsonResponse = unityWebRequest.downloadHandler.text;
    
                conversation = new ConversationObject();
                conversation = JsonConvert.DeserializeObject<ConversationObject>(jsonResponse);
                Debug.Log($"Start Conversation - Id: {conversation.ConversationId}");
                conversationStarted = true; 
            }
    
            // The following call is necessary to create and inject an activity of type //"conversationUpdate" to request a first "introduction" from the Bot Service.
            StartCoroutine(SendMessageToBot("", botId, botName, "conversationUpdate"));
        }    
    
  10. 调用以下协同例程,生成要发送到机器人服务的活动。

        /// <summary>
        /// Send the user message to the Bot Service in form of activity
        /// and call for a response
        /// </summary>
        private IEnumerator SendMessageToBot(string message, string fromId, string fromName, string activityType)
        {
            Debug.Log($"SendMessageCoroutine: {conversation.ConversationId}, message: {message} from Id: {fromId} from name: {fromName}");
    
            // Create a new activity here
            Activity activity = new Activity();
            activity.from = new From();
            activity.conversation = new Conversation();
            activity.from.id = fromId;
            activity.from.name = fromName;
            activity.text = message;
            activity.type = activityType;
            activity.channelId = "DirectLineChannelId";
            activity.conversation.id = conversation.ConversationId;     
    
            // Serialize the activity
            string json = JsonConvert.SerializeObject(activity);
    
            string sendActivityEndpoint = string.Format("{0}/conversations/{1}/activities", botEndpoint, conversation.ConversationId);
    
            // Send the activity to the Bot
            using (UnityWebRequest www = new UnityWebRequest(sendActivityEndpoint, "POST"))
            {
                www.uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(json));
    
                www.downloadHandler = new DownloadHandlerBuffer();
                www.SetRequestHeader("Authorization", "Bearer " + botSecret);
                www.SetRequestHeader("Content-Type", "application/json");
    
                yield return www.SendWebRequest();
    
                // extrapolate the response Id used to keep track of the conversation
                string jsonResponse = www.downloadHandler.text;
                string cleanedJsonResponse = jsonResponse.Replace("\r\n", string.Empty);
                string responseConvId = cleanedJsonResponse.Substring(10, 30);
    
                // Request a response from the Bot Service
                StartCoroutine(GetResponseFromBot(activity));
            }
        }
    
  11. 在将活动发送到机器人服务后,调用以下协同例程来请求响应。

        /// <summary>
        /// Request a response from the Bot by using a previously sent activity
        /// </summary>
        private IEnumerator GetResponseFromBot(Activity activity)
        {
            string getActivityEndpoint = string.Format("{0}/conversations/{1}/activities", botEndpoint, conversation.ConversationId);
    
            using (UnityWebRequest unityWebRequest1 = UnityWebRequest.Get(getActivityEndpoint))
            {
                unityWebRequest1.downloadHandler = new DownloadHandlerBuffer();
                unityWebRequest1.SetRequestHeader("Authorization", "Bearer " + botSecret);
    
                yield return unityWebRequest1.SendWebRequest();
    
                string jsonResponse = unityWebRequest1.downloadHandler.text;
    
                ActivitiesRootObject root = new ActivitiesRootObject();
                root = JsonConvert.DeserializeObject<ActivitiesRootObject>(jsonResponse);
    
                foreach (var act in root.activities)
                {
                    Debug.Log($"Bot Response: {act.text}");
                    SetBotResponseText(act.text);
                }
    
                botState = BotState.ReadyToListen;
                botMaterial.color = Color.blue;
            }
        } 
    
  12. 需要添加到此类的最后一个方法在场景中显示消息:

        /// <summary>
        /// Set the UI Response Text of the bot
        /// </summary>
        internal void SetBotResponseText(string responseString)
        {        
            SceneOrganiser.Instance.botResponseText.text =  responseString;
        }
    

    注意

    你可能会在 Unity 编辑器控制台中看到一个错误,显示缺少 SceneOrganiser 类。 请忽略此消息,因为稍后会在本教程中创建此类。

  13. 返回到 Unity 之前,请务必在 Visual Studio 中保存所做的更改

第 11 章 - 创建 Interactions 类

现在要创建的类称为“Interactions”。 此类用于检测来自用户的 HoloLens 敲击输入。

如果用户在注视场景中的机器人对象时进行敲击,并且机器人准备好侦听语音输入,则机器人对象会将颜色更改为红色,并开始侦听语音输入

此类继承自 GazeInput 类,因此能够从该类中引用 Start() 方法和变量,使用 base 来表示

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

  1. 双击“脚本”文件夹,将其打开

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

  3. 双击此新脚本,通过 Visual Studio 将其打开。

  4. 在 Interactions 类的顶部,将命名空间和类继承更新为与以下内容相同

    using UnityEngine.XR.WSA.Input;
    
    public class Interactions : GazeInput
    {
    
  5. 在 Interactions 类中添加以下变量

        /// <summary>
        /// Allows input recognition with the HoloLens
        /// </summary>
        private GestureRecognizer _gestureRecognizer;
    
  6. 然后,添加 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();
        }
    
  7. 添加当用户在 HoloLens 摄像头前方执行敲击手势时将会触发的处理程序

        /// <summary>
        /// Detects the User Tap Input
        /// </summary>
        private void GestureRecognizer_Tapped(TappedEventArgs obj)
        {
            // Ensure the bot is being gazed upon.
            if(base.FocusedObject != null)
            {
                // If the user is tapping on Bot and the Bot is ready to listen
                if (base.FocusedObject.name == "Bot" && Bot.Instance.botState == Bot.BotState.ReadyToListen)
                {
                    // If a conversation has not started yet, request one
                    if(Bot.Instance.conversationStarted)
                    {
                        Bot.Instance.SetBotResponseText("Listening...");
                        Bot.Instance.StartCapturingAudio();
                    }
                    else
                    {
                        Bot.Instance.SetBotResponseText("Requesting Conversation...");
                        StartCoroutine(Bot.Instance.StartConversation());
                    }                                  
                }
            }
        }
    
  8. 返回到 Unity 之前,请务必在 Visual Studio 中保存所做的更改

第 12 章 - 创建 SceneOrganiser 类

此实验室中所需的最后一个类称为 SceneOrganiser。 此类以编程方式设置场景,方法是将组件和脚本添加到主摄像头,并在场景中创建相应的对象。

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

  1. 双击“脚本”文件夹,将其打开

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

  3. 双击此新脚本,通过 Visual Studio 将其打开。

  4. 在 SceneOrganiser 类中,添加以下变量

        /// <summary>
        /// Static instance of this class
        /// </summary>
        public static SceneOrganiser Instance;
    
        /// <summary>
        /// The 3D text representing the Bot response
        /// </summary>
        internal TextMesh botResponseText;
    
  5. 然后,添加 Awake() 和 Start() 方法

        /// <summary>
        /// Called on Initialization
        /// </summary>
        private void Awake()
        {
            Instance = this;
        }
    
        /// <summary>
        /// Called immediately after Awake method
        /// </summary>
        void Start ()
        {
            // Add the GazeInput class to this object
            gameObject.AddComponent<GazeInput>();
    
            // Add the Interactions class to this object
            gameObject.AddComponent<Interactions>();
    
            // Create the Bot in the scene
            CreateBotInScene();
        }
    
  6. 添加以下方法,此方法负责在场景中创建机器人对象并设置参数和组件:

        /// <summary>
        /// Create the Sign In button object in the scene
        /// and sets its properties
        /// </summary>
        private void CreateBotInScene()
        {
            GameObject botObjInScene = GameObject.CreatePrimitive(PrimitiveType.Sphere);
            botObjInScene.name = "Bot";
    
            // Add the Bot class to the Bot GameObject
            botObjInScene.AddComponent<Bot>();
    
            // Create the Bot UI
            botResponseText = CreateBotResponseText();
    
            // Set properties of Bot GameObject
            Bot.Instance.botMaterial = new Material(Shader.Find("Diffuse"));
            botObjInScene.GetComponent<Renderer>().material = Bot.Instance.botMaterial;
            Bot.Instance.botMaterial.color = Color.blue;
            botObjInScene.transform.position = new Vector3(0f, 2f, 10f);
            botObjInScene.tag = "BotTag";
        }
    
  7. 添加以下方法,此方法负责在场景中创建 UI 对象,以表示来自机器人的响应:

        /// <summary>
        /// Spawns cursor for the Main Camera
        /// </summary>
        private TextMesh CreateBotResponseText()
        {
            // Create a sphere as new cursor
            GameObject textObject = new GameObject();
            textObject.transform.parent = Bot.Instance.transform;
            textObject.transform.localPosition = new Vector3(0,1,0);
    
            // Resize the new cursor
            textObject.transform.localScale = new Vector3(0.1f, 0.1f, 0.1f);
    
            // Creating the text of the Label
            TextMesh textMesh = textObject.AddComponent<TextMesh>();
            textMesh.anchor = TextAnchor.MiddleCenter;
            textMesh.alignment = TextAlignment.Center;
            textMesh.fontSize = 50;
            textMesh.text = "Hi there, tap on me and I will start listening.";
    
            return textMesh;
        }
    
  8. 返回到 Unity 之前,请务必在 Visual Studio 中保存所做的更改

  9. 在“Unity 编辑器”中,将 SceneOrganiser 脚本从“脚本”文件夹拖到主摄像头。 “场景管理器”组件现在应显示在“主摄像头”对象上,如下图所示。

    Screenshot that shows the Scene Organiser script being added to the Main Camera object in the Unity Editor.

第 13 章 - 生成之前

若要对应用程序执行全面测试,需要将应用程序旁加载到 HoloLens。 执行此操作之前,请确保:

  • 第 4 章中提到的所有设置均正确设置
  • 将脚本“SceneOrganiser”附加到主摄像头对象
  • 在 Bot 类中,确保已将机器人密钥插入 botSecret 变量

第 14 章 - 生成并旁加载到 HoloLens

此项目的 Unity 部分所需的一切现已完成,接下来要从 Unity 生成它。

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

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

    Building the app from Unity

  3. 如果尚未勾选“Unity C# 项目”,请勾选

  4. 单击“生成”。 Unity 将启动“文件资源管理器”窗口,你需要在其中创建并选择一个文件夹来生成应用。 现在创建该文件夹,并将其命名为“应用”。 选择“应用”文件夹,然后,单击“选择文件夹”

  5. Unity 将开始将项目生成到“应用”文件夹

  6. Unity 完成生成(可能需要一些时间)后,会在生成位置打开“文件资源管理器”窗口(检查任务栏,因为它可能不会始终显示在窗口上方,但会通知你增加了一个新窗口)

第 15 章 - 部署到 HoloLens

在 HoloLens 上部署:

  1. 将需要 HoloLens 的 IP 地址(用于远程部署),并确保 HoloLens 处于“开发人员模式”。 要执行此操作:

    1. 佩戴 HoloLens 时,打开“设置”
    2. 转到“网络和 Internet”>“Wi-Fi”>“高级选项”
    3. 记下 “IPv4” 地址
    4. 接下来,导航回“设置”,然后转到“更新和安全”>“面向开发人员”
    5. 将“开发人员模式”设置为“打开”。
  2. 导航到新的 Unity 生成(“应用”文件夹)并使用 Visual Studio 打开解决方案文件

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

  4. 在“解决方案平台”中,选择“x86,远程计算机”

    Deploy the solution from Visual Studio.

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

  6. 你的应用现在应显示在 HoloLens 上的已安装应用列表中,随时可以启动!

    注意

    若要部署到沉浸式头戴显示设备,请将“解决方案平台”设置为“本地计算机”,并将“配置”设置为“调试”,将“平台”设置为“x86”。 然后,使用“生成”菜单,选择“部署解决方案”,将其部署到本地计算机

第 16 章 - 在 HoloLens 上使用应用程序

  • 启动应用程序后,会看到机器人作为一个蓝色的球体呈现在你面前。

  • 凝视该球体时,使用“敲击手势”来发起对话

  • 等待对话开始(UI 将在对话发生时显示消息)。 收到来自机器人的介绍性消息后,再次在机器人上敲击,使其变为红色之后,它将开始倾听你的声音。

  • 当你停止说话后,应用程序会将你的信息发送给机器人,你将迅速收到回应,并显示在 UI 上。

  • 重复此过程,向机器人发送更多的信息(每次发送信息时,必须进行敲击)。

此对话演示了机器人如何保留信息(你的名字),同时还提供已知信息(例如库存的物品)。

询问机器人的一些问题:

what do you sell? 

how much are umbrellas?

how much are raincoats?

已完成的 Web 应用机器人 (v4) 应用程序

恭喜,你已生成一个利用 Azure Web 应用机器人和 Microsoft Bot Framework v4 的混合现实应用。

Final Product

额外练习

练习 1

此实验室中的对话结构非常基础。 使用 Microsoft LUIS,为机器人提供自然语言理解能力。

练习 2

此示例不包括终止对话和重启新的对话。 若要完善机器人功能,请尝试结束对话。