前言

注意:本文已更新到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;}}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

(代码适用: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;}}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

脚本实现了Cube的移动和放置,可以测试一下效果。

  • 下面,我们来实现两台设备传递Cube的位置。
  • Hierarchy面板,创建根级空对象,命名为:”Controller
  • 建立一个消息传递类。
    新建脚本:”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;// 当前连接的事件监听器,这是一个典型的适配器模式,继承自NetworkConnectionListenerNetworkConnectionAdapter 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());}
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
  • 修改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;}}
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    (代码适用: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;}}
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
  • 发布到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); // 默认频道}
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
  • 找到”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);}
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    再次测试,不论是移动还是放置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_STANDALONEInitRoomApi();#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();}
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227
    • 228
    • 229
    • 230
    • 231
    • 232
    • 233
    • 234
    • 235
    • 236
    • 237
    • 238
    • 239
    • 240
    • 241
    • 242
    • 243
    • 244
    • 245
    • 246
    • 247
    • 248
    • 249
    • 250
    • 251
    • 252
    • 253
    • 254
    • 255
    • 256
    • 257
    • 258
    • 259
    • 260
    • 261
    • 262
    • 263
    • 264
    • 265
    • 266
    • 267
    • 268
    • 269
    • 270
    • 271
    • 272
    • 273
    • 274
    • 275
    • 276
    • 277
    • 278
    • 279
    • 280
    • 281
    • 282
    • 283
    • 284
    • 285
    • 286
    • 287
    • 288
    • 289
    • 290
    • 291
    • 292
    • 293
    • 294
    • 295
    • 296
    • 297
    • 298
    • 299
    • 300
    • 301
    • 302
    • 303
    • 304
    • 305
    • 306
    • 307
    • 308
    • 309
    • 310
    • 311
    • 312
    • 313
    • 314
    • 315
    • 316
    • 317
    • 318
    • 319
    • 320
    • 321
    • 322
    • 323
    • 324
    • 325
    • 326
    • 327
    • 328
    • 329
    • 330
    • 331
    • 332
    • 333
    • 334
    • 335
    • 336
    • 337
    • 338
    • 339
    • 340
    • 341
    • 342
    • 343
    • 344
    • 345
    • 346
    • 347
    • 348
    • 349
    • 350
    • 351
    • 352
    • 353
    • 354
    • 355
    • 356
    • 357
    • 358
    • 359
    • 360
    • 361
    • 362
    • 363
    • 364
    • 365
    • 366
    • 367
    • 368
    • 369
    • 370
    • 371
    • 372
    • 373
    • 374
    • 375
    • 376
    • 377
    • 378
    • 379
    • 380
    • 381
    • 382
    • 383
    • 384
    • 385
    • 386
    • 387
    • 388
    • 389
    • 390
    • 391
    • 392
    • 393
    • 394
    • 395
    • 396
    • 397
    • 398
    • 399
    • 400
    • 401
    • 402
    • 403
    • 404
    • 405
    • 406
    • 407
    • 408
    • 409
    • 410
    • 411
    • 412
    • 413
    • 414
    • 415
    • 416
    • 417
    • 418
    • 419
    • 420
    • 421
    • 422
    • 423
    • 424
    • 425
    • 426
    • 427
    • 428
    • 429
    • 430
    • 431
    • 432
    • 433
    • 434
    • 435
    • 436
    • 437
    • 438
    • 439
    • 440
    • 441
    • 442
    • 443
    • 444
    • 445
    • 446
    • 447
    • 448
    • 449
    • 450
    • 451
    • 452
    • 453
    • 454
    • 455
    • 456
    • 457
    • 458
    • 459
    • 460
    • 461
    • 462
    • 463
    • 464
    • 465
    • 466
    • 467
    • 468
    • 469
    • 470
    • 471
    • 472
    • 473
    • 474
    • 475
    • 476
    • 477
    • 478
    • 479
    • 480
    • 481
    • 482
    • 483
    • 484
    • 485
    • 486
    • 487
    • 488
    • 489
    • 490
    • 491
    • 492
    • 493
    • 494
    • 495
    • 496
    • 497
    • 498
    • 499
    • 500
    • 501
    • 502
    • 503
    • 504
    • 505
    • 506
    • 507
    • 508
    • 509
    • 510
    • 511
    • 512
    • 513
    • 514
    • 515
    • 516
    • 517
    • 518
    • 519
    • 520
    • 521
    • 522
    • 523
    • 524
    • 525
    • 526
    • 527
    • 528
    • 529
    • 530
    • 531
    • 532
    • 533
    • 534
    • 535
    • 536
    • 537
    • 538
    • 539
    • 540
    • 541
    • 542
    • 543
    • 544
    • 545
    • 546
    • 547
    • 548
    • 549
    • 550
    • 551
    • 552
    • 553
    • 554
    • 555
    • 556
    • 557
    • 558
    • 559
    • 560
    • 561
    • 562
    • 563
    • 564
    • 565
    • 566
    • 567
    • 568
    • 569
    • 570
    • 571
    • 572
    • 573
    • 574
    • 575
    • 576
    • 577
    • 578
    • 579
    • 580
    • 581
    • 582
    • 583
    • 584
    • 585
    • 586
    • 587
    • 588
    • 589
    • 590
    • 591
    • 592
    • 593
    • 594
    • 595
    • 596
    • 597
    • 598
    • 599
    • 600
    • 601
    • 602
    • 603
    • 604
    • 605
    • 606
    • 607
    • 608
    • 609
    • 610
    • 611
    • 612
    • 613
    • 614
    • 615
    • 616
    • 617
    • 618
    • 619
    • 620
    • 621
    • 622
    • 623
    • 624
    • 625
    • 626
    • 627
    • 628
    • 629
    • 630
    • 631
    • 632
    • 633
    • 634
    • 635
    • 636
    • 637
    • 638
    • 639
    • 640

    (代码适用: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_STANDALONEInitRoomApi();#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();}
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227
    • 228
    • 229
    • 230
    • 231
    • 232
    • 233
    • 234
    • 235
    • 236
    • 237
    • 238
    • 239
    • 240
    • 241
    • 242
    • 243
    • 244
    • 245
    • 246
    • 247
    • 248
    • 249
    • 250
    • 251
    • 252
    • 253
    • 254
    • 255
    • 256
    • 257
    • 258
    • 259
    • 260
    • 261
    • 262
    • 263
    • 264
    • 265
    • 266
    • 267
    • 268
    • 269
    • 270
    • 271
    • 272
    • 273
    • 274
    • 275
    • 276
    • 277
    • 278
    • 279
    • 280
    • 281
    • 282
    • 283
    • 284
    • 285
    • 286
    • 287
    • 288
    • 289
    • 290
    • 291
    • 292
    • 293
    • 294
    • 295
    • 296
    • 297
    • 298
    • 299
    • 300
    • 301
    • 302
    • 303
    • 304
    • 305
    • 306
    • 307
    • 308
    • 309
    • 310
    • 311
    • 312
    • 313
    • 314
    • 315
    • 316
    • 317
    • 318
    • 319
    • 320
    • 321
    • 322
    • 323
    • 324
    • 325
    • 326
    • 327
    • 328
    • 329
    • 330
    • 331
    • 332
    • 333
    • 334
    • 335
    • 336
    • 337
    • 338
    • 339
    • 340
    • 341
    • 342
    • 343
    • 344
    • 345
    • 346
    • 347
    • 348
    • 349
    • 350
    • 351
    • 352
    • 353
    • 354
    • 355
    • 356
    • 357
    • 358
    • 359
    • 360
    • 361
    • 362
    • 363
    • 364
    • 365
    • 366
    • 367
    • 368
    • 369
    • 370
    • 371
    • 372
    • 373
    • 374
    • 375
    • 376
    • 377
    • 378
    • 379
    • 380
    • 381
    • 382
    • 383
    • 384
    • 385
    • 386
    • 387
    • 388
    • 389
    • 390
    • 391
    • 392
    • 393
    • 394
    • 395
    • 396
    • 397
    • 398
    • 399
    • 400
    • 401
    • 402
    • 403
    • 404
    • 405
    • 406
    • 407
    • 408
    • 409
    • 410
    • 411
    • 412
    • 413
    • 414
    • 415
    • 416
    • 417
    • 418
    • 419
    • 420
    • 421
    • 422
    • 423
    • 424
    • 425
    • 426
    • 427
    • 428
    • 429
    • 430
    • 431
    • 432
    • 433
    • 434
    • 435
    • 436
    • 437
    • 438
    • 439
    • 440
    • 441
    • 442
    • 443
    • 444
    • 445
    • 446
    • 447
    • 448
    • 449
    • 450
    • 451
    • 452
    • 453
    • 454
    • 455
    • 456
    • 457
    • 458
    • 459
    • 460
    • 461
    • 462
    • 463
    • 464
    • 465
    • 466
    • 467
    • 468
    • 469
    • 470
    • 471
    • 472
    • 473
    • 474
    • 475
    • 476
    • 477
    • 478
    • 479
    • 480
    • 481
    • 482
    • 483
    • 484
    • 485
    • 486
    • 487
    • 488
    • 489
    • 490
    • 491
    • 492
    • 493
    • 494
    • 495
    • 496
    • 497
    • 498
    • 499
    • 500
    • 501
    • 502
    • 503
    • 504
    • 505
    • 506
    • 507
    • 508
    • 509
    • 510
    • 511
    • 512
    • 513
    • 514
    • 515
    • 516
    • 517
    • 518
    • 519
    • 520
    • 521
    • 522
    • 523
    • 524
    • 525
    • 526
    • 527
    • 528
    • 529
    • 530
    • 531
    • 532
    • 533
    • 534
    • 535
    • 536
    • 537
    • 538
    • 539
    • 540
    • 541
    • 542
    • 543
    • 544
    • 545
    • 546
    • 547
    • 548
    • 549
    • 550
    • 551
    • 552
    • 553
    • 554
    • 555
    • 556
    • 557
    • 558
    • 559
    • 560
    • 561
    • 562
    • 563
    • 564
    • 565
    • 566
    • 567
    • 568
    • 569
    • 570
    • 571
    • 572
    • 573
    • 574
    • 575
    • 576
    • 577
    • 578
    • 579
    • 580
    • 581
    • 582
    • 583
    • 584
    • 585
    • 586
    • 587
    • 588
    • 589
    • 590
    • 591
    • 592
    • 593
    • 594
    • 595
    • 596
    • 597
    • 598
    • 599
    • 600
    • 601
    • 602
    • 603
    • 604
    • 605
    • 606
    • 607
    • 608
    • 609
    • 610
    • 611
    • 612
    • 613
    • 614
    • 615
    • 616
    • 617
    • 618
    • 619
    • 620
    • 621
    • 622
    • 623
    • 624
    • 625
    • 626
    • 627
    • 628
    • 629
    • 630
    • 631
    • 632
    • 633
    • 634
    • 635
    • 636
    • 637
    • 638
    • 639
    • 640
    • 641
    • 642
    • 643
    • 644
    • 645
    • 646
    • 647
    • 648
    • 649
    • 650
    • 651
    • 652
    • 653
    • 654
    • 655
    • 656
    • 657
    • 658
    • 659
    • 660
    • 661
    • 662
    • 663
    • 664
    • 665
    • 666
    • 667
    • 668
    • 669
    • 670
    • 671
    • 672
    • 673
    • 674
    • 675
    • 676
    • 677
    • 678
    • 679
    • 680
    • 681
    • 682
    • 683
    • 684
    • 685
    • 686
    • 687
    • 688
    • 689
    • 690
    • 691
    • 692
    • 693
    • 694
    • 695
    • 696
    • 697
    • 698
    • 699
    • 700
    • 701
    • 702
    • 703
    • 704
    • 705
    • 706
    • 707
    • 708
    • 709
    • 710

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

  • 因为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);}}
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    (代码适用: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);}}
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    本节完成!

    将代码发布到两台设备上,进行测试!注意,本例中一开始并未同步Cube位置,需要某台设备移动Cube后才能看见效果。

    完成后,Hierarchy结构类似:

    说明

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

    roomManager.GetRoom(0)
    currentRoom.GetAnchorName(0)
    wat.GetAllIds()[0]
    • 1
    • 2
    • 3

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

    小结

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

    1. 问: 为什么真机扫描不出来空间模型?
      答: 请检查是否开启Spatial Perception功能(最终需要检查Package.appxmanifest文件中节点是否加入了我们需要的设备特性),下面是我测试时的配置:

    <Capabilities><Capability Name="internetClient" /><Capability Name="internetClientServer" /><Capability Name="privateNetworkClientServer" /><uap2:Capability Name="spatialPerception" /><DeviceCapability Name="microphone" />
    </Capabilities>
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

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

  • 问: 出现”SpatialAnchorTransferManager denied access to WorldAnchor serialization”的提示
    答: 首先,请不要在Unity里测试(不管是不是Remoting),否则这个现象肯定会出现
    其次,看第1点。
  • 问: 只能用多台真机进行测试吗?
    答: 目前是这样,主要是因为World Anchor
  • 问: 设备中共享的物体,并没有重叠?
    答: 好吧,请注意代码逻辑,这要求设备扫描了同一空间,加入了同一房间,共享了同一锚点,锚点附加同一物体(要求真高!)。
  • 问: SpatialMapping好卡啊
    答: 暂时先关闭Draw Visual Meshes
  • 问: 设备测试的时候,设备休眠时间太短,在哪里可以设置得长一点?
    答: 设备打开,浏览器访问设备IP,进入:Hololens Device PortalHome菜单下有个Sleep settings,最长设置30分钟。
  • 参考文档
    官方教程Holograms 240:https://developer.microsoft.com/en-us/windows/mixed-reality/holograms_240

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

  1. Hololens官方教程精简版 - 07. Spatial mapping(空间映射)

    前言 注意:本文已更新到5.5.1f1版本 个人建议,学习Holograms 230之前,一定完成<Hololens官方教程精简版 - 02. Introduction with Device& ...

  2. 微软官方XP精简版 FLP入门版SP3截图

    导读:         Windows Fundamentals for Legacy PCs作为官方发布的精简版Windows XP,与XP SP2基于相同的内核.         所以它同样适用于 ...

  3. 微软官方的精简版Windows 7——Windows Thin PC

    Windows Thin PC是微软的Window 7的精简版本,它的一个非常明显的特点是系统资源占用小.如下是官方的系统配置需求: CPU:    主频1 GHz 内存:    1GB(实际占用约5 ...

  4. Uniapp教程精简版

    uniapp中似乎默认是严格模式 必须声明变量 https://ask.dcloud.net.cn/article/35657 这个写得好 promise化 还有vue2和3不一样 https://u ...

  5. 技术员联盟Win11 64位官方全新旗舰版镜像V2021.08

    技术员联盟Win11 64位官方全新旗舰版镜像V2021.08以微软官方纯净版作为母盘对系统进行了全面优化更新,用户使用更加流畅顺手,轻松体验到系统的优秀性能,适用目前市场最新机型以及老旧机型,多种安 ...

  6. qnap虚拟linux,QNAP进阶教程:威联通NAS 虚拟机教程 安装精简版win10、软路由、群晖NAS系统!...

    威联通最具体的虚拟机利用方式之一,接待点赞.收藏! 6 t* L# m7 X+ E前言 - A- ^8 |# l0 E' E$ k大师好,俺又来了!5 G, c% {7 ]- Z6 Q$ ] 4 }% ...

  7. 官方精简版!Windows 10 LTSC 2021 正式发布

    微软现已正式发布 Windows 10 企业版 LTSC 2021.LTSC 全称为 the Long-Term Servicing Channel,即长期服务频道. 使用 LTSC 服务模型,Win ...

  8. Hanwang汉王笔精简版 20120207官方最新版

    2019独角兽企业重金招聘Python工程师标准>>> Hanwang汉王笔精简版是一款汉王笔精简版驱动程序软件,适用于08年之后生产的USB系列汉王笔,支持Windows XP/V ...

  9. 【体验贴】EOS_v2.0版官方教程

    Getting Started 本次体验依据EOSIO官方教程Getting Started章节 体验了启动节点.管理钱包.创建账户.编写合约.编译部署合约.调用合约等功能 详情参看https://d ...

最新文章

  1. 【Linux 内核】编译 Linux 内核 ⑤ ( 查看 .config 编译配置文件 | 正式编译内核 )
  2. 《周四橄榄球之夜》流媒体视频拆解:Twitch VS Amazon Prime
  3. 数据即代码:元驱动编程
  4. 前端学习(3099):vue+element今日头条管理-使用富文本比编辑器
  5. MyBatis-Plus_入门试炼03
  6. html获取随机字母,JavaScript实现4位随机验证码的生成
  7. 并不对劲的[USACO07NOV,洛谷p2886]Cow Relays
  8. 【BZOJ4554】游戏(二分图匹配,网络流)
  9. 消费者原理分析-RocketMQ知识体系4
  10. Android 中关于属性动画的一些思考,或许能为你解决一定的性能问题
  11. presscad排样lisp_PRESSCAD统赢使用技巧(二)
  12. [RK3399][Android7.1] DDR动态频率调节驱动小结
  13. macOS Ventura 13.0 (22A380) 正式版发布,ISO、IPSW、PKG 下载
  14. creator pvr etc
  15. BlazeFace测试
  16. mysql daemon failed to start._MySQL Daemon failed to start错误解决办法
  17. 华为H3C配置单臂路由、静态默认浮动路由的配置方法
  18. Linux系统忘记了root密码怎么办?
  19. 明星的阶梯:威尼斯电影节 | 经济学人早报精选20210831
  20. JavaScript动画和CSS3动画的区别

热门文章

  1. 安装postman工具 出现请设置注册表项HKLM\Software\Microsoft\.NETFramework\InstallRoot,指向.NET Fra
  2. 综述 - 染色质可及性与调控表观基因组 | Chromatin accessibility and the regulatory epigenome...
  3. SQLSTATE[HY000]: General error: 1366 Incorrect string value: ‘\xF0\x9F\x98\x84‘ for column ‘content‘
  4. 谷歌翻译退出,idea谷歌翻译无法使用(解决)
  5. java 检测表情符号_hutool工具包:判断一个字符串中是否包含emoji表情符号(emoji-java)...
  6. Brooklyn 2.1.0 (60+超炫苹果标志动画屏保)
  7. 清华教授的操作系统-----课程笔记
  8. Iphone 游戏引擎剖析
  9. 物理层、数据链路层间的PHY、MAC、MII、RMII、SMII、GMII、RGMII以及I2S总线、SFP接口
  10. 21考研复习规划和北邮计算机考研介绍