MR 共享 240:多个 HoloLens 设备MR Sharing 240: Multiple HoloLens devices

备注

混合现实学院教程在制作时考虑到了 HoloLens(第一代)和混合现实沉浸式头戴显示设备。The Mixed Reality Academy tutorials were designed with HoloLens (1st gen) and Mixed Reality Immersive Headsets in mind. 因此,对于仍在寻求这些设备的开发指导的开发人员而言,我们觉得很有必要保留这些教程。As such, we feel it is important to leave these tutorials in place for developers who are still looking for guidance in developing for those devices. 我们 不会 在这些教程中更新 HoloLens 2 所用的最新工具集或集成相关的内容。These tutorials will not be updated with the latest toolsets or interactions being used for HoloLens 2. 我们将维护这些教程,使之持续适用于支持的设备。They will be maintained to continue working on the supported devices. 已经为 HoloLens 2 发布了一系列新教程A new series of tutorials has been posted for HoloLens 2.

随着我们在空间中的发展,我们将在世界各地提供全息影像。Holograms are given presence in our world by remaining in place as we move about in space. 通过使用各种 坐标系统 跟踪对象的位置和方向,HoloLens 保留了全息影像。HoloLens keeps holograms in place by using various coordinate systems to keep track of the location and orientation of objects. 当我们在设备之间共享这些坐标系统时,我们可以创建共享体验,使我们能够参与共享全息环境。When we share these coordinate systems between devices, we can create a shared experience that allows us to take part in a shared holographic world.

在本教程中,我们将:In this tutorial, we will:

  • 为共享体验设置网络。Setup a network for a shared experience.
  • 跨 HoloLens 设备共享全息影像。Share holograms across HoloLens devices.
  • 了解我们的共享全息领域中的其他人员。Discover other people in our shared holographic world.
  • 创建可面向其他玩家的共享交互式体验,并在其上启动炮弹!Create a shared interactive experience where you can target other players - and launch projectiles at them!

设备支持Device support

课程Course HoloLensHoloLens 沉浸式头戴显示设备Immersive headsets
MR 共享 240:多个 HoloLens 设备MR Sharing 240: Multiple HoloLens devices ✔️✔️

开始之前Before you start

必备条件Prerequisites

项目文件Project files

  • 下载项目所需的 文件Download the files required by the project. 需要 Unity 2017.2 或更高版本。Requires Unity 2017.2 or later.
    • 如果仍需要 Unity 5.6 支持,请使用 此版本If you still need Unity 5.6 support, please use this release.
    • 如果仍需要 Unity 5.5 支持,请使用 此版本If you still need Unity 5.5 support, please use this release.
    • 如果仍需要 Unity 5.4 支持,请使用 此版本If you still need Unity 5.4 support, please use this release.
  • 取消将文件存档到桌面或其他易于访问的位置。Un-archive the files to your desktop or other easy to reach location. 将文件夹名称保留为 SharedHologramsKeep the folder name as SharedHolograms.

备注

如果要在下载之前查看源代码, 可在 GitHub 上找到。If you want to look through the source code before downloading, it's available on GitHub.

第1章-Holo WorldChapter 1 - Holo World

在本章中,我们将设置第一个 Unity 项目,并逐步完成生成和部署过程。In this chapter, we'll setup our first Unity project and step through the build and deploy process.

目标Objectives

  • 安装 Unity 以开发全息版应用。Setup Unity to develop holographic apps.
  • 查看全息图!See your hologram!

InstructionsInstructions

  • 启动 Unity。Start Unity.
  • 选择“打开” 。Select Open.
  • 输入位置作为之前 unarchived 的 SharedHolograms 文件夹。Enter location as the SharedHolograms folder you previously unarchived.
  • 选择 " 项目名称 ",然后单击 " 选择文件夹"。Select Project Name and click Select Folder.
  • 层次结构 中,右键单击 主相机 ,然后选择 " 删除"。In the Hierarchy, right-click the Main Camera and select Delete.
  • HoloToolkit/prototyping/相机 文件夹中,找到 摄像机的主 prefab。In the HoloToolkit-Sharing-240/Prefabs/Camera folder, find the Main Camera prefab.
  • 主相机 拖放到 层次结构 中。Drag and drop the Main Camera into the Hierarchy.
  • 层次结构 中,单击 " 创建 " 并 创建空In the Hierarchy, click on Create and Create Empty.
  • 右键单击新的 " GameObject ",然后选择 " 重命名"。Right-click the new GameObject and select Rename.
  • 将 GameObject 重命名为 HologramCollectionRename the GameObject to HologramCollection.
  • 选择 层次结构 中的 HologramCollection 对象。Select the HologramCollection object in the Hierarchy.
  • 检查器 中,将 转换位置 设置为: X:0,Y:-0.25,Z: 2In the Inspector set the transform position to: X: 0, Y: -0.25, Z: 2.
  • 在 "项目" 面板 的 "全息影像" 文件夹中,找到 " EnergyHub 资产"。In the Holograms folder in the Project panel, find the EnergyHub asset.
  • EnergyHub 对象从 " 项目" 面板 中拖放到 层次结构 中,将其作为 HologramCollection 的子项Drag and drop the EnergyHub object from the Project panel to the Hierarchy as a child of HologramCollection.
  • 选择 文件 > 将场景另存为 ...Select File > Save Scene As...
  • 将场景命名为 SharedHolograms ,然后单击 " 保存"。Name the scene SharedHolograms and click Save.
  • 按下 Unity 中的 " 播放 " 按钮,预览全息影像。Press the Play button in Unity to preview your holograms.
  • 按第二次 播放 以停止预览模式。Press Play a second time to stop preview mode.

