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

Posted PhiloChou

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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,设置如下:

本节完成!

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

目标

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

实践

搭建Socket服务基础环境

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

  1. 在下载的HoloToolkit-Unity开发包中,找到:”External\\”文件夹,将其复制到项目目录下(与Assets文件夹同级目录)。如图:
  2. 点击Unity主菜单下的:HoloToolkit > Sharing Service > Launch Sharing Service,如图:
  3. 此时将会打开一个Socket服务端,如图所示,记录下IP,例如本例为:192.168.0.108
  4. Project面板中,找到:”Assets/HoloToolkit/Sharing/Prefabs/Sharing.prefab”,拖动到Hierarchy根级,并在其Inspector面板中找到Server Address属性,填写上面一步得到的IP地址。如图:

    此步相当于为APP增加了一个Socket客户端。

以上步骤完成后,可以点击Play按钮,并观察Socket服务端界面,看是否有设备加入到服务器。如图:


创建Socket消息传输类

上一步中,我们利用HoloToolkit提供的Socket套件,搭建了基础数据传输环境(包含一个Socket服务端程序和一个Socket客户端连接组件),下面用一个移动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组件对应的脚本,内部是对经典的Socket客户端的封装。
            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消息(客户端传递给服务端)
        // 消息格式第一个必须为消息类型,其后再添加自己的数据
        // 我们在所有的消息一开始添加消息发送的用户编号
        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设备,启动,同时再点击UnityPlay按钮

当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,两个客户端都可以实时看到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
      可靠数据包:数据一定到达,但包可能乱序。适用于开关按钮等类似场景。
    • 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))