HoloLens(第一代)和 Azure 309:Application insights

注意

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

混合现实 Academy 教程欢迎屏幕。

在本课程中,你将学习如何使用 Azure Application Insights API 将 Application Insights 功能添加到混合现实应用程序,以便收集有关用户行为的分析。

Application Insights 是一项 Microsoft 服务,它允许开发人员从其应用程序收集分析,并从易于使用的门户对其进行管理。 分析可以是从性能到自定义信息等任何你想要收集的内容。 有关详细信息,请访问“Application Insights”页

完成本课程后,你将获得一个可执行以下操作的混合现实沉浸式头戴显示设备应用程序:

  1. 让用户能够凝视周围场景并在其中移动。
  2. 通过使用凝视和邻近感应场景对象来触发向 Application Insights 服务发送分析。
  3. 应用还将调用服务,并获取有关过去 24 小时内用户最接近的对象的信息。 该对象会将其颜色更改为绿色。

本课程将介绍如何将 Application Insights 服务的结果纳入到基于 Unity 的示例应用程序中。 你可以自行决定将这些概念应用到你可能会生成的自定义应用程序。

设备支持

课程 HoloLens 沉浸式头戴显示设备
MR 和 Azure 309:Application Insights ✔️ ✔️

注意

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

先决条件

注意

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

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

开始之前

为了避免在生成此项目时遇到问题,强烈建议在根文件夹或接近根的文件夹中创建本教程中的项目(长文件夹路径可能会在生成时导致问题)。

警告

请注意,Application Insights 接收数据需要一些时间,因此请耐心等待。 如果要检查此服务是否已收到数据,请查看第 14 章,其中介绍如何导航门户。

第 1 章 - Azure 门户

若要使用 Application Insights,需要在 Azure 门户中创建和配置 Application Insights 服务

  1. 登录到 Azure 门户

    注意

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

  2. 登录后,单击左上角的“新建”,搜索“Application Insights”,并单击“Enter”

    注意

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

    显示 Azure 门户的屏幕截图,“所有内容”窗格中突出显示了“见解”。

  3. 右侧的新页将提供 Azure Application Insights 服务的说明。 在此页面的左下角,选择“创建”按钮,以创建与此服务的关联

    Application Insights 屏幕的屏幕截图,其中突出显示了“创建”。

  4. 单击“创建”后

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

    2. 对于“应用程序类型”,请选择“常规”

    3. 选择相应的订阅

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

      若要详细了解 Azure 资源组,请访问资源组一文。

    5. 选择“位置” 。

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

    7. 选择创建

      Application Insights 窗口的屏幕截图。突出显示名称和应用程序类型。

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

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

    显示菜单功能区一部分的屏幕截图,其中突出显示了通知图标。

  7. 选择通知以浏览新的服务实例。

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

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

    显示实例名称为 MyNewInsight 的 Application Insights 服务实例的屏幕截图。

    注意

    保持此网页打开且易于访问,你将经常回到这里查看收集的数据。

    重要

    若要实现 Application Insights,需要使用三 (3) 个特定值:“检测密钥”、“应用程序 ID” 和 “API 密钥”。 下面将介绍如何从服务中检索这些值。 请确保在空白记事本页上记下这些值,因为不久将会在代码中用到它们。

  9. 若要查找“检测密钥”,需要向下滚动服务函数列表,然后选择“属性”,随即出现的选项卡将显示“服务密钥”

    显示服务函数的屏幕截图,“配置”部分突出显示了“属性”,并在主窗格中突出显示了检测密钥。

  10. 在“属性”下方一点,会发现“API 访问权限”,需要单击它。 右侧面板将提供应用的应用程序 ID

    显示服务函数的屏幕截图,其中突出显示了“P I 访问”。主窗格中突出显示了“创建 P I 密钥”和“应用程序 ID”。

  11. 保持打开“应用程序 ID”面板,单击“创建 API 密钥”,随即将打开“创建 API 密钥”面板。

    显示“创建 P I 密钥”面板的屏幕截图。

  12. 在打开的“创建 API 密钥”面板中,键入说明,并勾选三个框

  13. 单击“生成密钥”。 将创建并显示 API 密钥。

    “创建 P I 密钥”面板的屏幕截图,其中显示了新的服务密钥信息。

    警告

    “服务密钥”只会在此显示,因此请确保立刻复制服务密钥。

第 2 章 - 设置 Unity 项目

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

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

    Unity 项目窗口的屏幕截图。未显示项目信息。

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

    Unity 新建项目窗口的屏幕截图,其中显示了项目信息。

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

    显示 Visual Studio 设置为外部脚本编辑器的屏幕截图。

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

    “生成设置”窗口的屏幕截图,其中显示了“平台选择”列表。已选择通用 Windows 平台。

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

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

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

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

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

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

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

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

        “生成设置”窗口的屏幕截图,已选择“添加打开场景”。

      2. 创建一个新文件夹用于此场景和未来的任何场景,然后单击“新建文件夹”按钮创建新文件夹,将其命名为“场景”

        “保存场景”窗口的屏幕截图,其中选择了“场景”文件夹。

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

        “保存场景”窗口的屏幕截图,其中输入了文件名。

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

  7. 在“生成设置”窗口中,选择“播放器设置”,将在“检查器”所在的空间中打开相关面板

    显示“播放器设置”的“检查器”选项卡的屏幕截图。

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

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

      1. 脚本运行时版本应为实验性版本(.NET 4.6 等效版本),这将触发重启编辑器的需要。

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

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

      “检查器”选项卡的屏幕截图,其中显示了“其他设置”配置部分中的详细信息。

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

      • InternetClient

        “功能”列表的屏幕截图,其中选中了 Internet 客户端。

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

      “X R 设置”部分的屏幕截图,其中选中了“支持的虚拟现实”。

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

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

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