将项目从 Unity 导出到 Visual StudioExport the project from Unity to Visual Studio

  • 在 Unity 中,选择 " 文件 > 生成设置"。In Unity select File > Build Settings.
  • 单击 " 添加打开的场景 " 添加场景。Click Add Open Scenes to add the scene.
  • 选择 "平台" 列表中的 "通用 Windows 平台",然后单击 "切换平台"。Select Universal Windows Platform in the Platform list and click Switch Platform.
  • SDK 设置为 通用 10Set SDK to Universal 10.
  • 目标设备 设置为 HoloLens ,将 " UWP" 生成类型 设置为 " D3D"。Set Target device to HoloLens and UWP Build Type to D3D.
  • 检查 Unity c # 项目Check Unity C# Projects.
  • 单击“生成”。Click Build.
  • 在出现的 "文件资源管理器" 窗口中,创建一个名为 "App" 的 新文件夹In the file explorer window that appears, create a New Folder named "App".
  • 单击 应用 文件夹。Single click the App folder.
  • 按 " 选择文件夹"。Press Select Folder.
  • 当 Unity 完成后,将显示文件资源管理器窗口。When Unity is done, a File Explorer window will appear.
  • 打开 应用程序 文件夹。Open the App folder.
  • 打开 SharedHolograms 以启动 Visual Studio。Open SharedHolograms.sln to launch Visual Studio.
  • 使用 Visual Studio 中的顶部工具栏,将目标从 "调试" 更改为 " 发布 ",将 "从 ARM" 更改为 " X86"。Using the top toolbar in Visual Studio, change the target from Debug to Release and from ARM to X86.
  • 单击 "本地计算机" 旁边的下拉箭头,然后选择 " 远程设备"。Click on the drop-down arrow next to Local Machine, and select Remote Device.
    • 地址 设置为 HoloLens 的名称或 IP 地址。Set the Address to the name or IP address of your HoloLens. 如果你不知道设备 IP 地址,请在 "设置" 中查找 " > 网络 & Internet > 高级选项 " 或 "我的 IP 地址是什么?"。If you do not know your device IP address, look in Settings > Network & Internet > Advanced Options or ask Cortana "Hey Cortana, What's my IP address?"
    • 身份验证模式 设置为 " 通用"。Leave the Authentication Mode set to Universal.
    • 单击 "选择"Click Select
  • 单击 " 调试" > "开始但不调试 " 或按 Ctrl + F5Click Debug > Start Without debugging or press Ctrl + F5. 如果这是首次部署到设备,则需要将 其与 Visual Studio 配对If this is the first time deploying to your device, you will need to pair it with Visual Studio.
  • 放在你的 HoloLens 上,找到 EnergyHub 全息图。Put on your HoloLens and find the EnergyHub hologram.

第2章-交互Chapter 2 - Interaction

在本章中,我们将与全息影像交互。In this chapter, we'll interact with our holograms. 首先,我们将添加一个光标以直观显示 注视First, we'll add a cursor to visualize our Gaze. 接下来,我们将添加 手势 ,并使用我们的手在空间中放置全息影像。Then, we'll add Gestures and use our hand to place our holograms in space.

目标Objectives

  • 使用 "注视输入" 控制光标。Use gaze input to control a cursor.
  • 使用手势输入与全息影像交互。Use gesture input to interact with holograms.

InstructionsInstructions

凝视Gaze

  • 在 " 层次结构" 面板 中,选择 " HologramCollection " 对象。In the Hierarchy panel select the HologramCollection object.
  • 检查器面板 中,单击 " 添加组件 " 按钮。In the Inspector panel click the Add Component button.
  • 在菜单中,在 "搜索" 框中键入 " 注视经理"。In the menu, type in the search box Gaze Manager. 选择搜索结果。Select the search result.
  • HoloToolkit-Sharing-240\Prefabs\Input 文件夹中,找到 光标 资产。In the HoloToolkit-Sharing-240\Prefabs\Input folder, find the Cursor asset.
  • 光标 资产拖放到 层次结构 中。Drag and drop the Cursor asset onto the Hierarchy.

手势Gesture

  • 在 " 层次结构" 面板 中,选择 " HologramCollection " 对象。In the Hierarchy panel select the HologramCollection object.
  • 单击 " 添加组件 ",并在搜索字段中键入 笔势管理器Click Add Component and type Gesture Manager in the search field. 选择搜索结果。Select the search result.
  • 在 " 层次结构" 面板 中,展开 " HologramCollection"。In the Hierarchy panel, expand HologramCollection.
  • 选择 "子 EnergyHub " 对象。Select the child EnergyHub object.
  • 检查器面板 中,单击 " 添加组件 " 按钮。In the Inspector panel click the Add Component button.
  • 在 "搜索 " 框中,键入 "搜索" 对话框。In the menu, type in the search box Hologram Placement. 选择搜索结果。Select the search result.
  • 通过选择 "文件" > 保存 场景来保存场景。Save the scene by selecting File > Save Scene.

部署和体验Deploy and enjoy

  • 使用上一章中的说明生成并部署到 HoloLens。Build and deploy to your HoloLens, using the instructions from the previous chapter.
  • 在您的 HoloLens 上启动该应用程序后,请四处移动您的头,注意 EnergyHub 如何进入您的注视。Once the app launches on your HoloLens, move your head around and notice how the EnergyHub follows your gaze.
  • 请注意,在您看出全息影像时光标如何显示,当不 gazing 在全息图上时,也会改变点光。Notice how the cursor appears when you gaze upon the hologram, and changes to a point light when not gazing at a hologram.
  • 执行轻按下以放置全息影像。Perform an air-tap to place the hologram. 目前,在我们的项目中,你只能在 (重新部署后放置全息影像,然后) 重试。At this time in our project, you can only place the hologram once (redeploy to try again).

第3章-共享坐标Chapter 3 - Shared Coordinates

与全息影像进行查看和交互很有趣,但让我们进一步了解一下。It's fun to see and interact with holograms, but let's go further. 我们将设置我们的第一个共享体验-每个人都可以同时查看。We'll set up our first shared experience - a hologram everyone can see together.

