HoloLens(第一代)和 Azure 309:Application insights
注意
混合现实学院教程在制作时考虑到了 HoloLens(第一代)和混合现实沉浸式头戴显示设备。 因此,对于仍在寻求这些设备的开发指导的开发人员而言,我们觉得很有必要保留这些教程。 我们不会在这些教程中更新 HoloLens 2 所用的最新工具集或集成相关的内容。 我们将维护这些教程,使之持续适用于支持的设备。 将来会发布一系列演示如何针对 HoloLens 2 进行开发的新教程。 此通知将在教程发布时通过指向这些教程的链接进行更新。
在本课程中,你将学习如何使用 Azure Application Insights API 将 Application Insights 功能添加到混合现实应用程序,以便收集有关用户行为的分析。
Application Insights 是一项 Microsoft 服务,它允许开发人员从其应用程序收集分析,并从易于使用的门户对其进行管理。 分析可以是从性能到自定义信息等任何你想要收集的内容。 有关详细信息,请访问“Application Insights”页。
完成本课程后,你将获得一个可执行以下操作的混合现实沉浸式头戴显示设备应用程序:
- 让用户能够凝视周围场景并在其中移动。
- 通过使用凝视和邻近感应场景对象来触发向 Application Insights 服务发送分析。
- 应用还将调用服务,并获取有关过去 24 小时内用户最接近的对象的信息。 该对象会将其颜色更改为绿色。
本课程将介绍如何将 Application Insights 服务的结果纳入到基于 Unity 的示例应用程序中。 你可以自行决定将这些概念应用到你可能会生成的自定义应用程序。
设备支持
课程 | HoloLens | 沉浸式头戴显示设备 |
---|---|---|
MR 和 Azure 309:Application Insights | ✔️ | ✔️ |
注意
尽管本课程主要重点介绍 Windows Mixed Reality 沉浸式 (VR) 头戴显示设备,但你也可以将本课程中学到的内容应用到 Microsoft HoloLens。 随着课程的进行,你将看到有关支持 HoloLens 可能需要进行的任何更改的说明。 使用 HoloLens 时,你可能会在语音捕获过程中注意到某些回声。
先决条件
注意
本教程专为具有 Unity 和 C# 基本经验的开发人员设计。 另请注意,本文档中的先决条件和书面说明在编写时(2018 年 7 月)已经过测试和验证。 可以随意使用最新的软件(如安装工具一文所列),但不应假设本课程中的信息将与你在较新的软件中找到的信息(而不是下面列出的内容)完全匹配。
建议在本课程中使用以下硬件和软件:
- 一台与 Windows Mixed Reality 兼容的开发电脑,用于沉浸式 (VR) 头戴显示设备方面的开发
- 已启用开发人员模式的 Windows 10 Fall Creators Update(或更高版本)
- 最新的 Windows 10 SDK
- Unity 2017.4
- Visual Studio 2017
- Windows Mixed Reality 沉浸式 (VR) 头戴显示设备或已启用开发人员模式的 Microsoft HoloLens
- 一组内置麦克风的耳机(如果头戴显示设备未内置麦克风和扬声器)
- 针对 Azure 设置和 Application Insights 数据检索的 Internet 访问
开始之前
为了避免在生成此项目时遇到问题,强烈建议在根文件夹或接近根的文件夹中创建本教程中的项目(长文件夹路径可能会在生成时导致问题)。
警告
请注意,Application Insights 接收数据需要一些时间,因此请耐心等待。 如果要检查此服务是否已收到数据,请查看第 14 章,其中介绍如何导航门户。
第 1 章 - Azure 门户
若要使用 Application Insights,需要在 Azure 门户中创建和配置 Application Insights 服务。
登录到 Azure 门户。
注意
如果你没有 Azure 帐户,需要创建一个。 如果你在课堂或实验室场景中跟着本教程学习,请让讲师或监督人员帮助设置你的新帐户。
登录后,单击左上角的“新建”,搜索“Application Insights”,并单击“Enter”。
注意
在更新的门户中,“新建”一词可能已替换为“创建资源”。
右侧的新页将提供 Azure Application Insights 服务的说明。 在此页面的左下角,选择“创建”按钮,以创建与此服务的关联。
单击“创建”后:
插入此服务实例的所需名称。
对于“应用程序类型”,请选择“常规”。
选择相应的订阅。
选择一个资源组或创建一个新资源组。 通过资源组,可监视和预配 Azure 资产集合、控制其访问权限并管理其计费。 建议保留与常用资源组下的单个项目(例如这些课程)关联的所有 Azure 服务。
若要详细了解 Azure 资源组,请访问资源组一文。
选择“位置” 。
你还需要确认已了解适用于此服务的条款和条件。
选择创建。
单击“创建”后,必须等待服务创建完成,这可能需要一分钟时间。
创建服务实例后,门户中将显示一条通知。
选择通知以浏览新的服务实例。
单击通知中的“转到资源”按钮,浏览新的服务实例。 将访问新的 Azure Insights 服务实例。
注意
保持此网页打开且易于访问,你将经常回到这里查看收集的数据。
重要
若要实现 Application Insights,需要使用三 (3) 个特定值:“检测密钥”、“应用程序 ID” 和 “API 密钥”。 下面将介绍如何从服务中检索这些值。 请确保在空白记事本页上记下这些值,因为不久将会在代码中用到它们。
若要查找“检测密钥”,需要向下滚动服务函数列表,然后选择“属性”,随即出现的选项卡将显示“服务密钥”。
在“属性”下方一点,会发现“API 访问权限”,需要单击它。 右侧面板将提供应用的应用程序 ID。
保持打开“应用程序 ID”面板,单击“创建 API 密钥”,随即将打开“创建 API 密钥”面板。
在打开的“创建 API 密钥”面板中,键入说明,并勾选三个框。
单击“生成密钥”。 将创建并显示 API 密钥。
警告
“服务密钥”只会在此显示,因此请确保立刻复制服务密钥。
第 2 章 - 设置 Unity 项目
下面是用于使用混合现实进行开发的典型设置,因此对其他项目来说,这是一个不错的模板。
打开 Unity,单击“新建”。
现在,需要提供 Unity 项目名称,并插入 “MR_Azure_Application_Insights”。 请确保将“模板”设置为“3D”。 将“位置”设置为适合你的位置(请记住,越接近根目录越好)。 然后,单击“创建项目”。
当 Unity 处于打开状态时,最好是检查默认“脚本编辑器”是否设置为“Visual Studio”。 转到“编辑”>“首选项”,然后在新窗口中导航到“外部工具”。 将外部脚本编辑器更改为 Visual Studio 2017。 关闭“首选项”窗口。
接下来,转到“文件”>“生成设置”,然后单击“切换平台”按钮将平台切换到“通用 Windows 平台”。
转到“文件”>“生成设置”,并确保:
将“目标设备”设置为“任何设备”
对于 Microsoft HoloLens,请将“目标设备”设置为“HoloLens”。
将“生成类型”设置为“D3D”
将“SDK”设置为“最新安装的版本”
将“生成并运行”设置为“本地计算机”
保存场景并将其添加到生成。
通过选择“添加开放场景”来执行此操作。 将出现一个保存窗口。
创建一个新文件夹用于此场景和未来的任何场景,然后单击“新建文件夹”按钮创建新文件夹,将其命名为“场景”。
打开新创建的“场景”文件夹,然后在“文件名:”文本字段中,键入“ApplicationInsightsScene”,然后单击“保存”。
在“生成设置”中,其余设置目前应保留为默认值。
在“生成设置”窗口中,选择“播放器设置”,将在“检查器”所在的空间中打开相关面板。
在此面板中,需要验证一些设置:
在“其他设置”选项卡中:
脚本运行时版本应为实验性版本(.NET 4.6 等效版本),这将触发重启编辑器的需要。
“脚本后端”应为 “.NET”
“API 兼容性级别”应为“.NET 4.6”
在“发布设置”选项卡的“功能”下,检查以下内容:
InternetClient
再往下滑面板,在“XR 设置”(在“发布设置”下方)中,勾选“支持的虚拟现实”,确保已添加“Windows Mixed Reality SDK”。
返回“生成设置”,此时 “Unity C#” 项目不再灰显;勾选此内容旁边的复选框。
关闭“生成设置”窗口 。
保存场景和项目(“文件”>“保存场景/文件”>“保存项目”)。
第 3 章 - 导入 Unity 包
重要
如果要跳过本课程的“Unity 设置”组件并继续直接编写代码,请随意下载此 Azure-MR-309.unitypackage,并将其作为自定义包导入项目中。 这还将包含下一章中的 DLL。 导入后,请继续学习第 6 章。
重要
若要在 Unity 中使用 Application Insights,需要为其导入 DLL 以及 Newtonsoft DLL。 Unity 中当前存在一个已知问题,即需要在导入后重新配置插件。 解决 bug 后不再需要执行这些步骤(本部分的 4-7)。
若要将 Application Insights 导入自己的项目中,请确保已下载包含插件的“.unitypackage”。 然后执行以下操作:
通过使用“资产”>“导入包”>“自定义包”菜单选项,将“.unitypackage”添加到 Unity。
在弹出的“导入 Unity 包”框中,确保“插件”下面的所有内容(包括插件)都被选中。
单击“导入”按钮,将项添加到项目。
转到项目视图中“插件”下的“Insights”文件夹,然后仅选择以下插件:
- Microsoft.ApplicationInsights
选中此插件后,请确保“任何平台”处于“未选中状态”,并确保“WSAPlayer”也处于“未选中状态”,然后单击“应用”。 这样做只是为了确认文件配置正确。
注意
按此方式标记这些插件会将它们配置为仅在 Unity 编辑器中使用。 WSA 文件夹中还有一组不同的 DLL,从 Unity 导出项目后,将使用它们。
接下来,需要打开 “Insights” 文件夹中的 “WSA” 文件夹。 随后会显示配置的文件的副本。 选择此文件,然后在检查器中确保未选中任何平台,然后确保仅检查 WSAPlayer。 单击“应用”。
现在需要执行“步骤 4-6”,但需要改为针对 Newtonsoft 插件。 有关结果的外观,请参阅以下屏幕截图。
第 4 章 - 设置摄像头和用户控件
在本章中,你将设置摄像头和控件,以允许用户在场景中查看和移动。
右键单击“层次结构”面板中的空白区域,然后单击“创建”>“空白”。
将新的空 GameObject 重命名为“父摄像头”。
右键单击“层次结构”面板中的空白区域,然后依次单击“3D 对象”、“球体”。
将“球体”重命名为“右手”。
将“右手”的“转换缩放比例”设置为“0.1,0.1,0.1”
通过单击“球体碰撞体”组件中的齿轮并单击“删除组件”,从“右手”中删除“球体碰撞体”组件。
在“层次结构”面板中,将“主摄像头”和“右手”对象拖动到“父摄像头”对象上。
将“主摄像头”和“右手”对象的“转换位置”设置为“0,0,0”。
第 5 章 - 在 Unity 场景中设置对象
现在,为场景创建一些基本形状,以便用户可以与之交互。
右键单击“层次结构”面板中的空白区域,然后单击“3D 对象”并选择“平面”。
将平面的“转换位置”设置为“0,-1,0”。
将平面的“转换缩放比例”设置为“5,1,5”。
创建要与平面对象一起使用的基本材料,以便其他形状更易于查看。 导航到“项目”面板,右键单击,然后依次单击“创建”、“文件夹”,以创建新文件夹。 将其命名为“材料”。
打开“材料”文件夹,然后右键单击,单击“创建”,再单击“材料”,以创建新材料。 将其命名为“蓝色”。
选中新的“蓝色”材料后,查看“检查器”,然后单击“反照率”旁边的矩形窗口。 选择一种蓝色(下图为“十六进制颜色:#3592FFFF”)。 选择后,单击关闭按钮。
将新材料从“材料”文件夹拖动到场景中新创建的“平面”上(或将其拖放到“层次结构”中的“平面”对象上)。
右键单击“层次结构”面板中的空白区域,然后依次单击“3D 对象”、“胶囊”。
- 选择“胶囊”后,将其转换位置更改为:-10、1、0。
右键单击“层次结构”面板中的空白区域,然后依次单击“3D 对象”、“立方体”。
- 选择多维数据集后,将其转换位置更改为:0、0、10。
右键单击“层次结构”面板中的空白区域,然后依次单击“3D 对象”、“球体”。
- 选中 Sphere 后,将其转换位置更改为:10、0、0。
注意
这些“位置”值是建议值。 可随意将对象设置在任何所需的位置,但如果对象距离摄像头不远,应用程序用户会更轻松。
当应用程序运行时,它需要能够识别场景中的对象,为此,需要标记对象。 选择其中一个对象,然后在“检查器”面板中,单击“添加标记...”,此时“检查器”将与“标记和层”面板交换。
单击 +(加号)符号,然后将标记名称键入为“ObjectInScene”。
警告
如果为标记使用其他名称,则需要确保在稍后的 DataFromAnalytics、ObjectTrigger 和 Gaze 脚本中也进行此更改,以便在场景中查找并检测到这些对象。
创建标记后,现在需要将标记应用于所有三个对象。 在“层次结构”中,按住“Shift”键,依次单击“胶囊”、“立方体”和“球体”对象,然后在“检查器”中,单击“标记”旁边的下拉菜单,再单击创建的“ObjectInScene”标记。
第 6 章 - 创建 ApplicationInsightsTracker 类
需要创建的第一个脚本是 “ApplicationInsightsTracker”,它负责:
基于用户交互创建事件,以提交到 Azure Application Insights。
根据用户交互创建相应的事件名称。
将事件提交到 Application Insights 服务实例。
若要创建此类,请执行以下操作:
在“项目”面板右键单击,然后单击“创建”>“文件夹”。 将文件夹命名为“脚本”。
创建“脚本”文件夹后,双击文件夹打开它。 然后,在该文件夹中,右键单击并单击“创建”>“C# 脚本”。 将脚本命名为“ApplicationInsightsTracker”。
双击新的 “ApplicationInsightsTracker” 脚本以通过 “Visual Studio” 打开它。
将脚本顶部的命名空间更新为如下所示:
using Microsoft.ApplicationInsights; using Microsoft.ApplicationInsights.DataContracts; using Microsoft.ApplicationInsights.Extensibility; using UnityEngine;
在该类中插入以下变量:
/// <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” 的值。
然后添加 “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; }
添加负责发送应用程序注册的事件和指标的方法:
/// <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); }
返回到 Unity 之前,请务必在 Visual Studio 中保存所做的更改。
第 7 章 - 创建 Gaze 脚本
下一个要创建的脚本是 “Gaze” 脚本。 此脚本负责创建从主摄像头向前投影的光线投射,以检测用户正在查看的对象。 在这种情况下,光线投射需要确定用户是否正在查看具有 “ObjectInScene” 标记的对象,然后计算用户凝视该对象的时间。
双击“脚本”文件夹,将其打开。
右键单击“脚本”文件夹,然后单击“创建”>“C# 脚本”。 将脚本命名为“Gaze”。
双击该脚本以通过 Visual Studio 打开它。
将现有代码替换为以下代码:
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; }
现在需要添加 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; }
在“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; } }
添加“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; } } }
现在,你已经完成了 “Gaze” 脚本。 返回到 Unity 之前,请在 Visual Studio 中保存所做的更改。
第 8 章 - 创建 ObjectTrigger 类
需要创建的下一个脚本是 “ObjectTrigger”,它负责:
- 向主摄像头添加碰撞所需的组件。
- 检测摄像头是否靠近标记为“ObjectInScene”的对象。
若要创建脚本:
双击“脚本”文件夹,将其打开。
右键单击“脚本”文件夹,然后单击“创建”>“C# 脚本”。 将脚本命名为“ObjectTrigger”。
双击该脚本以通过 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); } } }
返回到 Unity 之前,请务必在 Visual Studio 中保存所做的更改。
第 9 章 - 创建 DataFromAnalytics 类
现在,你需要创建 “DataFromAnalytics” 脚本,它负责:
- 提取有关摄像头最靠近的对象的分析数据。
- 使用服务密钥,以实现与 Azure Application Insights 服务实例的通信。
- 根据具有的最高事件计数对场景中的对象进行排序。
- 将最靠近的对象的材料颜色更改为“绿色”。
若要创建脚本:
双击“脚本”文件夹,将其打开。
右键单击“脚本”文件夹,然后单击“创建”>“C# 脚本”。 将脚本命名为“DataFromAnalytics”。
双击该脚本以通过 Visual Studio 打开它。
插入以下命名空间:
using Newtonsoft.Json; using System; using System.Collections; using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEngine.Networking;
在脚本中,插入以下内容:
/// <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(); }
在 “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)); } }
紧跟 “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(); } } } }
下一个方法是 “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); }
添加用于反序列化从 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; } }
返回到 Unity 之前,请务必在 Visual Studio 中保存所做的更改。
第 10 章 - 创建 Movement 类
“Movement” 脚本是需要创建的下一个脚本。 它负责:
- 根据摄像头查看的方向移动主摄像头。
- 向场景对象添加所有其他脚本。
若要创建脚本:
双击“脚本”文件夹,将其打开。
右键单击“脚本”文件夹,然后单击“创建”>“C# 脚本”。 将脚本命名为 “Movement”。
双击该脚本以通过 Visual Studio 打开它。
将现有代码替换为以下代码:
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() { } }
在 “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); }
最后,在 “Update()” 方法中添加方法调用。
// Update is called once per frame void Update() { UpdateControllerState(); }
返回到 Unity 之前,请务必在 Visual Studio 中保存所做的更改。
第 11 章 - 设置脚本引用
在本章中,你需要将 “Movement” 脚本置于“父摄像头”上,并设置其引用目标。 然后,该脚本会负责将其他脚本放置在需要的位置。
从“项目”面板的“脚本”文件夹中,将 “Movement” 脚本拖动到“层次结构面板”中的“父摄像头”对象。
单击“父摄像头”。 在“层次结构面板”中,将“右手”对象从“层次结构面板”拖动到“检查器面板”中的引用目标“控制器”。 将“用户速度”设置为“5”,如下图所示。
第 12 章 - 生成 Unity 项目
该项目的 Unity 部分所需的一切现已完成,是时候从 Unity 构建它了。
导航到“生成设置”(“文件”>“生成设置”)。
在“生成设置”窗口中,单击“生成”。
随即将弹出一个“文件资源管理器”窗口,提示选择生成的位置。 创建一个新的文件夹(单击左上角的“新建文件夹”)并将其命名为“生成”。
打开新的“生成”文件夹,然后创建另一个文件夹(再次使用“新建文件夹”)并将其命名为“MR_Azure_Application_Insights”。
选中 “MR_Azure_Application_Insights” 文件夹后,单击“选择文件夹”。 生成项目大约需要一分钟时间。
生成后,随即出现“文件资源管理器”并显示新项目的位置。
第 13 章 - 将 MR_Azure_Application_Insights 应用部署到计算机
若要在本地计算机上部署 “MR_Azure_Application_Insights” 应用:
在 “Visual Studio” 中打开 “MR_Azure_Application_Insights” 应用的解决方案文件。
在“解决方案平台”中,选择“x86,本地计算机”。
在“解决方案配置”中,选择“调试”。
转到“生成”菜单,并单击“部署解决方案”,将应用程序旁加载到计算机。
应用现在应显示在已安装的应用列表中,随时可以启动。
启动混合现实应用程序。
在场景周围移动,接近对象并进行查看,当 Azure 见解服务收集了足够的事件数据时,它会将最接近的对象设置为绿色。
重要
尽管等待服务收集事件和指标的平均时间大约为 15 分钟,但在某些情况下,可能长达 1 小时。
第 14 章 - Application Insights 服务门户
在周围场景漫游并凝视多个对象后,可以在 Application Insights 服务门户中查看收集的数据。
返回到 Application Insights 服务门户。
选择“指标资源管理器”。
随即将打开一个包含图形的选项卡,该图形表示与应用程序相关的事件和指标。 如上所述,可能需要一段时间(长达 1 小时),图形中才能显示数据
选择“按应用程序版本的事件总数”中的事件栏,以查看事件及其名称的明细。
你已完成 Application Insights 服务应用程序
恭喜,你生成了一个混合现实应用,它利用 Application Insights 服务来监视应用中的用户活动。
额外练习
练习 1
尝试生成(而不是手动创建)ObjectInScene 对象,并在脚本内的平面上设置它们的坐标。 通过这种方式,你可以询问 Azure 最热门的对象(凝视或邻近感应对象),并额外生成一个这样的对象。
练习 2
按时间对 Application Insights 结果进行排序,以便获取最相关的数据并在应用程序中实现时效性数据。