Hololens官方教程精简版 - 08. Sharing holograms(共享全息影像)

前言

注意:本文已更新到5.5.1f1版本号

本篇集中学习全息影像“共享”的功能,以实如今同一房间的人,看到“同一个物体”。之所以打引號,是由于。每一个人看到的并不是同一个物体,仅仅是空间位置等信息相同的同类物体而已。

要想实现这个效果,有以下几点须要注意:

  • 需开启设备的Spatial Perception功能(在Player Settings…面板的Publishing Settings > Capabilities中勾选)
  • 需开启设备的网络功能
  • 临时仅仅能两台以上真机測试,无法在Unity中測试(即便是Remoting连接Hololens也不行)
  • 设备在同一房间内(废话)

友情提醒:本章需在多台设备间折腾,把设备休眠时间设置得长一点。会方便非常多。详细方法例如以下:

设备打开。浏览器訪问设备IP,进入:Hololens Device PortalHome菜单下有个Sleep settings。最长设置30分钟。

要实现共享全息影像的效果,主要掌握以下技术点:

  • 使用Socket协议传递数据
  • 理解世界坐标系及空间锚点的使用(WorldAnchor及WorldAnchorStore)
  • Sharing组件的使用(锚点的上传和下载)

Chapter 1 - Unity Setup

  1. 请依照第一篇的教程,完毕项目的创建。
  2. 新建目录:”Assets/_Scenes/Holograms 240/”
  3. 新建场景:”Assets/_Scenes/Holograms 240/Holograms 240.unity
  4. 打开场景,删除默认的Main Camera
  5. 将”Assets/HoloToolkit/Input/Prefabs/HololensCamera.prefab”增加到Hierarchy根级
  6. 将”Assets/HoloToolkit/Input/Prefabs/InputManager.prefab”增加到Hierarchy根级
  7. 将”Assets/HoloToolkit/Input/Prefabs/Cursor/DefaultCursor.prefab”增加到Hierarchy根级
  8. Hierarchy面板根级,增加一个Cube,设置例如以下:

    Hololens官方教程精简版 - 08. Sharing holograms(共享全息影像)

本节完毕!

Chapter 2 - 使用Socket协议传递数据

目标

使用HoloToolkit提供的Socket套件进行数据传输

实践

搭建Socket服务基础环境

首先要说明的是:HoloToolkit提供的Socket套件,使用的是RakNet。对其原理感兴趣的同学。能够去官网查看。

  1. 在下载的HoloToolkit-Unity开发包中。找到:”External\”目录,将其拷贝到项目目录下(与Assets目录同级目录)。

    如图:

    Hololens官方教程精简版 - 08. Sharing holograms(共享全息影像)

  2. 点击Unity主菜单下的:HoloToolkit > Sharing Service > Launch Sharing Service,如图:

    Hololens官方教程精简版 - 08. Sharing holograms(共享全息影像)
  3. 此时将会打开一个Socket服务端。如图所看到的。记录下IP。比如本例为:192.168.0.108

    Hololens官方教程精简版 - 08. Sharing holograms(共享全息影像)
  4. Project面板中,找到:”Assets/HoloToolkit/Sharing/Prefabs/Sharing.prefab”,拖动到Hierarchy根级,并在其Inspector面板中找到Server Address属性,填写上面一步得到的IP地址。如图:

    Hololens官方教程精简版 - 08. Sharing holograms(共享全息影像)

    watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQveXVyaWNob3U=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="增加Sharing组件" title="">

    此步相当于为APP增加了一个Socketclient。

以上步骤完毕后,能够点击Playbutton,并观察Socket服务端界面,看是否有设备增加到服务器。

如图:

Hololens官方教程精简版 - 08. Sharing holograms(共享全息影像)


创建Socket消息传输类