目标Objectives

  • 为共享体验设置网络。Setup a network for a shared experience.
  • 建立一个公共参考点。Establish a common reference point.
  • 跨设备共享坐标系统。Share coordinate systems across devices.
  • 所有人都看到同一个全息图!Everyone sees the same hologram!

备注

必须为应用声明 InternetClientServerPrivateNetworkClientServer 功能才能连接到共享服务器。The InternetClientServer and PrivateNetworkClientServer capabilities must be declared for an app to connect to the sharing server. 此操作已在全息影像240中完成,但请记住你自己的项目。This is done for you already in Holograms 240, but keep this in mind for your own projects.

  1. 在 Unity 编辑器中,导航到 "编辑 > 项目设置 > Player",转到 "播放机" 设置In the Unity Editor, go to the player settings by navigating to "Edit > Project Settings > Player"
  2. 单击 "Windows 应用商店" 选项卡Click on the "Windows Store" tab
  3. 在 "发布设置 > 功能" 部分中,查看 InternetClientServer 功能和 PrivateNetworkClientServer 功能In the "Publishing Settings > Capabilities" section, check the InternetClientServer capability and the PrivateNetworkClientServer capability

InstructionsInstructions

  • 在 " 项目" 面板 中,导航到 HoloToolkit-Sharing-240\Prefabs\Sharing 文件夹。In the Project panel navigate to the HoloToolkit-Sharing-240\Prefabs\Sharing folder.
  • 共享 prefab 拖放到 层次结构面板Drag and drop the Sharing prefab into the Hierarchy panel.

接下来,我们需要启动共享服务。Next we need to launch the sharing service. 共享体验中只有 一台 PC 需要执行此步骤。Only one PC in the shared experience needs to do this step.

  • 在 Unity 中-在顶部菜单中,选择 " HoloToolkit-240" 菜单In Unity - in the top-hand menu - select the HoloToolkit-Sharing-240 menu.
  • 在下拉栏中选择 " 启动共享服务 " 项。Select the Launch Sharing Service item in the drop-down.
  • 选中 " 专用网络 " 选项,并在出现防火墙提示时单击 " 允许访问 "。Check the Private Network option and click Allow Access when the firewall prompt appears.
  • 记下 "共享服务控制台" 窗口中显示的 IPv4 地址。Note down the IPv4 address displayed in the Sharing Service console window. 此 IP 与运行服务的计算机的 IP 相同。This is the same IP as the machine the service is being run on.

按照将加入共享体验的 所有 pc 上的其余说明进行操作。Follow the rest of the instructions on all PCs that will join the shared experience.

  • 层次结构 中,选择 共享 对象。In the Hierarchy, select the Sharing object.
  • 检查器共享阶段 组件上,将 服务器地址 从 "localhost" 更改为运行 SharingService.exe 的计算机的 IPv4 地址。In the Inspector, on the Sharing Stage component, change the Server Address from 'localhost' to the IPv4 address of the machine running SharingService.exe.
  • 层次结构 中,选择 " HologramCollection " 对象。In the Hierarchy select the HologramCollection object.
  • 检查器 中,单击 " 添加组件 " 按钮。In the Inspector click the Add Component button.
  • 在搜索框中,键入 " 导入导出定位点管理器"。In the search box, type Import Export Anchor Manager. 选择搜索结果。Select the search result.
  • 在 " 项目" 面板 中,导航到 Scripts 文件夹。In the Project panel navigate to the Scripts folder.
  • 双击 HologramPlacement 脚本,在 Visual Studio 中将其打开。Double-click the HologramPlacement script to open it in Visual Studio.
  • 将内容替换为以下代码。Replace the contents with the code below.
using UnityEngine;
using System.Collections.Generic;
using UnityEngine.Windows.Speech;
using Academy.HoloToolkit.Unity;
using Academy.HoloToolkit.Sharing;

public class HologramPlacement : Singleton<HologramPlacement>
{
    /// <summary>
    /// Tracks if we have been sent a transform for the anchor model.
    /// The anchor model is rendered relative to the actual anchor.
    /// </summary>
    public bool GotTransform { get; private set; }

    private bool animationPlayed = false;

    void Start()
    {
        // We care about getting updates for the anchor transform.
        CustomMessages.Instance.MessageHandlers[CustomMessages.TestMessageID.StageTransform] = this.OnStageTransform;

        // And when a new user join we will send the anchor transform we have.
        SharingSessionTracker.Instance.SessionJoined += Instance_SessionJoined;
    }

    /// <summary>
    /// When a new user joins we want to send them the relative transform for the anchor if we have it.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void Instance_SessionJoined(object sender, SharingSessionTracker.SessionJoinedEventArgs e)
    {
        if (GotTransform)
        {
            CustomMessages.Instance.SendStageTransform(transform.localPosition, transform.localRotation);
        }
    }

    void Update()
    {
        if (GotTransform)
        {
            if (ImportExportAnchorManager.Instance.AnchorEstablished &&
                animationPlayed == false)
            {
                // This triggers the animation sequence for the anchor model and 
                // puts the cool materials on the model.
                GetComponent<EnergyHubBase>().SendMessage("OnSelect");
                animationPlayed = true;
            }
        }
        else
        {
            transform.position = Vector3.Lerp(transform.position, ProposeTransformPosition(), 0.2f);
        }
    }

    Vector3 ProposeTransformPosition()
    {
        // Put the anchor 2m in front of the user.
        Vector3 retval = Camera.main.transform.position + Camera.main.transform.forward * 2;

        return retval;
    }

    public void OnSelect()
    {
        // Note that we have a transform.
        GotTransform = true;

        // And send it to our friends.
        CustomMessages.Instance.SendStageTransform(transform.localPosition, transform.localRotation);
    }