第 3 章 - 导入 Unity 包

重要

如果要跳过本课程的“Unity 设置”组件并继续直接编写代码,请随意下载此 Azure-MR-309.unitypackage,并将其作为自定义包导入项目中。 这还将包含下一章中的 DLL。 导入后,请继续学习第 6 章

重要

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

若要将 Application Insights 导入自己的项目中,请确保已下载包含插件的“.unitypackage”。 然后执行以下操作:

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

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

    “导入 Unity 包”对话框的屏幕截图,其中显示了选中的所有项。

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

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

    • Microsoft.ApplicationInsights

    “项目”面板的屏幕截图,“Insights”文件夹处于打开状态。

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

    “检查器”面板的屏幕截图,其中显示了编辑器和独立检查。

    注意

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

  6. 接下来,需要打开 “Insights” 文件夹中的 “WSA” 文件夹。 随后会显示配置的文件的副本。 选择此文件,然后在检查器中确保未选中任何平台,然后确保检查 WSAPlayer。 单击“应用”。

    显示已选中 W S A Player 的检查器面板的屏幕截图。

  7. 现在需要执行“步骤 4-6”,但需要改为针对 Newtonsoft 插件。 有关结果的外观,请参阅以下屏幕截图。

    “项目”和“检查器”面板的四个视图的屏幕截图,其中显示了设置 Newtonsoft 文件夹和插件选择的结果。

第 4 章 - 设置摄像头和用户控件

在本章中,你将设置摄像头和控件,以允许用户在场景中查看和移动。

  1. 右键单击“层次结构”面板中的空白区域,然后单击“创建”>“空白”

    “层次结构”面板的屏幕截图,已选中“创建空”。

  2. 将新的空 GameObject 重命名为“父摄像头”

    “层次结构”面板的屏幕截图,其中选择了“相机父级”。检查器面板

  3. 右键单击“层次结构”面板中的空白区域,然后依次单击“3D 对象”、“球体”。

  4. 将“球体”重命名为“右手”

  5. 将“右手”的“转换缩放比例”设置为“0.1,0.1,0.1”

    “层次结构”和“检查器”面板的屏幕截图,其中突出显示了检查器面板上的“转换”部分。

  6. 通过单击“球体碰撞体”组件中的齿轮并单击“删除组件”,从“右手”中删除“球体碰撞体”组件。

    检查器面板的屏幕截图,齿轮图标和“删除组件”突出显示在 Sphere 碰撞器部分中。

  7. 在“层次结构”面板中,将“主摄像头”和“右手”对象拖动到“父摄像头”对象上。

    选中“主相机”的“层次结构”面板的屏幕截图,“检查器”面板显示选中了“主相机”。

  8. 将“主摄像头”和“右手”对象的“转换位置”设置为“0,0,0”

    选中“主相机”的“层次结构”面板的屏幕截图,“检查器”面板中突出显示了“转换设置”。

    选中“右手”的“层次结构”面板的屏幕截图,“检查器”面板中突出显示了“转换设置”。

第 5 章 - 在 Unity 场景中设置对象