上一步中,我们利用HoloToolkit提供的Socket套件。搭建了基础数据传输环境(包括一个Socket服务端程序和一个Socketclient连接组件)。以下用一个移动Cube的样例来学习怎样同步数据。

  1. 新建目录:”Assets/_Scenes/Holograms 240/Scripts/
  2. 新建脚本:”Assets/_Scenes/Holograms 240/Scripts/Cube240.cs”。附加给Cube,编写脚本例如以下:

    (代码适用:5.5.0f3版本号)

    using HoloToolkit.Unity.InputModule;
    using UnityEngine; public class Cube240 : MonoBehaviour, IInputClickHandler { // 是否正在移动
    bool isMoving = false; // 单击Cube,切换是否移动
    public void OnInputClicked(InputEventData eventData)
    {
    isMoving = !isMoving;
    } // 假设Cube为移动状态,让其放置在镜头前2米位置
    void Update () {
    if (isMoving)
    {
    transform.position = Camera.main.transform.position + Camera.main.transform.forward * 2f;
    }
    }
    }

    (代码适用:5.5.1f1版本号)

    using HoloToolkit.Unity.InputModule;
    using UnityEngine; public class Cube240 : MonoBehaviour, IInputClickHandler
    {
    // 是否正在移动
    bool isMoving = false; // 单击Cube。切换是否移动
    public void OnInputClicked(InputClickedEventData eventData)
    {
    isMoving = !isMoving;
    } // 假设Cube为移动状态。让其放置在镜头前2米位置
    void Update()
    {
    if (isMoving)
    {
    transform.position = Camera.main.transform.position + Camera.main.transform.forward * 2f;
    }
    }
    }

    脚本实现了Cube的移动和放置。能够測试一下效果。

  3. 以下,我们来实现两台设备传递Cube的位置。
  4. Hierarchy面板。创建根级空对象,命名为:”Controller
  5. 建立一个消息传递类。

    新建脚本:”Assets/_Scenes/Holograms 240/Scripts/CustomMessages240.cs,附加给Controller,编辑内容例如以下:

    using HoloToolkit.Sharing;
    using HoloToolkit.Unity;
    using System.Collections.Generic;
    using UnityEngine; public class CustomMessages240 : Singleton<CustomMessages240>
    {
    // 代表当前的Socket连接
    NetworkConnection serverConnection; // 当前连接的事件监听器,这是一个典型的适配器模式,继承自NetworkConnectionListener
    NetworkConnectionAdapter connectionAdapter; // 自己定义消息类型
    public enum CustomMessageID : byte
    {
    // 自己的消息从MessageID.UserMessageIDStart開始编号,避免与MessageID内置消息编号冲突
    // Cube位置消息
    CubePosition = MessageID.UserMessageIDStart,
    Max
    } // 消息处理代理
    public delegate void MessageCallback(NetworkInMessage msg); // 消息处理字典
    public Dictionary<CustomMessageID, MessageCallback> MessageHandlers { get; private set; } // 当前用户在Sorket服务器中的唯一编号(自己主动生成)
    public long LocalUserID { get; private set; } protected override void Awake()
    {
    base.Awake();
    // 初始化消息处理字典
    MessageHandlers = new Dictionary<CustomMessageID, MessageCallback>();
    for (byte index = (byte)MessageID.UserMessageIDStart; index < (byte)CustomMessageID.Max; index++)
    {
    if (!MessageHandlers.ContainsKey((CustomMessageID)index))
    {
    MessageHandlers.Add((CustomMessageID)index, null);
    }
    }
    } void Start () {
    // SharingStage是Sharing组件相应的脚本,内部是对经典的Socketclient的封装。 SharingStage.Instance.SharingManagerConnected += Instance_SharingManagerConnected;
    } private void Instance_SharingManagerConnected(object sender, System.EventArgs e)
    {
    // 初始化消息处理器
    InitializeMessageHandlers();
    } // 初始化消息处理器
    private void InitializeMessageHandlers()
    {
    SharingStage sharingStage = SharingStage.Instance; if (sharingStage == null)
    {
    return;
    } // 获取当前Socket连接
    serverConnection = sharingStage.Manager.GetServerConnection();
    if (serverConnection == null)
    {
    return;
    } // 初始化消息监听
    connectionAdapter = new NetworkConnectionAdapter();
    connectionAdapter.MessageReceivedCallback += ConnectionAdapter_MessageReceivedCallback; // 获取当前用户在Socket服务器中生成的唯一编号
    LocalUserID = sharingStage.Manager.GetLocalUser().GetID(); // 依据每一个自己定义消息。增加监听器
    for (byte index = (byte)MessageID.UserMessageIDStart; index < (byte)CustomMessageID.Max; index++)
    {
    serverConnection.AddListener(index, connectionAdapter);
    }
    } // 接收到服务器端消息的回调处理
    private void ConnectionAdapter_MessageReceivedCallback(NetworkConnection connection, NetworkInMessage msg)
    {
    byte messageType = msg.ReadByte();
    MessageCallback messageHandler = MessageHandlers[(CustomMessageID)messageType];
    if (messageHandler != null)
    {
    messageHandler(msg);
    }
    } protected override void OnDestroy()
    {
    if (serverConnection != null)
    {
    for (byte index = (byte)MessageID.UserMessageIDStart; index < (byte)CustomMessageID.Max; index++)
    {
    serverConnection.RemoveListener(index, connectionAdapter);
    }
    connectionAdapter.MessageReceivedCallback -= ConnectionAdapter_MessageReceivedCallback;
    }
    base.OnDestroy();
    } // 创建一个Out消息(client传递给服务端)
    // 消息格式第一个必须为消息类型,其后再增加自己的数据
    // 我们在全部的消息一開始增加消息发送的用户编号
    private NetworkOutMessage CreateMessage(byte messageType)
    {
    NetworkOutMessage msg = serverConnection.CreateMessage(messageType);
    msg.Write(messageType);
    msg.Write(LocalUserID);
    return msg;
    } // 将Cube位置广播给其它用户
    public void SendCubePosition(Vector3 position)
    {
    if (serverConnection != null && serverConnection.IsConnected())
    {
    // 将Cube的位置写入消息
    NetworkOutMessage msg = CreateMessage((byte)CustomMessageID.CubePosition); msg.Write(position.x);
    msg.Write(position.y);
    msg.Write(position.z); // 将消息广播给其它人
    serverConnection.Broadcast(msg,
    MessagePriority.Immediate, //马上发送
    MessageReliability.ReliableOrdered, //可靠排序数据包
    MessageChannel.Default); // 默认频道
    }
    } // 读取Cube的位置
    public static Vector3 ReadCubePosition(NetworkInMessage msg)
    {
    // 读取用户编号,但不使用
    msg.ReadInt64(); // 依次读取XYZ。这个和发送Cube时。写入參数顺序是一致的
    return new Vector3(msg.ReadFloat(), msg.ReadFloat(), msg.ReadFloat());
    }
    }
  6. 改动Cube240.cs,内容例如以下:

    (代码适用:5.5.0f3版本号)

    using HoloToolkit.Sharing;
    using HoloToolkit.Unity.InputModule;
    using UnityEngine; public class Cube240 : MonoBehaviour, IInputClickHandler { // 是否正在移动
    bool isMoving = false; // 消息传递类
    CustomMessages240 customMessage; private void Start()
    {
    customMessage = CustomMessages240.Instance; // 指定收到Cube位置消息后的处理方法
    customMessage.MessageHandlers[CustomMessages240.CustomMessageID.CubePosition] = OnCubePositionReceived;
    } private void OnCubePositionReceived(NetworkInMessage msg)
    {
    // 同步Cube位置
    if (!isMoving)
    {
    transform.position = CustomMessages240.ReadCubePosition(msg);
    }
    } // 单击Cube。切换是否移动
    public void OnInputClicked(InputEventData eventData)
    {
    isMoving = !isMoving;
    // 放置Cube后。发送Cube的位置消息给其它人
    if (!isMoving)
    {
    customMessage.SendCubePosition(transform.position);
    }
    } // 假设Cube为移动状态,让其放置在镜头前2米位置
    void Update () {
    if (isMoving)
    {
    transform.position = Camera.main.transform.position + Camera.main.transform.forward * 2f;
    }
    }
    }

    (代码适用:5.5.1f1版本号)

    using HoloToolkit.Sharing;
    using HoloToolkit.Unity.InputModule;
    using UnityEngine; public class Cube240 : MonoBehaviour, IInputClickHandler
    { // 是否正在移动
    bool isMoving = false; // 消息传递类
    CustomMessages240 customMessage; private void Start()
    {
    customMessage = CustomMessages240.Instance; // 指定收到Cube位置消息后的处理方法
    customMessage.MessageHandlers[CustomMessages240.CustomMessageID.CubePosition] = OnCubePositionReceived;
    } private void OnCubePositionReceived(NetworkInMessage msg)
    {
    // 同步Cube位置
    if (!isMoving)
    {
    transform.position = CustomMessages240.ReadCubePosition(msg);
    }
    } // 单击Cube,切换是否移动
    public void OnInputClicked(InputClickedEventData eventData)
    {
    isMoving = !isMoving;
    // 放置Cube后。发送Cube的位置消息给其它人
    if (!isMoving)
    {
    customMessage.SendCubePosition(transform.position);
    }
    } // 假设Cube为移动状态,让其放置在镜头前2米位置
    void Update()
    {
    if (isMoving)
    {
    transform.position = Camera.main.transform.position + Camera.main.transform.forward * 2f;
    }
    }
    }
  7. 公布到Hololens设备。启动。同一时候再点击UnityPlaybutton

当Hololens放置完Cube后。Play窗体中的Cube也会发生位置变化。反之亦然。


实时更新Cube的位置

我们仅仅需做少量改动,就能够实现实时传递Cube的位置。

  1. 找到文件”CustomMessages240.cs”的SendCubePosition方法(大概在124行的位置)。改动为:

    // 将Cube位置广播给其它用户
    public void SendCubePosition(Vector3 position, MessageReliability? reliability = MessageReliability.ReliableOrdered)
    {
    if (serverConnection != null && serverConnection.IsConnected())
    {
    // 将Cube的位置写入消息
    NetworkOutMessage msg = CreateMessage((byte)CustomMessageID.CubePosition); msg.Write(position.x);
    msg.Write(position.y);
    msg.Write(position.z); // 将消息广播给其它人
    serverConnection.Broadcast(msg,
    MessagePriority.Immediate, //马上发送
    reliability.Value, //可靠排序数据包
    MessageChannel.Default); // 默认频道
    }
    }
  2. 找到”Cube240.cs”文件的Update方法,改动为:

    // 假设Cube为移动状态。让其放置在镜头前2米位置
    void Update () {
    if (isMoving)
    {
    transform.position = Camera.main.transform.position + Camera.main.transform.forward * 2f;
    // 实时传递Cube位置
    customMessage.SendCubePosition(transform.position, MessageReliability.UnreliableSequenced);
    }
    }

再次測试,不论是移动还是放置Cube。两个client都能够实时看到Cube的位置变化。

大家注意到,在同步Cube实时移动时,使用了MessageReliability.UnreliableSequenced(不可靠序列数据包)。而在同步Cube放置时,使用了默认的MessageReliability.ReliableOrdered(可靠排序数据包)。是有原因的。

两种情况相应了两种不同场景,一种是高频的数据同步,第二种是低频的数据同步。

不同场景对消息的可靠性、消息传递序列也有不同的要求。详细请看以下《关于消息传递方式》的说明。