    /// <summary>
    /// When a remote system has a transform for us, we'll get it here.
    /// </summary>
    /// <param name="msg"></param>
    void OnStageTransform(NetworkInMessage msg)
    {
        // We read the user ID but we don't use it here.
        msg.ReadInt64();

        transform.localPosition = CustomMessages.Instance.ReadVector3(msg);
        transform.localRotation = CustomMessages.Instance.ReadQuaternion(msg);

        // The first time, we'll want to send the message to the anchor to do its animation and
        // swap its materials.
        if (GotTransform == false)
        {
            GetComponent<EnergyHubBase>().SendMessage("OnSelect");
        }

        GotTransform = true;
    }

    public void ResetStage()
    {
        // We'll use this later.
    }
}
  • 返回 Unity,在 "层次结构" 面板 中选择 " HologramCollection "。Back in Unity, select the HologramCollection in the Hierarchy panel.
  • 检查器面板 中,单击 " 添加组件 " 按钮。In the Inspector panel click the Add Component button.
  • 在菜单中,在 "搜索" 框中键入 " 应用状态管理器"。In the menu, type in the search box App State Manager. 选择搜索结果。Select the search result.

部署和体验Deploy and enjoy

  • 生成适用于 HoloLens 设备的项目。Build the project for your HoloLens devices.
  • 指定要首先部署到的一个 HoloLens。Designate one HoloLens to deploy to first. 你将需要等待定位点上传到服务,然后才能放置 EnergyHub (这可能需要大约30-60 秒) 。You will need to wait for the Anchor to be uploaded to the service before you can place the EnergyHub (this can take ~30-60 seconds). 完成上传之前,将忽略攻丝手势。Until the upload is done, your tap gestures will be ignored.
  • 放置 EnergyHub 后,其位置将被上传到服务,然后你可以将其部署到所有其他 HoloLens 设备。After the EnergyHub has been placed, its location will be uploaded to the service and you can then deploy to all other HoloLens devices.
  • 新的 HoloLens 首次加入会话时,该设备上的 EnergyHub 位置可能不正确。When a new HoloLens first joins the session, the location of the EnergyHub may not be correct on that device. 但是,一旦从服务下载了定位点和 EnergyHub 位置,EnergyHub 应跳转到新的共享位置。However, as soon as the anchor and EnergyHub locations have been downloaded from the service, the EnergyHub should jump to the new, shared location. 如果这不是在约30-60 秒内发生的,则在设置定位点时转到原始 HoloLens 的位置,以收集更多的环境线索。If this does not happen within ~30-60 seconds, walk to where the original HoloLens was when setting the anchor to gather more environment clues. 如果位置仍未锁定,请重新部署到设备。If the location still does not lock on, redeploy to the device.
  • 当设备全部准备就绪并运行应用程序时,请查找 EnergyHub。When the devices are all ready and running the app, look for the EnergyHub. 你是否完全同意了全息图的位置以及文本的方向:Can you all agree on the hologram's location and which direction the text is facing?

第4章-发现Chapter 4 - Discovery

现在,所有人都可以看到相同的全息图!Everyone can now see the same hologram! 现在,让我们看看与共享全息环境连接的其他人。Now let's see everyone else connected to our shared holographic world. 在本章中,我们将抓住同一共享会话中所有其他 HoloLens 设备的打印头位置和旋转。In this chapter, we'll grab the head location and rotation of all other HoloLens devices in the same sharing session.

目标Objectives

  • 了解共享体验。Discover each other in our shared experience.
  • 选择并共享播放机头像。Choose and share a player avatar.
  • 将播放机头像附加到每个人的标题旁边。Attach the player avatar next to everyone's heads.

InstructionsInstructions

  • 在 " 项目" 面板 中,导航到 " 全息影像 " 文件夹。In the Project panel navigate to the Holograms folder.
  • PlayerAvatarStore 拖放到 层次结构 中。Drag and drop the PlayerAvatarStore into the Hierarchy.
  • 在 " 项目" 面板 中,导航到 Scripts 文件夹。In the Project panel navigate to the Scripts folder.
  • 双击 AvatarSelector 脚本,在 Visual Studio 中将其打开。Double-click the AvatarSelector script to open it in Visual Studio.
  • 将内容替换为以下代码。Replace the contents with the code below.
using UnityEngine;
using Academy.HoloToolkit.Unity;

/// <summary>
/// Script to handle the user selecting the avatar.
/// </summary>
public class AvatarSelector : MonoBehaviour
{
    /// <summary>
    /// This is the index set by the PlayerAvatarStore for the avatar.
    /// </summary>
    public int AvatarIndex { get; set; }

    /// <summary>
    /// Called when the user is gazing at this avatar and air-taps it.
    /// This sends the user's selection to the rest of the devices in the experience.
    /// </summary>
    void OnSelect()
    {
        PlayerAvatarStore.Instance.DismissAvatarPicker();

        LocalPlayerManager.Instance.SetUserAvatar(AvatarIndex);
    }

    void Start()
    {
        // Add Billboard component so the avatar always faces the user.
        Billboard billboard = gameObject.GetComponent<Billboard>();
        if (billboard == null)
        {
            billboard = gameObject.AddComponent<Billboard>();
        }

        // Lock rotation along the Y axis.
        billboard.PivotAxis = PivotAxis.Y;
    }
}
  • 层次结构 中,选择 " HologramCollection " 对象。In the Hierarchy select the HologramCollection object.
  • 检查器 中单击 " 添加组件"。In the Inspector click Add Component.
  • 在搜索框中,键入 " 本地播放机管理器"。In the search box, type Local Player Manager. 选择搜索结果。Select the search result.
  • 层次结构 中,选择 " HologramCollection " 对象。In the Hierarchy select the HologramCollection object.
  • 检查器 中单击 " 添加组件"。In the Inspector click Add Component.
  • 在搜索框中,键入 " 远程播放机管理器"。In the search box, type Remote Player Manager. 选择搜索结果。Select the search result.
  • 在 Visual Studio 中打开 HologramPlacement 脚本。Open the HologramPlacement script in Visual Studio.
  • 将内容替换为以下代码。Replace the contents with the code below.