现在,为场景创建一些基本形状,以便用户可以与之交互。

  1. 右键单击“层次结构”面板中的空白区域,然后单击“3D 对象”并选择“平面”。

  2. 将平面的“转换位置”设置为“0,-1,0”

  3. 将平面的“转换缩放比例”设置为“5,1,5”

    场景、层次结构和检查器面板的屏幕截图。突出显示了检查器面板中的“转换”部分。

  4. 创建要与平面对象一起使用的基本材料,以便其他形状更易于查看。 导航到“项目”面板,右键单击,然后依次单击“创建”、“文件夹”,以创建新文件夹。 将其命名为“材料”。

    项目面板的屏幕截图,其中突出显示了“创建”和“文件夹”。 “项目”面板的屏幕截图。“资产”窗格中突出显示了材料。

  5. 打开“材料”文件夹,然后右键单击,单击“创建”,再单击“材料”,以创建新材料。 将其命名为“蓝色”。

    项目面板的屏幕截图,其中突出显示了“创建和材料”。 “项目”面板的屏幕截图。“材料”窗格中突出显示了蓝色。

  6. 选中新的“蓝色”材料后,查看“检查器”,然后单击“反照率”旁边的矩形窗口。 选择一种蓝色(下图为“十六进制颜色:#3592FFFF”)。 选择后,单击关闭按钮。

    检查器面板的屏幕截图。突出显示颜色部分。

  7. 将新材料从“材料”文件夹拖动到场景中新创建的“平面”上(或将其拖放到“层次结构”中的“平面”对象上)。

    “场景”面板的屏幕截图,其中显示了“材料”文件夹中的新材料。

  8. 右键单击“层次结构”面板中的空白区域,然后依次单击“3D 对象”、“胶囊”。

    • 选择“胶囊”后,将其转换位置更改为:-10、1、0
  9. 右键单击“层次结构”面板中的空白区域,然后依次单击“3D 对象”、“立方体”。

    • 选择多维数据集后,将其转换位置更改为:0、0、10
  10. 右键单击“层次结构”面板中的空白区域,然后依次单击“3D 对象”、“球体”。

    • 选中 Sphere 后,将其转换位置更改为:10、0、0。

    场景、层次结构和检查器面板的屏幕截图。“层次结构”面板中选择了“胶囊”。

    注意

    这些“位置”值是建议值。 可随意将对象设置在任何所需的位置,但如果对象距离摄像头不远,应用程序用户会更轻松。

  11. 当应用程序运行时,它需要能够识别场景中的对象,为此,需要标记对象。 选择其中一个对象,然后在“检查器”面板中,单击“添加标记...”,此时“检查器”将与“标记和层”面板交换

    “检查器”面板的屏幕截图,其中突出显示了“添加标记”选项。 “检查器”面板的屏幕截图,其中突出显示了“标记和层”。

  12. 单击 +(加号)符号,然后将标记名称键入为“ObjectInScene”

    选中了“标记和层”的检查器面板的屏幕截图。突出显示了“新建标记名称”对话框。

    警告

    如果为标记使用其他名称,则需要确保在稍后的 DataFromAnalytics、ObjectTrigger 和 Gaze 脚本中也进行此更改,以便在场景中查找并检测到这些对象。

  13. 创建标记后,现在需要将标记应用于所有三个对象。 在“层次结构”中,按住“Shift”键,依次单击“胶囊”、“立方体”和“球体”对象,然后在“检查器”中,单击“标记”旁边的下拉菜单,再单击创建的“ObjectInScene”标记。

    检查器面板的屏幕截图,箭头指向“标记”。“未标记”菜单显示已选中“未标记”且已选中 ObjectInScene。 显示两个菜单的屏幕截图,其中突出显示了“创建”和“文件夹”。

第 6 章 - 创建 ApplicationInsightsTracker 类

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

  1. 基于用户交互创建事件,以提交到 Azure Application Insights。

  2. 根据用户交互创建相应的事件名称。

  3. 将事件提交到 Application Insights 服务实例。

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

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

    “项目”面板的屏幕截图。“资产”窗格中突出显示了“脚本”文件夹图标。 显示选择选项、创建和 C# 脚本的菜单选项的屏幕截图。

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

  3. 双击新的 “ApplicationInsightsTracker” 脚本以通过 “Visual Studio” 打开它

  4. 将脚本顶部的命名空间更新为如下所示:

        using Microsoft.ApplicationInsights;
        using Microsoft.ApplicationInsights.DataContracts;
        using Microsoft.ApplicationInsights.Extensibility;
        using UnityEngine;
    
  5. 在该类中插入以下变量:

        /// <summary>
        /// Allows this class to behavior like a singleton
        /// </summary>
        public static ApplicationInsightsTracker Instance;
    
        /// <summary>
        /// Insert your Instrumentation Key here
        /// </summary>
        internal string instrumentationKey = "Insert Instrumentation Key here";
    
        /// <summary>
        /// Insert your Application Id here
        /// </summary>
        internal string applicationId = "Insert Application Id here";
    
        /// <summary>
        /// Insert your API Key here
        /// </summary>
        internal string API_Key = "Insert API Key here";
    
        /// <summary>
        /// Represent the Analytic Custom Event object
        /// </summary>
        private TelemetryClient telemetryClient;
    
        /// <summary>
        /// Represent the Analytic object able to host gaze duration
        /// </summary>
        private MetricTelemetry metric;
    

    注意

    使用 Azure 门户中的服务密钥(如第 1 章 步骤 9 起所述)正确设置 “instrumentationKey、applicationId 和 API_Key” 的值

  6. 然后添加 “Start()” 和 “Awake()” 方法,当类初始化时将调用这些方法:

        /// <summary>
        /// Sets this class instance as a singleton
        /// </summary>
        void Awake()
        {
            Instance = this;
        }
    
        /// <summary>
        /// Use this for initialization
        /// </summary>
        void Start()
        {
            // Instantiate telemetry and metric
            telemetryClient = new TelemetryClient();
    
            metric = new MetricTelemetry();
    
            // Assign the Instrumentation Key to the Event and Metric objects
            TelemetryConfiguration.Active.InstrumentationKey = instrumentationKey;
    
            telemetryClient.InstrumentationKey = instrumentationKey;
        }
    
  7. 添加负责发送应用程序注册的事件和指标的方法:

        /// <summary>
        /// Submit the Event to Azure Analytics using the event trigger object
        /// </summary>
        public void RecordProximityEvent(string objectName)
        {
            telemetryClient.TrackEvent(CreateEventName(objectName));
        }
    
        /// <summary>
        /// Uses the name of the object involved in the event to create 
        /// and return an Event Name convention
        /// </summary>
        public string CreateEventName(string name)
        {
            string eventName = $"User near {name}";
            return eventName;
        }
    
        /// <summary>
        /// Submit a Metric to Azure Analytics using the metric gazed object
        /// and the time count of the gaze
        /// </summary>
        public void RecordGazeMetrics(string objectName, int time)
        {
            // Output Console information about gaze.
            Debug.Log($"Finished gazing at {objectName}, which went for <b>{time}</b> second{(time != 1 ? "s" : "")}");
    
            metric.Name = $"Gazed {objectName}";
    
            metric.Value = time;
    
            telemetryClient.TrackMetric(metric);
        }
    
  8. 返回到 Unity 之前,请务必在 Visual Studio 中保存所做的更改

第 7 章 - 创建 Gaze 脚本

下一个要创建的脚本是 “Gaze” 脚本。 此脚本负责创建从主摄像头向前投影的光线投射,以检测用户正在查看的对象。 在这种情况下,光线投射需要确定用户是否正在查看具有 “ObjectInScene” 标记的对象,然后计算用户凝视该对象的时间。

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

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

  3. 双击该脚本以通过 Visual Studio 打开它。

  4. 将现有代码替换为以下代码:

        using UnityEngine;
    
        public class Gaze : MonoBehaviour
        {
            /// <summary>
            /// Provides Singleton-like behavior to this class.
            /// </summary>
            public static Gaze Instance;
    
            /// <summary>
            /// Provides a reference to the object the user is currently looking at.
            /// </summary>
            public GameObject FocusedGameObject { get; private set; }
    
            /// <summary>
            /// Provides whether an object has been successfully hit by the raycast.
            /// </summary>
            public bool Hit { get; private set; }
    
            /// <summary>
            /// Provides a reference to compare whether the user is still looking at 
            /// the same object (and has not looked away).
            /// </summary>
            private GameObject _oldFocusedObject = null;
    
            /// <summary>
            /// Max Ray Distance
            /// </summary>
            private float _gazeMaxDistance = 300;
    
            /// <summary>
            /// Max Ray Distance
            /// </summary>
            private float _gazeTimeCounter = 0;
    
            /// <summary>
            /// The cursor object will be created when the app is running,
            /// this will store its values. 
            /// </summary>
            private GameObject _cursor;
        }
    
  5. 现在需要添加 Awake() 和 Start() 方法的代码

        private void Awake()
        {
            // Set this class to behave similar to singleton
            Instance = this;
            _cursor = CreateCursor();
        }
    
        void Start()
        {
            FocusedGameObject = null;
        }
    
        /// <summary>
        /// Create a cursor object, to provide what the user
        /// is looking at.
        /// </summary>
        /// <returns></returns>
        private GameObject CreateCursor()    
        {
            GameObject newCursor = GameObject.CreatePrimitive(PrimitiveType.Sphere);
    
            // Remove the collider, so it does not block raycast.
            Destroy(newCursor.GetComponent<SphereCollider>());
    
            newCursor.transform.localScale = new Vector3(0.1f, 0.1f, 0.1f);
    
            newCursor.GetComponent<MeshRenderer>().material.color = 
            Color.HSVToRGB(0.0223f, 0.7922f, 1.000f);
    
            newCursor.SetActive(false);
            return newCursor;
        }
    
  6. 在“Gaze”类中,在 “Update()” 方法中添加以下代码,以投影光线投射并检测命中目标:

        /// <summary>
        /// Called every frame
        /// </summary>
        void Update()
        {
            // Set the old focused gameobject.
            _oldFocusedObject = FocusedGameObject;
    
            RaycastHit hitInfo;
    
            // Initialize Raycasting.
            Hit = Physics.Raycast(Camera.main.transform.position, Camera.main.transform.forward, out hitInfo, _gazeMaxDistance);
    
            // Check whether raycast has hit.
            if (Hit == true)
            {
                // Check whether the hit has a collider.
                if (hitInfo.collider != null)
                {
                    // Set the focused object with what the user just looked at.
                    FocusedGameObject = hitInfo.collider.gameObject;
    
                    // Lerp the cursor to the hit point, which helps to stabilize the gaze.
                    _cursor.transform.position = Vector3.Lerp(_cursor.transform.position, hitInfo.point, 0.6f);
    
                    _cursor.SetActive(true);
                }
                else
                {
                    // Object looked on is not valid, set focused gameobject to null.
                    FocusedGameObject = null;
    
                    _cursor.SetActive(false);
                }
            }
            else
            {
                // No object looked upon, set focused gameobject to null.
                FocusedGameObject = null;
    
                _cursor.SetActive(false);
            }
    
            // Check whether the previous focused object is this same object. If so, reset the focused object.
            if (FocusedGameObject != _oldFocusedObject)
            {
                ResetFocusedObject();
            }
            // If they are the same, but are null, reset the counter. 
            else if (FocusedGameObject == null && _oldFocusedObject == null)
            {
                _gazeTimeCounter = 0;
            }
            // Count whilst the user continues looking at the same object.
            else
            {
                _gazeTimeCounter += Time.deltaTime;
            }
        }
    
  7. 添加“ResetFocusedObject()”方法,以在用户查看对象时将数据发送到 “Application Insights”。

        /// <summary>
        /// Reset the old focused object, stop the gaze timer, and send data if it
        /// is greater than one.
        /// </summary>
        public void ResetFocusedObject()
        {
            // Ensure the old focused object is not null.
            if (_oldFocusedObject != null)
            {
                // Only looking for objects with the correct tag.
                if (_oldFocusedObject.CompareTag("ObjectInScene"))
                {
                    // Turn the timer into an int, and ensure that more than zero time has passed.
                    int gazeAsInt = (int)_gazeTimeCounter;
    
                    if (gazeAsInt > 0)
                    {
                        //Record the object gazed and duration of gaze for Analytics
                        ApplicationInsightsTracker.Instance.RecordGazeMetrics(_oldFocusedObject.name, gazeAsInt);
                    }
                    //Reset timer
                    _gazeTimeCounter = 0;
                }
            }
        }
    
  8. 现在,你已经完成了 “Gaze” 脚本。 返回到 Unity 之前,请在 Visual Studio 中保存所做的更改

第 8 章 - 创建 ObjectTrigger 类

需要创建的下一个脚本是 “ObjectTrigger”,它负责:

  • 向主摄像头添加碰撞所需的组件。
  • 检测摄像头是否靠近标记为“ObjectInScene”的对象

若要创建脚本:

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

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

  3. 双击该脚本以通过 Visual Studio 打开它。 将现有代码替换为以下代码:

        using UnityEngine;
    
        public class ObjectTrigger : MonoBehaviour
        {
            private void Start()
            {
                // Add the Collider and Rigidbody components, 
                // and set their respective settings. This allows for collision.
                gameObject.AddComponent<SphereCollider>().radius = 1.5f;
    
                gameObject.AddComponent<Rigidbody>().useGravity = false;
            }
    
            /// <summary>
            /// Triggered when an object with a collider enters this objects trigger collider.
            /// </summary>
            /// <param name="collision">Collided object</param>
            private void OnCollisionEnter(Collision collision)
            {
                CompareTriggerEvent(collision, true);
            }
    
            /// <summary>
            /// Triggered when an object with a collider exits this objects trigger collider.
            /// </summary>
            /// <param name="collision">Collided object</param>
            private void OnCollisionExit(Collision collision)
            {
                CompareTriggerEvent(collision, false);
            }
    
            /// <summary>
            /// Method for providing debug message, and sending event information to InsightsTracker.
            /// </summary>
            /// <param name="other">Collided object</param>
            /// <param name="enter">Enter = true, Exit = False</param>
            private void CompareTriggerEvent(Collision other, bool enter)
            {
                if (other.collider.CompareTag("ObjectInScene"))
                {
                    string message = $"User is{(enter == true ? " " : " no longer ")}near <b>{other.gameObject.name}</b>";
    
                    if (enter == true)
                    {
                        ApplicationInsightsTracker.Instance.RecordProximityEvent(other.gameObject.name);
                    }
                    Debug.Log(message);
                }
            }
        }
    
  4. 返回到 Unity 之前,请务必在 Visual Studio 中保存所做的更改

第 9 章 - 创建 DataFromAnalytics 类

现在,你需要创建 “DataFromAnalytics” 脚本,它负责

  • 提取有关摄像头最靠近的对象的分析数据。
  • 使用服务密钥,以实现与 Azure Application Insights 服务实例的通信。
  • 根据具有的最高事件计数对场景中的对象进行排序。
  • 将最靠近的对象的材料颜色更改为“绿色”。

若要创建脚本:

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

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

  3. 双击该脚本以通过 Visual Studio 打开它。

  4. 插入以下命名空间:

        using Newtonsoft.Json;
        using System;
        using System.Collections;
        using System.Collections.Generic;
        using System.Linq;
        using UnityEngine;
        using UnityEngine.Networking;
    
  5. 在脚本中,插入以下内容:

        /// <summary>
        /// Number of most recent events to be queried
        /// </summary>
        private int _quantityOfEventsQueried = 10;
    
        /// <summary>
        /// The timespan with which to query. Needs to be in hours.
        /// </summary>
        private int _timepspanAsHours = 24;
    
        /// <summary>
        /// A list of the objects in the scene
        /// </summary>
        private List<GameObject> _listOfGameObjectsInScene;
    
        /// <summary>
        /// Number of queries which have returned, after being sent.
        /// </summary>
        private int _queriesReturned = 0;
    
        /// <summary>
        /// List of GameObjects, as the Key, with their event count, as the Value.
        /// </summary>
        private List<KeyValuePair<GameObject, int>> _pairedObjectsWithEventCount = new List<KeyValuePair<GameObject, int>>();
    
        // Use this for initialization
        void Start()
        {
            // Find all objects in scene which have the ObjectInScene tag (as there may be other GameObjects in the scene which you do not want).
            _listOfGameObjectsInScene = GameObject.FindGameObjectsWithTag("ObjectInScene").ToList();
    
            FetchAnalytics();
        }
    
  6. 在 “DataFromAnalytics” 类中,紧跟 “Start()” 方法之后添加以下名为 “FetchAnalytics()” 的方法。 此方法负责使用 GameObject 和占位符事件计数填充键值对列表。 然后,它会初始化 “GetWebRequest()” 协同例程。 调用 Application Insights 的查询结构也可以在此方法中找到,即查询 URL 终结点

        private void FetchAnalytics()
        {
            // Iterate through the objects in the list
            for (int i = 0; i < _listOfGameObjectsInScene.Count; i++)
            {
                // The current event number is not known, so set it to zero.
                int eventCount = 0;
    
                // Add new pair to list, as placeholder, until eventCount is known.
                _pairedObjectsWithEventCount.Add(new KeyValuePair<GameObject, int>(_listOfGameObjectsInScene[i], eventCount));
    
                // Set the renderer of the object to the default color, white
                _listOfGameObjectsInScene[i].GetComponent<Renderer>().material.color = Color.white;
    
                // Create the appropriate object name using Insights structure
                string objectName = _listOfGameObjectsInScene[i].name;
    
     		    // Build the queryUrl for this object.
     		    string queryUrl = Uri.EscapeUriString(string.Format(
                    "https://api.applicationinsights.io/v1/apps/{0}/events/$all?timespan=PT{1}H&$search={2}&$select=customMetric/name&$top={3}&$count=true",
     			    ApplicationInsightsTracker.Instance.applicationId, _timepspanAsHours, "Gazed " + objectName, _quantityOfEventsQueried));
    
    
                // Send this object away within the WebRequest Coroutine, to determine it is event count.
                StartCoroutine("GetWebRequest", new KeyValuePair<string, int>(queryUrl, i));
            }
        }
    
  7. 紧跟 “FetchAnalytics()” 方法的下方添加一个名为 “GetWebRequest()” 的方法,该方法会返回 IEnumerator。 此方法负责请求在 Application Insights 中调用与特定 GameObject 对应的事件的次数。 所有发送的查询都返回后,会调用 “DetermineWinner()” 方法

        /// <summary>
        /// Requests the data count for number of events, according to the
        /// input query URL.
        /// </summary>
        /// <param name="webQueryPair">Query URL and the list number count.</param>
        /// <returns></returns>
        private IEnumerator GetWebRequest(KeyValuePair<string, int> webQueryPair)
        {
            // Set the URL and count as their own variables (for readability).
            string url = webQueryPair.Key;
            int currentCount = webQueryPair.Value;
    
            using (UnityWebRequest unityWebRequest = UnityWebRequest.Get(url))
            {
                DownloadHandlerBuffer handlerBuffer = new DownloadHandlerBuffer();
    
                unityWebRequest.downloadHandler = handlerBuffer;
    
                unityWebRequest.SetRequestHeader("host", "api.applicationinsights.io");
    
                unityWebRequest.SetRequestHeader("x-api-key", ApplicationInsightsTracker.Instance.API_Key);
    
                yield return unityWebRequest.SendWebRequest();
    
                if (unityWebRequest.isNetworkError)
                {
                    // Failure with web request.
                    Debug.Log("<color=red>Error Sending:</color> " + unityWebRequest.error);
                }
                else
                {
                    // This query has returned, so add to the current count.
                    _queriesReturned++;
    
                    // Initialize event count integer.
                    int eventCount = 0;
    
                    // Deserialize the response with the custom Analytics class.
                    Analytics welcome = JsonConvert.DeserializeObject<Analytics>(unityWebRequest.downloadHandler.text);
    
                    // Get and return the count for the Event
                    if (int.TryParse(welcome.OdataCount, out eventCount) == false)
                    {
                        // Parsing failed. Can sometimes mean that the Query URL was incorrect.
                        Debug.Log("<color=red>Failure to Parse Data Results. Check Query URL for issues.</color>");
                    }
                    else
                    {
                        // Overwrite the current pair, with its actual values, now that the event count is known.
                        _pairedObjectsWithEventCount[currentCount] = new KeyValuePair<GameObject, int>(_pairedObjectsWithEventCount[currentCount].Key, eventCount);
                    }
    
                    // If all queries (compared with the number which was sent away) have 
                    // returned, then run the determine winner method. 
                    if (_queriesReturned == _pairedObjectsWithEventCount.Count)
                    {
                        DetermineWinner();
                    }
                }
            }
        }
    
  8. 下一个方法是 “DetermineWinner()”,它会根据最高事件计数对 GameObject 和 Int 对的列表进行排序。 然后,它会将该 GameObject 的材料颜色更改为绿色(根据反馈,它具有最高计数)。 此时将显示一条包含分析结果的消息。

        /// <summary>
        /// Call to determine the keyValue pair, within the objects list, 
        /// with the highest event count.
        /// </summary>
        private void DetermineWinner()
        {
            // Sort the values within the list of pairs.
            _pairedObjectsWithEventCount.Sort((x, y) => y.Value.CompareTo(x.Value));
    
            // Change its colour to green
            _pairedObjectsWithEventCount.First().Key.GetComponent<Renderer>().material.color = Color.green;
    
            // Provide the winner, and other results, within the console window. 
            string message = $"<b>Analytics Results:</b>\n " +
                $"<i>{_pairedObjectsWithEventCount.First().Key.name}</i> has the highest event count, " +
                $"with <i>{_pairedObjectsWithEventCount.First().Value.ToString()}</i>.\nFollowed by: ";
    
            for (int i = 1; i < _pairedObjectsWithEventCount.Count; i++)
            {
                message += $"{_pairedObjectsWithEventCount[i].Key.name}, " +
                    $"with {_pairedObjectsWithEventCount[i].Value.ToString()} events.\n";
            }
    
            Debug.Log(message);
        }
    
  9. 添加用于反序列化从 Application Insights 接收的 JSON 对象的类结构。 将这些类添加到类定义“之外”的 “DataFromAnalytics” 类文件的最底部。

        /// <summary>
        /// These classes represent the structure of the JSON response from Azure Insight
        /// </summary>
        [Serializable]
        public class Analytics
        {
            [JsonProperty("@odata.context")]
            public string OdataContext { get; set; }
    
            [JsonProperty("@odata.count")]
            public string OdataCount { get; set; }
    
            [JsonProperty("value")]
            public Value[] Value { get; set; }
        }
    
        [Serializable]
        public class Value
        {
            [JsonProperty("customMetric")]
            public CustomMetric CustomMetric { get; set; }
        }
    
        [Serializable]
        public class CustomMetric
        {
            [JsonProperty("name")]
            public string Name { get; set; }
        }
    
  10. 返回到 Unity 之前,请务必在 Visual Studio 中保存所做的更改

第 10 章 - 创建 Movement 类

“Movement” 脚本是需要创建的下一个脚本。 它负责:

  • 根据摄像头查看的方向移动主摄像头。
  • 向场景对象添加所有其他脚本。

若要创建脚本:

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

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

  3. 双击该脚本以通过 Visual Studio 打开它。

  4. 将现有代码替换为以下代码:

        using UnityEngine;
        using UnityEngine.XR.WSA.Input;
    
        public class Movement : MonoBehaviour
        {
            /// <summary>
            /// The rendered object representing the right controller.
            /// </summary>
            public GameObject Controller;
    
            /// <summary>
            /// The movement speed of the user.
            /// </summary>
            public float UserSpeed;
    
            /// <summary>
            /// Provides whether source updates have been registered.
            /// </summary>
            private bool _isAttached = false;
    
            /// <summary>
            /// The chosen controller hand to use. 
            /// </summary>
            private InteractionSourceHandedness _handness = InteractionSourceHandedness.Right;
    
            /// <summary>
            /// Used to calculate and proposes movement translation.
            /// </summary>
            private Vector3 _playerMovementTranslation;
    
            private void Start()
            {
                // You are now adding components dynamically 
                // to ensure they are existing on the correct object  
    
                // Add all camera related scripts to the camera. 
                Camera.main.gameObject.AddComponent<Gaze>();
                Camera.main.gameObject.AddComponent<ObjectTrigger>();
    
                // Add all other scripts to this object.
                gameObject.AddComponent<ApplicationInsightsTracker>();
                gameObject.AddComponent<DataFromAnalytics>();
            }
    
            // Update is called once per frame
            void Update()
            {
    
            }
        }
    
  5. 在 “Movement” 类中的空的 “Update()” 方法下,插入以下方法,以允许用户使用手部控制器在虚拟空间中移动:

        /// <summary>
        /// Used for tracking the current position and rotation of the controller.
        /// </summary>
        private void UpdateControllerState()
        {
    #if UNITY_WSA && UNITY_2017_2_OR_NEWER
            // Check for current connected controllers, only if WSA.
            string message = string.Empty;
    
            if (InteractionManager.GetCurrentReading().Length > 0)
            {
                foreach (var sourceState in InteractionManager.GetCurrentReading())
                {
                    if (sourceState.source.kind == InteractionSourceKind.Controller && sourceState.source.handedness == _handness)
                    {
                        // If a controller source is found, which matches the selected handness, 
                        // check whether interaction source updated events have been registered. 
                        if (_isAttached == false)
                        {
                            // Register events, as not yet registered.
                            message = "<color=green>Source Found: Registering Controller Source Events</color>";
                            _isAttached = true;
    
                            InteractionManager.InteractionSourceUpdated += InteractionManager_InteractionSourceUpdated;
                        }
    
                        // Update the position and rotation information for the controller.
                        Vector3 newPosition;
                        if (sourceState.sourcePose.TryGetPosition(out newPosition, InteractionSourceNode.Pointer) && ValidPosition(newPosition))
                        {
                            Controller.transform.localPosition = newPosition;
                        }
    
                        Quaternion newRotation;
    
                        if (sourceState.sourcePose.TryGetRotation(out newRotation, InteractionSourceNode.Pointer) && ValidRotation(newRotation))
                        {
                            Controller.transform.localRotation = newRotation;
                        }
                    }
                }
            }
            else
            {
                // Controller source not detected. 
                message = "<color=blue>Trying to detect controller source</color>";
    
                if (_isAttached == true)
                {
                    // A source was previously connected, however, has been lost. Disconnected
                    // all registered events. 
    
                    _isAttached = false;
    
                    InteractionManager.InteractionSourceUpdated -= InteractionManager_InteractionSourceUpdated;
    
                    message = "<color=red>Source Lost: Detaching Controller Source Events</color>";
                }
            }
    
            if(message != string.Empty)
            {
                Debug.Log(message);
            }
    #endif
        }
    
        /// <summary>
        /// This registered event is triggered when a source state has been updated.
        /// </summary>
        /// <param name="obj"></param>
        private void InteractionManager_InteractionSourceUpdated(InteractionSourceUpdatedEventArgs obj)
        {
            if (obj.state.source.handedness == _handness)
            {
                if(obj.state.thumbstickPosition.magnitude > 0.2f)
                {
                    float thumbstickY = obj.state.thumbstickPosition.y;
    
                    // Vertical Input.
                    if (thumbstickY > 0.3f || thumbstickY < -0.3f)
                    {
                        _playerMovementTranslation = Camera.main.transform.forward;
                        _playerMovementTranslation.y = 0;
                        transform.Translate(_playerMovementTranslation * UserSpeed * Time.deltaTime * thumbstickY, Space.World);
                    }
                }
            }
        }
    
        /// <summary>
        /// Check that controller position is valid. 
        /// </summary>
        /// <param name="inputVector3">The Vector3 to check</param>
        /// <returns>The position is valid</returns>
        private bool ValidPosition(Vector3 inputVector3)
        {
            return !float.IsNaN(inputVector3.x) && !float.IsNaN(inputVector3.y) && !float.IsNaN(inputVector3.z) && !float.IsInfinity(inputVector3.x) && !float.IsInfinity(inputVector3.y) && !float.IsInfinity(inputVector3.z);
        }
    
        /// <summary>
        /// Check that controller rotation is valid. 
        /// </summary>
        /// <param name="inputQuaternion">The Quaternion to check</param>
        /// <returns>The rotation is valid</returns>
        private bool ValidRotation(Quaternion inputQuaternion)
        {
            return !float.IsNaN(inputQuaternion.x) && !float.IsNaN(inputQuaternion.y) && !float.IsNaN(inputQuaternion.z) && !float.IsNaN(inputQuaternion.w) && !float.IsInfinity(inputQuaternion.x) && !float.IsInfinity(inputQuaternion.y) && !float.IsInfinity(inputQuaternion.z) && !float.IsInfinity(inputQuaternion.w);
        }   
    
  6. 最后,在 “Update()” 方法中添加方法调用。

        // Update is called once per frame
        void Update()
        {
            UpdateControllerState();
        }
    
  7. 返回到 Unity 之前,请务必在 Visual Studio 中保存所做的更改

第 11 章 - 设置脚本引用

在本章中,你需要将 “Movement” 脚本置于“父摄像头”上,并设置其引用目标。 然后,该脚本会负责将其他脚本放置在需要的位置。

  1. 从“项目”面板的“脚本”文件夹中,将 “Movement” 脚本拖动到“层次结构面板”中的“父摄像头”对象。

    “项目”和“层次结构”面板的屏幕截图。突出显示移动。

  2. 单击“父摄像头”。 在“层次结构面板”中,将“右手”对象从“层次结构面板”拖动到“检查器面板”中的引用目标“控制器”。 将“用户速度”设置为“5”,如下图所示。

    显示“层次结构”和“检查器”面板的屏幕截图。这两个面板上的右手连接线。

第 12 章 - 生成 Unity 项目

该项目的 Unity 部分所需的一切现已完成,是时候从 Unity 构建它了。

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

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

    “生成设置”窗口的屏幕截图,其中显示了“生成中的场景”。

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

    文件资源管理器的屏幕截图,其中突出显示了“生成”文件夹。

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

      显示MR_Azure_Insights文件夹的文件资源管理器的屏幕截图。

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

  4. 生成后,随即出现“文件资源管理器”并显示新项目的位置。

第 13 章 - 将 MR_Azure_Application_Insights 应用部署到计算机

若要在本地计算机上部署 “MR_Azure_Application_Insights” 应用:

  1. 在 “Visual Studio” 中打开 “MR_Azure_Application_Insights” 应用的解决方案文件。

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

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

    Visual Studio 解决方案配置屏幕的屏幕截图,其中显示了菜单栏中的“调试”。

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

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

  6. 启动混合现实应用程序。

  7. 在场景周围移动,接近对象并进行查看,当 Azure 见解服务收集了足够的事件数据时,它会将最接近的对象设置为绿色。

重要

尽管等待服务收集事件和指标的平均时间大约为 15 分钟,但在某些情况下,可能长达 1 小时。

第 14 章 - Application Insights 服务门户

在周围场景漫游并凝视多个对象后,可以在 Application Insights 服务门户中查看收集的数据。

  1. 返回到 Application Insights 服务门户。

  2. 选择“指标资源管理器”

    显示选项列表的 MyNewInsight 面板的屏幕截图。“调查”部分列出了指标资源管理器。

  3. 随即将打开一个包含图形的选项卡,该图形表示与应用程序相关的事件和指标。 如上所述,可能需要一段时间(长达 1 小时),图形中才能显示数据

    “指标资源管理器”的屏幕截图,其中显示了事件和指标图。

  4. 选择“按应用程序版本的事件总数”中的事件栏,以查看事件及其名称的明细。

    “搜索”面板的屏幕截图,其中显示了自定义事件筛选器的结果。

你已完成 Application Insights 服务应用程序

恭喜,你生成了一个混合现实应用,它利用 Application Insights 服务来监视应用中的用户活动。

课程欢迎屏幕。

额外练习

练习 1

尝试生成(而不是手动创建)ObjectInScene 对象,并在脚本内的平面上设置它们的坐标。 通过这种方式,你可以询问 Azure 最热门的对象(凝视或邻近感应对象),并额外生成一个这样的对象。

练习 2

按时间对 Application Insights 结果进行排序,以便获取最相关的数据并在应用程序中实现时效性数据。