说明

  • 关于消息结构

    这里要注意的是,组装消息时所使用的数据结构和解析消息时所使用的数据结构须要保持一致。

    比方。本例中,组装Cube消息后的数据结构例如以下:

    1. 消息类型,在CreateMessage(byte messageType)方法中的msg.Write(messageType);
    2. 用户编号,在CreateMessage(byte messageType)方法中的msg.Write(LocalUserID);
    3. Cube的X坐标,在SendCubePosition(Vector3 position)方法中的msg.Write(position.x);
    4. Cube的Y坐标,在SendCubePosition(Vector3 position)方法中的msg.Write(position.y);
    5. Cube的Z坐标,在SendCubePosition(Vector3 position)方法中的msg.Write(position.z);

    相同,在解析消息时。也应该依照上面的顺序进行,例如以下:

    1. 消息类型,在ConnectionAdapter_MessageReceivedCallback(NetworkConnection connection, NetworkInMessage msg)方法中的byte messageType = msg.ReadByte();
    2. 用户编号,在ReadCubePosition(NetworkInMessage msg)方法中的msg.ReadInt64();
    3. Cube的X坐标,在ReadCubePosition(NetworkInMessage msg)方法中的”return new Vector3(msg.ReadFloat(), msg.ReadFloat(), msg.ReadFloat());”
    4. Cube的Y坐标。在ReadCubePosition(NetworkInMessage msg)方法中的”return new Vector3(msg.ReadFloat(), msg.ReadFloat(), msg.ReadFloat());”
    5. Cube的Z坐标,在ReadCubePosition(NetworkInMessage msg)方法中的”return new Vector3(msg.ReadFloat(), msg.ReadFloat(), msg.ReadFloat());”
  • 关于消息传递方式

    • MessageReliability.Reliable

      可靠数据包:数据一定到达。但包可能乱序。适用于开关button等相似场景。
    • MessageReliability.ReliableOrdered

      可靠排序数据包:数据一定到达,且经过排序。但须要等待传输最慢的包。适用于聊天等相似场景。
    • MessageReliability.ReliableSequenced

      可靠序列数据包:数据一定到达,且经过排序,不等待慢包。旧包被抛弃。适用于低频有顺序要求的场景。

      比方:每2000ms更新物体的位置。

    • MessageReliability.Unreliable

      不可靠数据包:数据不一定到达,包也可能乱序。适用于语音通话等相似场景。
    • MessageReliability.UnreliableSequenced

      不可靠序列数据包:数据不一定到达。但经过排序,不等待慢包,旧包被抛弃。适用于高频有顺序要求的场景。比方:每100ms更新物体的位置。

Chapter 3 - 空间锚点的使用

目标

实现固化物体到空间,实现仿真的“共享”物体效果

实践

上一章节中,我们尽管实现了Cube的数据同步,但由于每台设备启动后的參考坐标系不同,导致看到的Cube仍然是独立与设备的(对不齐)。所以,要实现仿真的“共享”效果,肯定须要同步设备的世界坐标系。这一章节。我们将会结合空间扫描、空间锚点,来调整Cube的位置,以实现高仿真的“共享”效果。

准备工作:

  • 需开启设备的Spatial Perception功能(在Player Settings…面板的Publishing Settings > Capabilities中勾选)
  • 两台Hololens
  • 设备在同一房间内

原理:

  1. 两台设备在同一房间开启空间扫描,得到基本一致的世界坐标參考系
  2. 当中一台设备在世界坐标系中设置一个锚点(坐标),并绑定到APP中的一个物体上(一般为一个根节点(0, 0, 0))。全部物体作为这个根节点的子集。
  3. 这台设备开设房间(事实上就是自己的世界坐标參考系。房间包括上面的锚点)。并将锚点上传至服务器
  4. 其它设备增加房间。并下载房间中的锚点信息
  5. 将锚点信息绑定到自己APP的根节点上(0, 0, 0)
  6. 之后通过上文提到的Socket技术,传递子集中的各种数据(比方:LocalPosition等)