using UnityEngine;
using System.Collections.Generic;
using UnityEngine.Windows.Speech;
using Academy.HoloToolkit.Unity;
using Academy.HoloToolkit.Sharing;

public class HologramPlacement : Singleton<HologramPlacement>
{
    /// <summary>
    /// Tracks if we have been sent a transform for the model.
    /// The model is rendered relative to the actual anchor.
    /// </summary>
    public bool GotTransform { get; private set; }

    /// <summary>
    /// When the experience starts, we disable all of the rendering of the model.
    /// </summary>
    List<MeshRenderer> disabledRenderers = new List<MeshRenderer>();

    void Start()
    {
        // When we first start, we need to disable the model to avoid it obstructing the user picking a hat.
        DisableModel();

        // We care about getting updates for the model transform.
        CustomMessages.Instance.MessageHandlers[CustomMessages.TestMessageID.StageTransform] = this.OnStageTransform;

        // And when a new user join we will send the model transform we have.
        SharingSessionTracker.Instance.SessionJoined += Instance_SessionJoined;
    }

    /// <summary>
    /// When a new user joins we want to send them the relative transform for the model if we have it.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void Instance_SessionJoined(object sender, SharingSessionTracker.SessionJoinedEventArgs e)
    {
        if (GotTransform)
        {
            CustomMessages.Instance.SendStageTransform(transform.localPosition, transform.localRotation);
        }
    }

    /// <summary>
    /// Turns off all renderers for the model.
    /// </summary>
    void DisableModel()
    {
        foreach (MeshRenderer renderer in gameObject.GetComponentsInChildren<MeshRenderer>())
        {
            if (renderer.enabled)
            {
                renderer.enabled = false;
                disabledRenderers.Add(renderer);
            }
        }

        foreach (MeshCollider collider in gameObject.GetComponentsInChildren<MeshCollider>())
        {
            collider.enabled = false;
        }
    }

    /// <summary>
    /// Turns on all renderers that were disabled.
    /// </summary>
    void EnableModel()
    {
        foreach (MeshRenderer renderer in disabledRenderers)
        {
            renderer.enabled = true;
        }

        foreach (MeshCollider collider in gameObject.GetComponentsInChildren<MeshCollider>())
        {
            collider.enabled = true;
        }

        disabledRenderers.Clear();
    }


    void Update()
    {
        // Wait till users pick an avatar to enable renderers.
        if (disabledRenderers.Count > 0)
        {
            if (!PlayerAvatarStore.Instance.PickerActive &&
            ImportExportAnchorManager.Instance.AnchorEstablished)
            {
                // After which we want to start rendering.
                EnableModel();

                // And if we've already been sent the relative transform, we will use it.
                if (GotTransform)
                {
                    // This triggers the animation sequence for the model and
                    // puts the cool materials on the model.
                    GetComponent<EnergyHubBase>().SendMessage("OnSelect");
                }
            }
        }
        else if (GotTransform == false)
        {
            transform.position = Vector3.Lerp(transform.position, ProposeTransformPosition(), 0.2f);
        }
    }

    Vector3 ProposeTransformPosition()
    {
        // Put the model 2m in front of the user.
        Vector3 retval = Camera.main.transform.position + Camera.main.transform.forward * 2;

        return retval;
    }

    public void OnSelect()
    {
        // Note that we have a transform.
        GotTransform = true;

        // And send it to our friends.
        CustomMessages.Instance.SendStageTransform(transform.localPosition, transform.localRotation);
    }

    /// <summary>
    /// When a remote system has a transform for us, we'll get it here.
    /// </summary>
    /// <param name="msg"></param>
    void OnStageTransform(NetworkInMessage msg)
    {
        // We read the user ID but we don't use it here.
        msg.ReadInt64();

        transform.localPosition = CustomMessages.Instance.ReadVector3(msg);
        transform.localRotation = CustomMessages.Instance.ReadQuaternion(msg);

        // The first time, we'll want to send the message to the model to do its animation and
        // swap its materials.
        if (disabledRenderers.Count == 0 && GotTransform == false)
        {
            GetComponent<EnergyHubBase>().SendMessage("OnSelect");
        }

        GotTransform = true;
    }

    public void ResetStage()
    {
        // We'll use this later.
    }
}
  • 在 Visual Studio 中打开 AppStateManager 脚本。Open the AppStateManager script in Visual Studio.
  • 将内容替换为以下代码。Replace the contents with the code below.
using UnityEngine;
using Academy.HoloToolkit.Unity;

/// <summary>
/// Keeps track of the current state of the experience.
/// </summary>
public class AppStateManager : Singleton<AppStateManager>
{
    /// <summary>
    /// Enum to track progress through the experience.
    /// </summary>
    public enum AppState
    {
        Starting = 0,
        WaitingForAnchor,
        WaitingForStageTransform,
        PickingAvatar,
        Ready
    }

    /// <summary>
    /// Tracks the current state in the experience.
    /// </summary>
    public AppState CurrentAppState { get; set; }

    void Start()
    {
        // We start in the 'picking avatar' mode.
        CurrentAppState = AppState.PickingAvatar;

        // We start by showing the avatar picker.
        PlayerAvatarStore.Instance.SpawnAvatarPicker();
    }

    void Update()
    {
        switch (CurrentAppState)
        {
            case AppState.PickingAvatar:
                // Avatar picking is done when the avatar picker has been dismissed.
                if (PlayerAvatarStore.Instance.PickerActive == false)
                {
                    CurrentAppState = AppState.WaitingForAnchor;
                }
                break;
            case AppState.WaitingForAnchor:
                if (ImportExportAnchorManager.Instance.AnchorEstablished)
                {
                    CurrentAppState = AppState.WaitingForStageTransform;
                    GestureManager.Instance.OverrideFocusedObject = HologramPlacement.Instance.gameObject;
                }
                break;
            case AppState.WaitingForStageTransform:
                // Now if we have the stage transform we are ready to go.
                if (HologramPlacement.Instance.GotTransform)
                {
                    CurrentAppState = AppState.Ready;
                    GestureManager.Instance.OverrideFocusedObject = null;
                }
                break;
        }
    }
}

部署和体验Deploy and Enjoy

  • 生成项目并将其部署到 HoloLens 设备。Build and deploy the project to your HoloLens devices.
  • 听到 ping 声音时,请找到头像选择菜单,并选择一个具有 "轻攻器" 手势的头像。When you hear a pinging sound, find the avatar selection menu and select an avatar with the air-tap gesture.
  • 如果你不想查看任何全息影像,则当你的 HoloLens 与服务进行通信时,光标周围的点亮将变为不同的颜色:初始化 (深紫色) ,下载 (绿色) 的定位点,导入/导出位置数据 (黄色) ,将定位点上传 (蓝色) 。If you're not looking at any holograms, the point light around your cursor will turn a different color when your HoloLens is communicating with the service: initializing (dark purple), downloading the anchor (green), importing/exporting location data (yellow), uploading the anchor (blue). 如果光标周围的点亮是 (浅紫色) 的默认颜色,则已准备好与会话中的其他玩家交互!If your point light around your cursor is the default color (light purple), then you are ready to interact with other players in your session!
  • 查看与你的空间连接的其他人员-将有一个全息机器人浮在其肩上并模拟其头运动!Look at other people connected to your space - there will be a holographic robot floating above their shoulder and mimicking their head motions!

第5章-位置Chapter 5 - Placement

在本章中,我们将定位标记,使其能够放置在实际的表面上。In this chapter, we'll make the anchor able to be placed on real-world surfaces. 我们将使用共享坐标将该定位点置于连接到共享体验的每个用户之间的中间点。We'll use shared coordinates to place that anchor in the middle point between everyone connected to the shared experience.

目标Objectives

  • 根据玩家的头位置,将全息影像置于空间映射网格上。Place holograms on the spatial mapping mesh based on players’ head position.

InstructionsInstructions

  • 在 " 项目" 面板 中,导航到 " 全息影像 " 文件夹。In the Project panel navigate to the Holograms folder.
  • CustomSpatialMapping prefab 拖放到 层次结构 中。Drag and drop the CustomSpatialMapping prefab onto the Hierarchy.
  • 在 " 项目" 面板 中,导航到 Scripts 文件夹。In the Project panel navigate to the Scripts folder.
  • 双击 AppStateManager 脚本,在 Visual Studio 中将其打开。Double-click the AppStateManager script to open it in Visual Studio.
  • 将内容替换为以下代码。Replace the contents with the code below.
using UnityEngine;
using Academy.HoloToolkit.Unity;

/// <summary>
/// Keeps track of the current state of the experience.
/// </summary>
public class AppStateManager : Singleton<AppStateManager>
{
    /// <summary>
    /// Enum to track progress through the experience.
    /// </summary>
    public enum AppState
    {
        Starting = 0,
        PickingAvatar,
        WaitingForAnchor,
        WaitingForStageTransform,
        Ready
    }

    // The object to call to make a projectile.
    GameObject shootHandler = null;

    /// <summary>
    /// Tracks the current state in the experience.
    /// </summary>
    public AppState CurrentAppState { get; set; }

    void Start()
    {
        // The shootHandler shoots projectiles.
        if (GetComponent<ProjectileLauncher>() != null)
        {
            shootHandler = GetComponent<ProjectileLauncher>().gameObject;
        }

        // We start in the 'picking avatar' mode.
        CurrentAppState = AppState.PickingAvatar;

        // Spatial mapping should be disabled when we start up so as not
        // to distract from the avatar picking.
        SpatialMappingManager.Instance.StopObserver();
        SpatialMappingManager.Instance.gameObject.SetActive(false);

        // On device we start by showing the avatar picker.
        PlayerAvatarStore.Instance.SpawnAvatarPicker();
    }

    public void ResetStage()
    {
        // If we fall back to waiting for anchor, everything needed to
        // get us into setting the target transform state will be setup.
        if (CurrentAppState != AppState.PickingAvatar)
        {
            CurrentAppState = AppState.WaitingForAnchor;
        }

        // Reset the underworld.
        if (UnderworldBase.Instance)
        {
            UnderworldBase.Instance.ResetUnderworld();
        }
    }

    void Update()
    {
        switch (CurrentAppState)
        {
            case AppState.PickingAvatar:
                // Avatar picking is done when the avatar picker has been dismissed.
                if (PlayerAvatarStore.Instance.PickerActive == false)
                {
                    CurrentAppState = AppState.WaitingForAnchor;
                }
                break;
            case AppState.WaitingForAnchor:
                // Once the anchor is established we need to run spatial mapping for a
                // little while to build up some meshes.
                if (ImportExportAnchorManager.Instance.AnchorEstablished)
                {
                    CurrentAppState = AppState.WaitingForStageTransform;
                    GestureManager.Instance.OverrideFocusedObject = HologramPlacement.Instance.gameObject;

                    SpatialMappingManager.Instance.gameObject.SetActive(true);
                    SpatialMappingManager.Instance.DrawVisualMeshes = true;
                    SpatialMappingDeformation.Instance.ResetGlobalRendering();
                    SpatialMappingManager.Instance.StartObserver();
                }
                break;
            case AppState.WaitingForStageTransform:
                // Now if we have the stage transform we are ready to go.
                if (HologramPlacement.Instance.GotTransform)
                {
                    CurrentAppState = AppState.Ready;
                    GestureManager.Instance.OverrideFocusedObject = shootHandler;
                }
                break;
        }
    }
}
  • 在 " 项目" 面板 中,导航到 Scripts 文件夹。In the Project panel navigate to the Scripts folder.
  • 双击 HologramPlacement 脚本,在 Visual Studio 中将其打开。Double-click the HologramPlacement script to open it in Visual Studio.
  • 将内容替换为以下代码。Replace the contents with the code below.