详细实施

  1. Cube拖放到Controller上,作为子集
  2. Project面板中,找到”Assets/HoloToolkit/ShatialMapping/Prefabs/SpatialMapping.prefab”,拖放到Hierarchy根级
  3. 为了方便測试。我们放置一个文本,显示測试信息。

    将”Assets/HoloToolkit/Utilities/Prefabs/FPSDisplay”拖放到Hierarchy根级,点击FPSDisplay下的FPSText,去掉FPS Display脚本

  4. 新建脚本ImportExportAnchorManager240.cs,并附加给Controller,内容例如以下:

    (代码适用:5.5.0f3版本号)

    using HoloToolkit.Sharing;
    using HoloToolkit.Unity;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.VR.WSA.Persistence;
    using UnityEngine.VR.WSA.Sharing;
    using System;
    using UnityEngine.VR.WSA;
    using HoloToolkit.Unity.SpatialMapping; public class ImportExportAnchorManager240 : Singleton<ImportExportAnchorManager240> { /// <summary>
    /// 建立共享坐标系过程中的各种状态
    /// </summary>
    private enum ImportExportState
    {
    // 总体状态
    /// <summary>
    /// 開始
    /// </summary>
    Start,
    /// <summary>
    /// 已完毕
    /// </summary>
    Ready,
    /// <summary>
    /// 失败
    /// </summary>
    Failed,
    // 本地锚点存储器状态
    /// <summary>
    /// 本地锚点存储器正在初始化
    /// </summary>
    AnchorStore_Initializing,
    /// <summary>
    /// 本地锚点存储器已初始化完毕(在状态机中)
    /// </summary>
    AnchorStore_Initialized,
    /// <summary>
    /// 房间API已初始化完毕(在状态机中)
    /// </summary>
    RoomApiInitialized,
    // Anchor creation values
    /// <summary>
    /// 须要初始锚点(在状态机中)
    /// </summary>
    InitialAnchorRequired,
    /// <summary>
    /// 正在创建初始锚点
    /// </summary>
    CreatingInitialAnchor,
    /// <summary>
    /// 准备导出初始锚点(在状态机中)
    /// </summary>
    ReadyToExportInitialAnchor,
    /// <summary>
    /// 正在上传初始锚点
    /// </summary>
    UploadingInitialAnchor,
    // Anchor values
    /// <summary>
    /// 已请求数据
    /// </summary>
    DataRequested,
    /// <summary>
    /// 数据已准备(在状态机中)
    /// </summary>
    DataReady,
    /// <summary>
    /// 导入中
    /// </summary>
    Importing
    } /// <summary>
    /// 当前状态
    /// </summary>
    private ImportExportState currentState = ImportExportState.Start; /// <summary>
    /// 上次状态。用来測试的。代码在Update中
    /// </summary>
    private ImportExportState lastState = ImportExportState.Start; /// <summary>
    /// 当前状态名
    /// </summary>
    public string StateName
    {
    get
    {
    return currentState.ToString();
    }
    } /// <summary>
    /// 共享坐标系是否已经建立完毕
    /// </summary>
    public bool AnchorEstablished
    {
    get
    {
    return currentState == ImportExportState.Ready;
    }
    } /// <summary>
    /// 序列化坐标锚点并进行设备间的传输
    /// </summary>
    private WorldAnchorTransferBatch sharedAnchorInterface; /// <summary>
    /// 下载的原始锚点数据
    /// </summary>
    private byte[] rawAnchorData = null; /// <summary>
    /// 本地锚点存储器
    /// </summary>
    private WorldAnchorStore anchorStore = null; /// <summary>
    /// 保存我们正在导出的锚点名称
    /// </summary>
    public string ExportingAnchorName = "anchor-1234567890"; /// <summary>
    /// 正在导出的锚点数据
    /// </summary>
    private List<byte> exportingAnchorBytes = new List<byte>(); /// <summary>
    /// 共享服务是否已经准备好,这个是上传和下载锚点数据的前提条件
    /// </summary>
    private bool sharingServiceReady = false; /// <summary>
    /// 共享服务中的房间管理器
    /// </summary>
    private RoomManager roomManager; /// <summary>
    /// 当前房间(锚点将会保存在房间中)
    /// </summary>
    private Room currentRoom; /// <summary>
    /// 有时我们会发现一些非常小非常小的锚点数据。这些往往没法使用。所以我们设置一个最小的可信任大小值
    /// </summary>
    private const uint minTrustworthySerializedAnchorDataSize = 100000; /// <summary>
    /// 房间编号
    /// </summary>
    private const long roomID = 8675309; /// <summary>
    /// 房间管理器的各种事件监听
    /// </summary>
    private RoomManagerAdapter roomManagerCallbacks; protected override void Awake()
    {
    base.Awake();
    // 開始初始化本地锚点存储器
    currentState = ImportExportState.AnchorStore_Initializing;
    WorldAnchorStore.GetAsync(AnchorStoreReady);
    } /// <summary>
    /// 本地锚点存储器已准备好
    /// </summary>
    /// <param name="store">本地锚点存储器</param>
    private void AnchorStoreReady(WorldAnchorStore store)
    {
    Debug.Log("本地锚点存储器(WorldAnchorStore)已准备好 - AnchorStoreReady(WorldAnchorStore store)"); anchorStore = store;
    currentState = ImportExportState.AnchorStore_Initialized;
    } private void Start()
    {
    bool isObserverRunning = SpatialMappingManager.Instance.IsObserverRunning();
    Debug.Log("空间扫描状态:" + isObserverRunning);
    if (!isObserverRunning)
    {
    SpatialMappingManager.Instance.StartObserver();
    } // 共享管理器是否已经连接
    SharingStage.Instance.SharingManagerConnected += Instance_SharingManagerConnected; // 是否增加到当前会话中(此事件在共享管理器连接之后才会触发)
    SharingSessionTracker.Instance.SessionJoined += Instance_SessionJoined;
    } #region 共享管理器连接成功后的一系列处理 // 共享管理器连接事件
    private void Instance_SharingManagerConnected(object sender, EventArgs e)
    {
    Debug.Log("共享管理器连接成功 - Instance_SharingManagerConnected(object sender, EventArgs e)"); // 从共享管理器中获取房间管理器
    roomManager = SharingStage.Instance.Manager.GetRoomManager(); // 房间管理器的事件监听
    roomManagerCallbacks = new RoomManagerAdapter(); // 房间中锚点下载完毕事件
    roomManagerCallbacks.AnchorsDownloadedEvent += RoomManagerCallbacks_AnchorsDownloadedEvent;
    // 房间中锚点上传完毕事件
    roomManagerCallbacks.AnchorUploadedEvent += RoomManagerCallbacks_AnchorUploadedEvent; // 为房间管理器增加上面的事件监听
    roomManager.AddListener(roomManagerCallbacks);
    } // 房间中锚点上传完毕事件
    private void RoomManagerCallbacks_AnchorUploadedEvent(bool successful, XString failureReason)
    {
    if (successful)
    {
    Debug.Log("房间锚点上传完毕 - RoomManagerCallbacks_AnchorUploadedEvent(bool successful, XString failureReason)"); // 房间锚点上传成功后,空间坐标共享机制建立完毕
    currentState = ImportExportState.Ready;
    }
    else
    {
    Debug.Log("房间锚点上传失败 - RoomManagerCallbacks_AnchorUploadedEvent(bool successful, XString failureReason)"); // 房间锚点上传失败
    Debug.Log("Anchor Upload Failed!" + failureReason);
    currentState = ImportExportState.Failed;
    }
    } // 房间中锚点下载完毕事件
    private void RoomManagerCallbacks_AnchorsDownloadedEvent(bool successful, AnchorDownloadRequest request, XString failureReason)
    {
    if (successful)
    {
    Debug.Log("房间锚点下载完毕 - RoomManagerCallbacks_AnchorsDownloadedEvent(bool successful, AnchorDownloadRequest request, XString failureReason)"); // 房间锚点下载完毕
    // 获取锚点数据长度
    int datasize = request.GetDataSize();
    // 将下载的锚点数据缓存到数组中
    rawAnchorData = new byte[datasize]; request.GetData(rawAnchorData, datasize); // 保存完锚点数据,能够開始准备数据传输
    currentState = ImportExportState.DataReady;
    }
    else
    {
    Debug.Log("锚点下载失败!" + failureReason + " - RoomManagerCallbacks_AnchorsDownloadedEvent(bool successful, AnchorDownloadRequest request, XString failureReason)"); // 锚点下载失败,又一次開始请求锚点数据
    MakeAnchorDataRequest();
    }
    } /// <summary>
    /// 请求锚点数据
    /// </summary>
    private void MakeAnchorDataRequest()
    {
    if (roomManager.DownloadAnchor(currentRoom, new XString(ExportingAnchorName)))
    {
    // 下载锚点完毕
    currentState = ImportExportState.DataRequested;
    }
    else
    {
    currentState = ImportExportState.Failed;
    }
    } #endregion #region 成功增加当前会话后的一系列处理 // 增加当前会话完毕
    private void Instance_SessionJoined(object sender, SharingSessionTracker.SessionJoinedEventArgs e)
    {
    SharingSessionTracker.Instance.SessionJoined -= Instance_SessionJoined; // 稍等一下。将共享服务状态设置为正常,即能够開始同步锚点了
    Invoke("MarkSharingServiceReady", 5);
    } /// <summary>
    /// 将共享服务状态设置为正常
    /// </summary>
    private void MarkSharingServiceReady()
    {
    sharingServiceReady = true; #if UNITY_EDITOR || UNITY_STANDALONE InitRoomApi(); #endif } /// <summary>
    /// 初始化房间,直到增加到房间中(Update中会持续调用)
    /// </summary>
    private void InitRoomApi()
    {
    int roomCount = roomManager.GetRoomCount();
    if (roomCount == 0)
    {
    Debug.Log("未找到房间 - InitRoomApi()"); // 假设当前会话中,没有获取到不论什么房间
    if (LocalUserHasLowestUserId())
    {
    // 假设当前用户编号最小,则创建房间
    currentRoom = roomManager.CreateRoom(new XString("DefaultRoom"), roomID, false);
    // 房间创建好,准备载入本地的初始锚点。供其它人共享
    currentState = ImportExportState.InitialAnchorRequired; Debug.Log("我是房主,创建房间完毕 - InitRoomApi()");
    }
    }
    else
    {
    for (int i = 0; i < roomCount; i++)
    {
    // 获取第一个房间为当前房间
    currentRoom = roomManager.GetRoom(i);
    if (currentRoom.GetID() == roomID)
    {
    // 增加当前房间
    roomManager.JoinRoom(currentRoom);
    // TODO: 增加房间,房间API初始化完毕。准备同步初始锚点
    currentState = ImportExportState.RoomApiInitialized; Debug.Log("找到房间并增加! - InitRoomApi()"); return;
    }
    }
    }
    } /// <summary>
    /// 推断当前用户编号是不是全部用户中最小的
    /// </summary>
    /// <returns></returns>
    private bool LocalUserHasLowestUserId()
    {
    for (int i = 0; i < SharingSessionTracker.Instance.UserIds.Count; i++)
    {
    if (SharingSessionTracker.Instance.UserIds[i] < CustomMessages240.Instance.LocalUserID)
    {
    return false;
    }
    } return true;
    } #endregion // Update中处理各种状态(简单状态机)
    private void Update()
    {
    if (currentState != lastState)
    {
    Debug.Log("状态变化:" + lastState.ToString() + " > " + currentState.ToString());
    lastState = currentState;
    } switch (currentState)
    {
    case ImportExportState.AnchorStore_Initialized:
    // 本地锚点存储器初始化完毕
    // 假设成功增加当前会话。则開始载入房间
    if (sharingServiceReady)
    {
    InitRoomApi();
    }
    break;
    case ImportExportState.RoomApiInitialized:
    // 房间已载入完毕,開始载入锚点信息
    StartAnchorProcess();
    break;
    case ImportExportState.DataReady:
    // 锚点数据下载完毕后,開始导入锚点数据
    currentState = ImportExportState.Importing;
    WorldAnchorTransferBatch.ImportAsync(rawAnchorData, ImportComplete);
    break;
    case ImportExportState.InitialAnchorRequired:
    // 房主房间创建完毕后,须要创建初始锚点共享给他人
    currentState = ImportExportState.CreatingInitialAnchor;
    // 创建本地锚点
    CreateAnchorLocally();
    break;
    case ImportExportState.ReadyToExportInitialAnchor:
    // 准备导出初始锚点
    currentState = ImportExportState.UploadingInitialAnchor;
    // 运行导出
    Export();
    break;
    }
    } /// <summary>
    /// 房主将本地锚点共享给其它人
    /// </summary>
    private void Export()
    {
    // 获取锚点,这个组件会在CreateAnchorLocally()中自己主动增加
    WorldAnchor anchor = GetComponent<WorldAnchor>(); if (anchor == null)
    {
    return;
    } // 本地保存该锚点
    if (anchorStore.Save(ExportingAnchorName, anchor))
    {
    // 将锚点导出
    sharedAnchorInterface = new WorldAnchorTransferBatch();
    sharedAnchorInterface.AddWorldAnchor(ExportingAnchorName, anchor);
    WorldAnchorTransferBatch.ExportAsync(sharedAnchorInterface, WriteBuffer, ExportComplete);
    }
    else
    {
    currentState = ImportExportState.InitialAnchorRequired;
    }
    } /// <summary>
    /// 房主导出锚点成功
    /// </summary>
    /// <param name="completionReason"></param>
    private void ExportComplete(SerializationCompletionReason completionReason)
    {
    if (completionReason == SerializationCompletionReason.Succeeded && exportingAnchorBytes.Count > minTrustworthySerializedAnchorDataSize)
    {
    // 将锚点数据上传至当前房间中
    roomManager.UploadAnchor(
    currentRoom,
    new XString(ExportingAnchorName),
    exportingAnchorBytes.ToArray(),
    exportingAnchorBytes.Count);
    }
    else
    {
    currentState = ImportExportState.InitialAnchorRequired;
    }
    } private void WriteBuffer(byte[] data)
    {
    exportingAnchorBytes.AddRange(data);
    } /// <summary>
    /// 房主在本地创建一个新的锚点
    /// </summary>
    private void CreateAnchorLocally()
    {
    Debug.Log("開始创建本地锚点"); // 增加世界锚点组件
    WorldAnchor anchor = GetComponent<WorldAnchor>();
    if (anchor == null)
    {
    anchor = gameObject.AddComponent<WorldAnchor>();
    } if (anchor.isLocated)
    {
    // 房主自己定位好本地锚点后。准备导出给其它人
    currentState = ImportExportState.ReadyToExportInitialAnchor;
    }
    else
    {
    anchor.OnTrackingChanged += WorldAnchorForExport_OnTrackingChanged;
    }
    } private void WorldAnchorForExport_OnTrackingChanged(WorldAnchor self, bool located)
    {
    if (located)
    {
    // 房主自己定位好本地锚点后,准备导出给其它人
    currentState = ImportExportState.ReadyToExportInitialAnchor;
    }
    else
    {
    // 房主自己的锚点定位失败,则同步总体失败
    currentState = ImportExportState.Failed;
    } self.OnTrackingChanged -= WorldAnchorForExport_OnTrackingChanged;
    } /// <summary>
    /// 锚点数据下载完毕后,開始导入锚点数据
    /// </summary>
    /// <param name="completionReason"></param>
    /// <param name="deserializedTransferBatch"></param>
    private void ImportComplete(SerializationCompletionReason completionReason, WorldAnchorTransferBatch deserializedTransferBatch)
    {
    if (completionReason == SerializationCompletionReason.Succeeded && deserializedTransferBatch.GetAllIds().Length > 0)
    {
    // 成功导入锚点
    // 获取第一个锚点名称
    bool hasAnchorName = false;
    string[] anchorNames = deserializedTransferBatch.GetAllIds();
    foreach (var an in anchorNames)
    {
    if (an == ExportingAnchorName)
    {
    hasAnchorName = true;
    break;
    }
    } if (!hasAnchorName)
    {
    currentState = ImportExportState.DataReady;
    return;
    } // 保存锚点到本地
    WorldAnchor anchor = deserializedTransferBatch.LockObject(ExportingAnchorName, gameObject);
    if (anchor.isLocated)
    {
    if(anchorStore.Save(ExportingAnchorName, anchor))
    {
    currentState = ImportExportState.Ready;
    }
    else
    {
    currentState = ImportExportState.DataReady;
    } }
    else
    {
    anchor.OnTrackingChanged += WorldAnchorForImport_OnTrackingChanged;
    }
    }
    else
    {
    // 未成功导入,则设置为DataReady。准备在下一帧再次导入。直到导入完毕
    currentState = ImportExportState.DataReady;
    }
    } private void WorldAnchorForImport_OnTrackingChanged(WorldAnchor self, bool located)
    {
    if (located)
    {
    WorldAnchor anchor = GetComponent<WorldAnchor>();
    if (anchorStore.Save(ExportingAnchorName, anchor))
    {
    currentState = ImportExportState.Ready;
    }
    else
    {
    currentState = ImportExportState.DataReady;
    }
    }
    else
    {
    currentState = ImportExportState.Failed;
    } self.OnTrackingChanged -= WorldAnchorForImport_OnTrackingChanged;
    } /// <summary>
    /// 载入锚点信息
    /// </summary>
    private void StartAnchorProcess()
    {
    Debug.Log("正在获取房间锚点…… - StartAnchorProcess()"); // 检查当前房间有无锚点
    int anchorCount = currentRoom.GetAnchorCount(); if (anchorCount > 0)
    {
    bool isRoomAnchorExists = false; for (int i = 0; i < anchorCount; i++)
    {
    string roomAnchor = currentRoom.GetAnchorName(i).GetString();
    if (roomAnchor == ExportingAnchorName)
    {
    isRoomAnchorExists = true;
    break;
    }
    } if (isRoomAnchorExists)
    {
    Debug.Log("获取房间锚点成功!開始下载锚点");
    // 获取房间锚点信息成功后,開始下载锚点数据
    MakeAnchorDataRequest();
    }
    }
    } protected override void OnDestroy()
    {
    if (SharingStage.Instance != null)
    {
    SharingStage.Instance.SharingManagerConnected -= Instance_SharingManagerConnected;
    } if (roomManagerCallbacks != null)
    {
    roomManagerCallbacks.AnchorsDownloadedEvent -= RoomManagerCallbacks_AnchorsDownloadedEvent;
    roomManagerCallbacks.AnchorUploadedEvent -= RoomManagerCallbacks_AnchorUploadedEvent; if (roomManager != null)
    {
    roomManager.RemoveListener(roomManagerCallbacks);
    }
    } base.OnDestroy();
    }
    }

    (代码适用:5.5.1f1版本号)

    using HoloToolkit.Sharing;
    using HoloToolkit.Unity;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.VR.WSA.Persistence;
    using UnityEngine.VR.WSA.Sharing;
    using System;
    using UnityEngine.VR.WSA;
    using HoloToolkit.Unity.SpatialMapping;
    using System.Text; public class ImportExportAnchorManager240 : Singleton<ImportExportAnchorManager240>
    {
    /// <summary>
    /// 建立共享坐标系过程中的各种状态
    /// </summary>
    private enum ImportExportState
    {
    // 总体状态
    /// <summary>
    /// 開始
    /// </summary>
    Start,
    /// <summary>
    /// 已完毕
    /// </summary>
    Ready,
    /// <summary>
    /// 失败
    /// </summary>
    Failed,
    // 本地锚点存储器状态
    /// <summary>
    /// 本地锚点存储器正在初始化
    /// </summary>
    AnchorStore_Initializing,
    /// <summary>
    /// 本地锚点存储器已初始化完毕(在状态机中)
    /// </summary>
    AnchorStore_Initialized,
    /// <summary>
    /// 房间API已初始化完毕(在状态机中)
    /// </summary>
    RoomApiInitialized,
    // Anchor creation values
    /// <summary>
    /// 须要初始锚点(在状态机中)
    /// </summary>
    InitialAnchorRequired,
    /// <summary>
    /// 正在创建初始锚点
    /// </summary>
    CreatingInitialAnchor,
    /// <summary>
    /// 准备导出初始锚点(在状态机中)
    /// </summary>
    ReadyToExportInitialAnchor,
    /// <summary>
    /// 正在上传初始锚点
    /// </summary>
    UploadingInitialAnchor,
    // Anchor values
    /// <summary>
    /// 已请求数据
    /// </summary>
    DataRequested,
    /// <summary>
    /// 数据已准备(在状态机中)
    /// </summary>
    DataReady,
    /// <summary>
    /// 导入中
    /// </summary>
    Importing
    } /// <summary>
    /// 当前状态
    /// </summary>
    private ImportExportState currentState = ImportExportState.Start; /// <summary>
    /// 上次状态。用来測试的,代码在Update中
    /// </summary>
    private ImportExportState lastState = ImportExportState.Start; /// <summary>
    /// 当前状态名
    /// </summary>
    public string StateName
    {
    get
    {
    return currentState.ToString();
    }
    } /// <summary>
    /// 共享坐标系是否已经建立完毕
    /// </summary>
    public bool AnchorEstablished
    {
    get
    {
    return currentState == ImportExportState.Ready;
    }
    } /// <summary>
    /// 序列化坐标锚点并进行设备间的传输
    /// </summary>
    private WorldAnchorTransferBatch sharedAnchorInterface; /// <summary>
    /// 下载的原始锚点数据
    /// </summary>
    private byte[] rawAnchorData = null; /// <summary>
    /// 本地锚点存储器
    /// </summary>
    private WorldAnchorStore anchorStore = null; /// <summary>
    /// 保存我们正在导出的锚点名称
    /// </summary>
    public string ExportingAnchorName = "anchor-1234567890"; /// <summary>
    /// 正在导出的锚点数据
    /// </summary>
    private List<byte> exportingAnchorBytes = new List<byte>(); /// <summary>
    /// 共享服务是否已经准备好。这个是上传和下载锚点数据的前提条件
    /// </summary>
    private bool sharingServiceReady = false; /// <summary>
    /// 共享服务中的房间管理器
    /// </summary>
    private RoomManager roomManager; /// <summary>
    /// 当前房间(锚点将会保存在房间中)
    /// </summary>
    private Room currentRoom; /// <summary>
    /// 有时我们会发现一些非常小非常小的锚点数据,这些往往没法使用,所以我们设置一个最小的可信任大小值
    /// </summary>
    private const uint minTrustworthySerializedAnchorDataSize = 100000; /// <summary>
    /// 房间编号
    /// </summary>
    private const long roomID = 8675309; /// <summary>
    /// 房间管理器的各种事件监听
    /// </summary>
    private RoomManagerAdapter roomManagerCallbacks; /// <summary>
    /// 锚点上传完毕事件
    /// </summary>
    public event Action<bool> AnchorUploaded; /// <summary>
    /// 锚点载入完毕事件
    /// </summary>
    public event Action AnchorLoaded; private TextMesh lblMsg;
    private StringBuilder sb = new StringBuilder();
    private void debug(string msg)
    {
    Debug.Log(msg);
    sb.AppendLine(msg);
    } protected override void Awake()
    {
    base.Awake(); lblMsg = GameObject.Find("FPSText").GetComponent<TextMesh>(); // 開始初始化本地锚点存储器
    currentState = ImportExportState.AnchorStore_Initializing;
    WorldAnchorStore.GetAsync(AnchorStoreReady);
    } /// <summary>
    /// 本地锚点存储器已准备好
    /// </summary>
    /// <param name="store">本地锚点存储器</param>
    private void AnchorStoreReady(WorldAnchorStore store)
    {
    debug("本地锚点存储器(WorldAnchorStore)已准备好 - AnchorStoreReady(WorldAnchorStore store)"); anchorStore = store;
    currentState = ImportExportState.AnchorStore_Initialized;
    } private void Start()
    { bool isObserverRunning = SpatialMappingManager.Instance.IsObserverRunning();
    debug("空间扫描状态:" + isObserverRunning); if (!isObserverRunning)
    {
    SpatialMappingManager.Instance.StartObserver();
    } // 共享管理器是否已经连接
    SharingStage.Instance.SharingManagerConnected += Instance_SharingManagerConnected; // 是否增加到当前会话中(此事件在共享管理器连接之后才会触发)
    SharingStage.Instance.SessionsTracker.CurrentUserJoined += SessionsTracker_CurrentUserJoined;
    SharingStage.Instance.SessionsTracker.CurrentUserLeft += SessionsTracker_CurrentUserLeft;
    } #region 共享管理器连接成功后的一系列处理 // 共享管理器连接事件
    private void Instance_SharingManagerConnected(object sender, EventArgs e)
    {
    debug("共享管理器连接成功 - Instance_SharingManagerConnected(object sender, EventArgs e)"); // 从共享管理器中获取房间管理器
    roomManager = SharingStage.Instance.Manager.GetRoomManager(); // 房间管理器的事件监听
    roomManagerCallbacks = new RoomManagerAdapter(); // 房间中锚点下载完毕事件
    roomManagerCallbacks.AnchorsDownloadedEvent += RoomManagerCallbacks_AnchorsDownloadedEvent;
    // 房间中锚点上传完毕事件
    roomManagerCallbacks.AnchorUploadedEvent += RoomManagerCallbacks_AnchorUploadedEvent; // 为房间管理器增加上面的事件监听
    roomManager.AddListener(roomManagerCallbacks);
    } // 房间中锚点上传完毕事件
    private void RoomManagerCallbacks_AnchorUploadedEvent(bool successful, XString failureReason)
    {
    if (successful)
    {
    debug("房间锚点上传完毕 - RoomManagerCallbacks_AnchorUploadedEvent(bool successful, XString failureReason)"); // 房间锚点上传成功后。空间坐标共享机制建立完毕
    currentState = ImportExportState.Ready;
    }
    else
    {
    debug("房间锚点上传失败 - RoomManagerCallbacks_AnchorUploadedEvent(bool successful, XString failureReason)"); // 房间锚点上传失败
    debug("Anchor Upload Failed!" + failureReason);
    currentState = ImportExportState.Failed;
    } if (AnchorUploaded != null)
    {
    AnchorUploaded(successful);
    }
    } // 房间中锚点下载完毕事件
    private void RoomManagerCallbacks_AnchorsDownloadedEvent(bool successful, AnchorDownloadRequest request, XString failureReason)
    {
    if (successful)
    {
    debug("房间锚点下载完毕 - RoomManagerCallbacks_AnchorsDownloadedEvent(bool successful, AnchorDownloadRequest request, XString failureReason)"); // 房间锚点下载完毕
    // 获取锚点数据长度
    int datasize = request.GetDataSize(); // 将下载的锚点数据缓存到数组中
    rawAnchorData = new byte[datasize]; request.GetData(rawAnchorData, datasize); // 保存完锚点数据,能够開始准备数据传输
    currentState = ImportExportState.DataReady;
    }
    else
    {
    debug("锚点下载失败! " + failureReason + " - RoomManagerCallbacks_AnchorsDownloadedEvent(bool successful, AnchorDownloadRequest request, XString failureReason)"); // 锚点下载失败,又一次開始请求锚点数据
    MakeAnchorDataRequest();
    }
    } /// <summary>
    /// 请求锚点数据
    /// </summary>
    private void MakeAnchorDataRequest()
    {
    if (roomManager.DownloadAnchor(currentRoom, new XString(ExportingAnchorName)))
    {
    // 下载锚点完毕
    currentState = ImportExportState.DataRequested;
    }
    else
    {
    currentState = ImportExportState.Failed;
    }
    } #endregion #region 成功增加当前会话后的一系列处理 // 增加当前会话完毕
    private void SessionsTracker_CurrentUserJoined(Session session)
    {
    SharingStage.Instance.SessionsTracker.CurrentUserJoined -= SessionsTracker_CurrentUserJoined; // 稍等一下,将共享服务状态设置为正常,即能够開始同步锚点了
    Invoke("MarkSharingServiceReady", 5);
    } // 退出当前会话
    private void SessionsTracker_CurrentUserLeft(Session session)
    {
    sharingServiceReady = false;
    if (anchorStore != null)
    {
    currentState = ImportExportState.AnchorStore_Initialized;
    }
    else
    {
    currentState = ImportExportState.AnchorStore_Initializing;
    }
    } /// <summary>
    /// 将共享服务状态设置为正常
    /// </summary>
    private void MarkSharingServiceReady()
    {
    sharingServiceReady = true; #if UNITY_EDITOR || UNITY_STANDALONE InitRoomApi(); #endif } /// <summary>
    /// 初始化房间,直到增加到房间中(Update中会持续调用)
    /// </summary>
    private void InitRoomApi()
    {
    int roomCount = roomManager.GetRoomCount(); if (roomCount == 0)
    {
    debug("未找到房间 - InitRoomApi()"); // 假设当前会话中,没有获取到不论什么房间
    if (LocalUserHasLowestUserId())
    {
    // 假设当前用户编号最小,则创建房间
    currentRoom = roomManager.CreateRoom(new XString("DefaultRoom"), roomID, false);
    // 房间创建好,准备载入本地的初始锚点。供其它人共享
    currentState = ImportExportState.InitialAnchorRequired; debug("我是房主,创建房间完毕 - InitRoomApi()");
    }
    }
    else
    {
    for (int i = 0; i < roomCount; i++)
    {
    currentRoom = roomManager.GetRoom(i);
    if (currentRoom.GetID() == roomID)
    {
    // 增加当前房间
    roomManager.JoinRoom(currentRoom);
    // TODO: 增加房间,房间API初始化完毕,准备同步初始锚点
    currentState = ImportExportState.RoomApiInitialized; debug("找到房间并增加! - InitRoomApi()"); return;
    }
    }
    }
    } /// <summary>
    /// 推断当前用户编号是不是全部用户中最小的
    /// </summary>
    /// <returns></returns>
    private bool LocalUserHasLowestUserId()
    {
    if (SharingStage.Instance == null)
    {
    return false;
    }
    if (SharingStage.Instance.SessionUsersTracker != null)
    {
    List<User> currentUsers = SharingStage.Instance.SessionUsersTracker.CurrentUsers;
    for (int i = 0; i < currentUsers.Count; i++)
    {
    if (currentUsers[i].GetID() < CustomMessages240.Instance.LocalUserID)
    {
    return false;
    }
    }
    }
    return true;
    } #endregion // Update中处理各种状态(简单状态机)
    private void Update()
    {
    if (currentState != lastState)
    {
    debug("状态变化:" + lastState.ToString() + " > " + currentState.ToString());
    lastState = currentState;
    } lblMsg.text = sb.ToString(); switch (currentState)
    {
    case ImportExportState.AnchorStore_Initialized:
    // 本地锚点存储器初始化完毕
    // 假设成功增加当前会话。则開始载入房间
    if (sharingServiceReady)
    {
    InitRoomApi();
    }
    break;
    case ImportExportState.RoomApiInitialized:
    // 房间已载入完毕,開始载入锚点信息
    StartAnchorProcess();
    break;
    case ImportExportState.DataReady:
    // 锚点数据下载完毕后。開始导入锚点数据
    currentState = ImportExportState.Importing;
    WorldAnchorTransferBatch.ImportAsync(rawAnchorData, ImportComplete);
    break;
    case ImportExportState.InitialAnchorRequired:
    // 房主房间创建完毕后,须要创建初始锚点共享给他人
    currentState = ImportExportState.CreatingInitialAnchor;
    // 创建本地锚点
    CreateAnchorLocally();
    break;
    case ImportExportState.ReadyToExportInitialAnchor:
    // 准备导出初始锚点
    currentState = ImportExportState.UploadingInitialAnchor;
    // 运行导出
    Export();
    break;
    }
    } /// <summary>
    /// 房主将本地锚点共享给其它人
    /// </summary>
    private void Export()
    {
    // 获取锚点,这个组件会在CreateAnchorLocally()中自己主动增加
    WorldAnchor anchor = GetComponent<WorldAnchor>(); anchorStore.Clear();
    // 本地保存该锚点
    if (anchor != null && anchorStore.Save(ExportingAnchorName, anchor))
    {
    debug("保存锚点完毕,准备导出! - Export()");
    // 将锚点导出
    sharedAnchorInterface = new WorldAnchorTransferBatch();
    sharedAnchorInterface.AddWorldAnchor(ExportingAnchorName, anchor);
    WorldAnchorTransferBatch.ExportAsync(sharedAnchorInterface, WriteBuffer, ExportComplete);
    }
    else
    {
    debug("保存本地锚点失败。 - Export()"); currentState = ImportExportState.InitialAnchorRequired;
    }
    } /// <summary>
    /// 房主导出锚点成功
    /// </summary>
    /// <param name="completionReason"></param>
    private void ExportComplete(SerializationCompletionReason completionReason)
    {
    if (completionReason == SerializationCompletionReason.Succeeded && exportingAnchorBytes.Count > minTrustworthySerializedAnchorDataSize)
    {
    // 将锚点数据上传至当前房间中
    roomManager.UploadAnchor(
    currentRoom,
    new XString(ExportingAnchorName),
    exportingAnchorBytes.ToArray(),
    exportingAnchorBytes.Count);
    }
    else
    {
    debug("导出锚点出错!" + completionReason.ToString());
    currentState = ImportExportState.InitialAnchorRequired;
    }
    } private void WriteBuffer(byte[] data)
    {
    exportingAnchorBytes.AddRange(data);
    } /// <summary>
    /// 房主在本地创建一个新的锚点
    /// </summary>
    private void CreateAnchorLocally()
    {
    debug("開始创建本地锚点"); // 增加世界锚点组件
    WorldAnchor anchor = GetComponent<WorldAnchor>();
    if (anchor == null)
    {
    anchor = gameObject.AddComponent<WorldAnchor>();
    } if (anchor.isLocated)
    {
    // 房主自己定位好本地锚点后,准备导出给其它人
    currentState = ImportExportState.ReadyToExportInitialAnchor;
    }
    else
    {
    anchor.OnTrackingChanged += WorldAnchorForExport_OnTrackingChanged;
    }
    } private void WorldAnchorForExport_OnTrackingChanged(WorldAnchor self, bool located)
    {
    if (located)
    {
    // 房主自己定位好本地锚点后,准备导出给其它人
    currentState = ImportExportState.ReadyToExportInitialAnchor;
    }
    else
    {
    // 房主自己的锚点定位失败,则同步总体失败
    currentState = ImportExportState.Failed;
    } self.OnTrackingChanged -= WorldAnchorForExport_OnTrackingChanged;
    } /// <summary>
    /// 锚点数据下载完毕后。開始导入锚点数据
    /// </summary>
    /// <param name="completionReason"></param>
    /// <param name="deserializedTransferBatch"></param>
    private void ImportComplete(SerializationCompletionReason completionReason, WorldAnchorTransferBatch deserializedTransferBatch)
    {
    if (completionReason == SerializationCompletionReason.Succeeded && deserializedTransferBatch.GetAllIds().Length > 0)
    {
    // 成功导入锚点
    // 获取第一个锚点名称
    bool hasAnchorName = false;
    string[] anchorNames = deserializedTransferBatch.GetAllIds();
    foreach (var an in anchorNames)
    {
    if (an == ExportingAnchorName)
    {
    hasAnchorName = true;
    break;
    }
    } if (!hasAnchorName)
    {
    currentState = ImportExportState.DataReady;
    return;
    } // 保存锚点到本地
    WorldAnchor anchor = deserializedTransferBatch.LockObject(ExportingAnchorName, gameObject);
    if (anchor.isLocated)
    {
    if (anchorStore.Save(ExportingAnchorName, anchor))
    {
    currentState = ImportExportState.Ready;
    }
    else
    {
    currentState = ImportExportState.DataReady;
    } }
    else
    {
    anchor.OnTrackingChanged += WorldAnchorForImport_OnTrackingChanged;
    }
    }
    else
    {
    // 未成功导入,则设置为DataReady。准备在下一帧再次导入,直到导入完毕
    currentState = ImportExportState.DataReady;
    }
    } private void WorldAnchorForImport_OnTrackingChanged(WorldAnchor self, bool located)
    {
    if (located)
    {
    WorldAnchor anchor = GetComponent<WorldAnchor>();
    if (anchorStore.Save(ExportingAnchorName, anchor))
    {
    currentState = ImportExportState.Ready;
    }
    else
    {
    currentState = ImportExportState.DataReady;
    }
    }
    else
    {
    currentState = ImportExportState.Failed;
    } self.OnTrackingChanged -= WorldAnchorForImport_OnTrackingChanged;
    } /// <summary>
    /// 载入锚点信息
    /// </summary>
    private void StartAnchorProcess()
    {
    debug("正在获取房间锚点…… - StartAnchorProcess()"); // 检查当前房间有无锚点
    int anchorCount = currentRoom.GetAnchorCount(); if (anchorCount > 0)
    {
    bool isRoomAnchorExists = false; for (int i = 0; i < anchorCount; i++)
    {
    string roomAnchor = currentRoom.GetAnchorName(i).GetString();
    if (roomAnchor == ExportingAnchorName)
    {
    isRoomAnchorExists = true;
    break;
    }
    } if (isRoomAnchorExists)
    {
    debug("获取房间锚点成功!開始下载锚点");
    // 获取房间锚点信息成功后,開始下载锚点数据
    MakeAnchorDataRequest();
    }
    }
    } protected override void OnDestroy()
    {
    if (SharingStage.Instance != null)
    {
    SharingStage.Instance.SharingManagerConnected -= Instance_SharingManagerConnected;
    if (SharingStage.Instance.SessionsTracker != null)
    {
    SharingStage.Instance.SessionsTracker.CurrentUserJoined -= SessionsTracker_CurrentUserJoined;
    SharingStage.Instance.SessionsTracker.CurrentUserLeft -= SessionsTracker_CurrentUserLeft;
    }
    } if (roomManagerCallbacks != null)
    {
    roomManagerCallbacks.AnchorsDownloadedEvent -= RoomManagerCallbacks_AnchorsDownloadedEvent;
    roomManagerCallbacks.AnchorUploadedEvent -= RoomManagerCallbacks_AnchorUploadedEvent; if (roomManager != null)
    {
    roomManager.RemoveListener(roomManagerCallbacks);
    } roomManagerCallbacks.Dispose();
    roomManagerCallbacks = null;
    } if (roomManager != null)
    {
    roomManager.Dispose();
    roomManager = null;
    } base.OnDestroy();
    }
    }

    代码有点多,但理解起来并不困难,核心就是一个维护一个简单状态机。我写好了凝视,然后还画了张状态图帮助大家理解,例如以下:

    Hololens官方教程精简版 - 08. Sharing holograms(共享全息影像)

  5. 由于Cube已经作为Controller的子集。我们将之前传递Position改为传递LocalPosition,改动Cube240.cs内容例如以下:

    (代码适用:5.5.0f3版本号)

    using HoloToolkit.Sharing;
    using HoloToolkit.Unity.InputModule;
    using UnityEngine; public class Cube240 : MonoBehaviour, IInputClickHandler { // 是否正在移动
    bool isMoving = false; // 消息传递类
    CustomMessages240 customMessage; private void Start()
    {
    customMessage = CustomMessages240.Instance; // 指定收到Cube位置变化消息后的处理方法
    customMessage.MessageHandlers[CustomMessages240.CustomMessageID.CubePosition] = OnCubePositionReceived;
    } private void OnCubePositionReceived(NetworkInMessage msg)
    {
    // 同步Cube位置
    if (!isMoving)
    {
    transform.localPosition = CustomMessages240.ReadCubePosition(msg);
    }
    } // 单击Cube。切换是否移动
    public void OnInputClicked(InputEventData eventData)
    {
    isMoving = !isMoving;
    // 放置Cube后,发送Cube的位置消息给其它人
    if (!isMoving)
    {
    customMessage.SendCubePosition(transform.localPosition);
    }
    } // 假设Cube为移动状态,让其放置在镜头前2米位置
    void Update () {
    if (isMoving)
    {
    transform.position = Camera.main.transform.position + Camera.main.transform.forward * 2f;
    customMessage.SendCubePosition(transform.localPosition, MessageReliability.UnreliableSequenced);
    }
    }
    }

    (代码适用:5.5.1f1版本号)

    using HoloToolkit.Sharing;
    using HoloToolkit.Unity.InputModule;
    using UnityEngine; public class Cube240 : MonoBehaviour, IInputClickHandler
    { // 是否正在移动
    bool isMoving = false; // 消息传递类
    CustomMessages240 customMessage; private void Start()
    {
    customMessage = CustomMessages240.Instance; // 指定收到Cube位置变化消息后的处理方法
    customMessage.MessageHandlers[CustomMessages240.CustomMessageID.CubePosition] = OnCubePositionReceived;
    } private void OnCubePositionReceived(NetworkInMessage msg)
    {
    // 同步Cube位置
    if (!isMoving)
    {
    transform.localPosition = CustomMessages240.ReadCubePosition(msg);
    }
    } // 单击Cube。切换是否移动
    public void OnInputClicked(InputClickedEventData eventData)
    {
    isMoving = !isMoving;
    // 放置Cube后,发送Cube的位置消息给其它人
    if (!isMoving)
    {
    customMessage.SendCubePosition(transform.localPosition);
    }
    } // 假设Cube为移动状态,让其放置在镜头前2米位置
    void Update()
    {
    if (isMoving)
    {
    transform.position = Camera.main.transform.position + Camera.main.transform.forward * 2f;
    customMessage.SendCubePosition(transform.localPosition, MessageReliability.UnreliableSequenced);
    }
    }
    }

    本节完毕。

将代码公布到两台设备上。进行測试!

注意,本例中一開始并未同步Cube位置,须要某台设备移动Cube后才干看见效果。

完毕后,Hierarchy结构相似:

Hololens官方教程精简版 - 08. Sharing holograms(共享全息影像)

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQveXVyaWNob3U=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="Hierarchy结构" title="">

说明

学习官方教程的过程中,有非常大几率会遇到多台设备物体尽管能够正常显示Cube。也能正常移动Cube。但各设备的Cube并没有重叠。移动Cube时,可能还有一台设备的Cube是往不同的方向移动。要问为什么?由于官方代码中充满了以下这样的代码:

roomManager.GetRoom(0)
currentRoom.GetAnchorName(0)
wat.GetAllIds()[0]

入坑的同学如今应该已经明确了,当出现多个房间或者多个锚点时。设备总是拿第一个。造成设备间同步的并不一定是同一个房间或者锚点。

小结

这一节太伤元气,小结就写成问答形式了。

  1. 问: 为什么真机扫描不出来空间模型?

    答: 请检查是否开启Spatial Perception功能(终于须要检查Package.appxmanifest文件里节点是否增加了我们须要的设备特性),以下是我測试时的配置:

    <Capabilities>
    <Capability Name="internetClient" />
    <Capability Name="internetClientServer" />
    <Capability Name="privateNetworkClientServer" />
    <uap2:Capability Name="spatialPerception" />
    <DeviceCapability Name="microphone" />
    </Capabilities>

    另外。请检查是否放置了SpatialMapping.prefab,并打开Auto Start Observer

  2. 问: 出现”SpatialAnchorTransferManager denied access to WorldAnchor serialization”的提示

    答: 首先,请不要在Unity里測试(无论是不是Remoting),否则这个现象肯定会出现

    其次。看第1点。
  3. 问: 仅仅能用多台真机进行測试吗?

    答: 眼下是这样,主要是由于World Anchor
  4. 问: 设备*享的物体,并没有重叠?

    答: 好吧,请注意代码逻辑,这要求设备扫描了同一空间。增加了同一房间,共享了同一锚点。锚点附加同一物体(要求真高!)。
  5. 问: SpatialMapping好卡啊

    答: 临时先关闭Draw Visual Meshes
  6. 问: 设备測试的时候,设备休眠时间太短,在哪里能够设置得长一点?

    答: 设备打开,浏览器訪问设备IP,进入:Hololens Device PortalHome菜单下有个Sleep settings,最长设置30分钟。

參考文档

官方教程Holograms 240:https://developer.microsoft.com/en-us/windows/mixed-reality/holograms_240


VR/AR/MR技术交流QQ群:594299228

Hololens官方教程精简版 - 08. Sharing holograms(共享全息影像)

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQveXVyaWNob3U=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="VR/AR/MR技术交流QQ群:594299228" title="">

上一篇:浅谈background-size的几个属性值


下一篇:Masonry 在swift下的使用