using UnityEngine;
using System.Collections.Generic;
using UnityEngine.Windows.Speech;
using Academy.HoloToolkit.Unity;
using Academy.HoloToolkit.Sharing;

public class HologramPlacement : Singleton<HologramPlacement>
{
    /// <summary>
    /// Tracks if we have been sent a transform for the model.
    /// The model is rendered relative to the actual anchor.
    /// </summary>
    public bool GotTransform { get; private set; }

    /// <summary>
    /// When the experience starts, we disable all of the rendering of the model.
    /// </summary>
    List<MeshRenderer> disabledRenderers = new List<MeshRenderer>();

    /// <summary>
    /// We use a voice command to enable moving the target.
    /// </summary>
    KeywordRecognizer keywordRecognizer;

    void Start()
    {
        // When we first start, we need to disable the model to avoid it obstructing the user picking a hat.
        DisableModel();

        // We care about getting updates for the model transform.
        CustomMessages.Instance.MessageHandlers[CustomMessages.TestMessageID.StageTransform] = this.OnStageTransform;

        // And when a new user join we will send the model transform we have.
        SharingSessionTracker.Instance.SessionJoined += Instance_SessionJoined;

        // And if the users want to reset the stage transform.
        CustomMessages.Instance.MessageHandlers[CustomMessages.TestMessageID.ResetStage] = this.OnResetStage;

        // Setup a keyword recognizer to enable resetting the target location.
        List<string> keywords = new List<string>();
        keywords.Add("Reset Target");
        keywordRecognizer = new KeywordRecognizer(keywords.ToArray());
        keywordRecognizer.OnPhraseRecognized += KeywordRecognizer_OnPhraseRecognized;
        keywordRecognizer.Start();
    }

    /// <summary>
    /// When the keyword recognizer hears a command this will be called.  
    /// In this case we only have one keyword, which will re-enable moving the
    /// target.
    /// </summary>
    /// <param name="args">information to help route the voice command.</param>
    private void KeywordRecognizer_OnPhraseRecognized(PhraseRecognizedEventArgs args)
    {
        ResetStage();
    }

    /// <summary>
    /// Resets the stage transform, so users can place the target again.
    /// </summary>
    public void ResetStage()
    {
        GotTransform = false;

        // AppStateManager needs to know about this so that
        // the right objects get input routed to them.
        AppStateManager.Instance.ResetStage();

        // Other devices in the experience need to know about this as well.
        CustomMessages.Instance.SendResetStage();

        // And we need to reset the object to its start animation state.
        GetComponent<EnergyHubBase>().ResetAnimation();
    }

    /// <summary>
    /// When a new user joins we want to send them the relative transform for the model if we have it.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void Instance_SessionJoined(object sender, SharingSessionTracker.SessionJoinedEventArgs e)
    {
        if (GotTransform)
        {
            CustomMessages.Instance.SendStageTransform(transform.localPosition, transform.localRotation);
        }
    }

    /// <summary>
    /// Turns off all renderers for the model.
    /// </summary>
    void DisableModel()
    {
        foreach (MeshRenderer renderer in gameObject.GetComponentsInChildren<MeshRenderer>())
        {
            if (renderer.enabled)
            {
                renderer.enabled = false;
                disabledRenderers.Add(renderer);
            }
        }

        foreach (MeshCollider collider in gameObject.GetComponentsInChildren<MeshCollider>())
        {
            collider.enabled = false;
        }
    }

    /// <summary>
    /// Turns on all renderers that were disabled.
    /// </summary>
    void EnableModel()
    {
        foreach (MeshRenderer renderer in disabledRenderers)
        {
            renderer.enabled = true;
        }

        foreach (MeshCollider collider in gameObject.GetComponentsInChildren<MeshCollider>())
        {
            collider.enabled = true;
        }

        disabledRenderers.Clear();
    }


    void Update()
    {
        // Wait till users pick an avatar to enable renderers.
        if (disabledRenderers.Count > 0)
        {
            if (!PlayerAvatarStore.Instance.PickerActive &&
            ImportExportAnchorManager.Instance.AnchorEstablished)
            {
                // After which we want to start rendering.
                EnableModel();

                // And if we've already been sent the relative transform, we will use it.
                if (GotTransform)
                {
                    // This triggers the animation sequence for the model and
                    // puts the cool materials on the model.
                    GetComponent<EnergyHubBase>().SendMessage("OnSelect");
                }
            }
        }
        else if (GotTransform == false)
        {
            transform.position = Vector3.Lerp(transform.position, ProposeTransformPosition(), 0.2f);
        }
    }

    Vector3 ProposeTransformPosition()
    {
        Vector3 retval;
        // We need to know how many users are in the experience with good transforms.
        Vector3 cumulatedPosition = Camera.main.transform.position;
        int playerCount = 1;
        foreach (RemotePlayerManager.RemoteHeadInfo remoteHead in RemotePlayerManager.Instance.remoteHeadInfos)
        {
            if (remoteHead.Anchored && remoteHead.Active)
            {
                playerCount++;
                cumulatedPosition += remoteHead.HeadObject.transform.position;
            }
        }

        // If we have more than one player ...
        if (playerCount > 1)
        {
            // Put the transform in between the players.
            retval = cumulatedPosition / playerCount;
            RaycastHit hitInfo;

            // And try to put the transform on a surface below the midpoint of the players.
            if (Physics.Raycast(retval, Vector3.down, out hitInfo, 5, SpatialMappingManager.Instance.LayerMask))
            {
                retval = hitInfo.point;
            }
        }
        // If we are the only player, have the model act as the 'cursor' ...
        else
        {
            // We prefer to put the model on a real world surface.
            RaycastHit hitInfo;

            if (Physics.Raycast(Camera.main.transform.position, Camera.main.transform.forward, out hitInfo, 30, SpatialMappingManager.Instance.LayerMask))
            {
                retval = hitInfo.point;
            }
            else
            {
                // But if we don't have a ray that intersects the real world, just put the model 2m in
                // front of the user.
                retval = Camera.main.transform.position + Camera.main.transform.forward * 2;
            }
        }
        return retval;
    }

    public void OnSelect()
    {
        // Note that we have a transform.
        GotTransform = true;

        // And send it to our friends.
        CustomMessages.Instance.SendStageTransform(transform.localPosition, transform.localRotation);
    }

    /// <summary>
    /// When a remote system has a transform for us, we'll get it here.
    /// </summary>
    /// <param name="msg"></param>
    void OnStageTransform(NetworkInMessage msg)
    {
        // We read the user ID but we don't use it here.
        msg.ReadInt64();

        transform.localPosition = CustomMessages.Instance.ReadVector3(msg);
        transform.localRotation = CustomMessages.Instance.ReadQuaternion(msg);

        // The first time, we'll want to send the message to the model to do its animation and
        // swap its materials.
        if (disabledRenderers.Count == 0 && GotTransform == false)
        {
            GetComponent<EnergyHubBase>().SendMessage("OnSelect");
        }

        GotTransform = true;
    }

    /// <summary>
    /// When a remote system has a transform for us, we'll get it here.
    /// </summary>
    void OnResetStage(NetworkInMessage msg)
    {
        GotTransform = false;

        GetComponent<EnergyHubBase>().ResetAnimation();
        AppStateManager.Instance.ResetStage();
    }
}

部署和体验Deploy and enjoy

  • 生成项目并将其部署到 HoloLens 设备。Build and deploy the project to your HoloLens devices.
  • 当应用程序准备就绪时,请将其放在一个圆圈上,并注意 EnergyHub 如何出现在每个人的中心。When the app is ready, stand in a circle and notice how the EnergyHub appears in the center of everyone.
  • 点击以放置 EnergyHub。Tap to place the EnergyHub.
  • 尝试使用语音命令 "重置目标" 来选择 EnergyHub 备份,并以组的形式协同工作,以便将全息图移动到新位置。Try the voice command 'Reset Target' to pick the EnergyHub back up and work together as a group to move the hologram to a new location.

第6章-Real-World 物理学Chapter 6 - Real-World Physics

在本章中,我们将添加从实际表面弹跳的全息影像。In this chapter we'll add holograms that bounce off real-world surfaces. 观看您和您的朋友所启动的项目的空间。Watch your space fill up with projects launched by both you and your friends!

目标Objectives

  • 启动炮弹,从实际表面弹跳。Launch projectiles that bounce off real-world surfaces.
  • 共享炮弹,使其他玩家可以看到它们。Share the projectiles so other players can see them.

InstructionsInstructions

  • 层次结构 中,选择 " HologramCollection " 对象。In the Hierarchy select the HologramCollection object.
  • 检查器 中单击 " 添加组件"。In the Inspector click Add Component.
  • 在搜索框中,键入 " Projectile 启动 程序"。In the search box, type Projectile Launcher. 选择搜索结果。Select the search result.

部署和体验Deploy and enjoy

  • 生成并部署到 HoloLens 设备。Build and deploy to your HoloLens devices.
  • 当应用程序在所有设备上运行时,请在实际的表面上执行轻点击以启动 projectile。When the app is running on all devices, perform an air-tap to launch projectile at real world surfaces.
  • 了解 projectile 与其他玩家的头像发生冲突时会发生什么情况!See what happens when your projectile collides with another player's avatar!

第7章-总计 FinaleChapter 7 - Grand Finale

在本章中,我们将发现一个只能通过协作发现的门户。In this chapter, we'll uncover a portal that can only be discovered with collaboration.

目标Objectives

  • 协同工作,在定位点上启动足够的炮弹来发现机密门户!Work together to launch enough projectiles at the anchor to uncover a secret portal!

InstructionsInstructions

  • 在 " 项目" 面板 中,导航到 " 全息影像 " 文件夹。In the Project panel navigate to the Holograms folder.
  • Underworld 资产拖放为 HologramCollection 的 子项Drag and drop the Underworld asset as a child of HologramCollection.
  • 选择 HologramCollection 后,在 检查器 中单击 "添加组件" 按钮。With HologramCollection selected, click the Add Component button in the Inspector.
  • 在菜单中,在 "搜索" 框中键入 ExplodeTargetIn the menu, type in the search box ExplodeTarget. 选择搜索结果。Select the search result.
  • 选择 HologramCollection 后,从 层次结构 中,将 EnergyHub 对象拖到 检查器 中的 目标 字段。With HologramCollection selected, from the Hierarchy drag the EnergyHub object to the Target field in the Inspector.
  • 选择 HologramCollection 后,从 层次结构 中,将 Underworld 对象拖到 检查器 中的 Underworld 字段。With HologramCollection selected, from the Hierarchy drag the Underworld object to the Underworld field in the Inspector.

部署和体验Deploy and enjoy

  • 生成并部署到 HoloLens 设备。Build and deploy to your HoloLens devices.
  • 当应用程序启动时,在 EnergyHub 上协作以启动炮弹。When the app has launched, collaborate together to launch projectiles at the EnergyHub.
  • 显示 underworld 时,请在 underworld 机器人上启动炮弹 (按下机器人三次,以) 额外的乐趣。When the underworld appears, launch projectiles at underworld robots (hit a robot three times for extra fun).