使用MultipeerConnectivity框架在附近设备之间传输ARKit世界地图数据以创建AR体验的共享基础。
此示例应用程序演示了两个或更多iOS 12设备的简单共享AR体验。 在探索代码之前,请尝试构建并运行应用,以熟悉它演示的用户体验:
按照以下步骤查看此应用程序如何使用ARWorldMap类保存和恢复ARKit的空间映射状态,以及MultipeerConnectivity框架在附近设备之间发送世界地图。
此应用程序扩展了构建ARKit应用程序的基本工作流程。 (有关详细信息,请参阅构建您的第一个AR体验。)它定义了启用了平面检测的ARWorldTrackingConfiguration,然后在附加到显示AR体验的ARSCNView的ARSession中运行该配置。
当UITapGestureRecognizer在屏幕上检测到轻击时,handleSceneTap方法使用ARKit命中测试在真实世界表面上查找3D点,然后放置标记该位置的ARAnchor。 当ARKit调用委托方法ARSCNView时,应用程序会加载ARSCNView的3D模型以显示在锚点的位置。
示例MultipeerSession类提供了有关此应用程序使用的MultipeerConnectivity功能的简单抽象。 在主视图控制器创建MultipeerSession实例(应用程序启动时)后,它开始运行MCNearbyServiceAdvertiser以广播设备加入多对等会话的能力,并通过MCNearbyServiceBrowser查找其他设备:
session = MCSession(peer: myPeerID, securityIdentity: nil, encryptionPreference: .required)
session.delegate = self
serviceAdvertiser = MCNearbyServiceAdvertiser(peer: myPeerID, discoveryInfo: nil, serviceType: MultipeerSession.serviceType)
serviceAdvertiser.delegate = self
serviceAdvertiser.startAdvertisingPeer()
serviceBrowser = MCNearbyServiceBrowser(peer: myPeerID, serviceType: MultipeerSession.serviceType)
serviceBrowser.delegate = self
serviceBrowser.startBrowsingForPeers()
当MCNearbyServiceBrowser找到另一个设备时,它会调用 browser:foundPeer:withDiscoveryInfo:
delegate方法。 要将其他设备邀请到共享会话,请调用browser的 invitePeer:toSession:withContext:timeout:
public func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String: String]?) {
// Invite the new peer to the session.
browser.invitePeer(peerID, to: session, withContext: nil, timeout: 10)
}
当其他设备收到该邀请时,MCNearbyServiceAdvertiser将调用 advertiser:didReceiveInvitationFromPeer:withContext:invitationHandler:
delegate方法。 要接受邀请,请调用所提供的invitationHandler:
func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void) {
// Call handler to accept invitation and join the session.
invitationHandler(true, self.session)
}
重要:此应用会自动加入找到的第一个附近会话。 根据您想要创建的共享AR体验的种类,您可能希望更精确地控制广播,邀请和接受行为。 有关详细信息,请参阅MultipeerConnectivity文档。
在多人会议中,所有参与者根据定义都是平等的; 没有将设备明确分离为主机(或服务器或主机)和来宾(或客户机或从机)角色。 但是,您可能希望为您自己的AR体验定义这些角色。 例如,多人游戏设计可能需要服务器角色来仲裁游戏玩法。 如果您需要按角色分隔对等点,则可以选择适合您应用设计的方式。 例如:
ARWorldMap对象包含ARKit用于在真实世界空间中定位用户设备的所有空间映射信息的快照。 将地图可靠地共享到其他设备需要两个关键步骤:找到拍摄地图并捕获和发送地图的好时机。
ARKit提供了一个worldMappingStatus值,该值指示当前是捕获世界地图的好时机(还是等到ARKit已映射更多本地环境为好)。 该应用使用该值为其“发送世界地图”按钮提供视觉反馈:
switch frame.worldMappingStatus {
case .notAvailable, .limited:
sendMapButton.isEnabled = false
case .extending:
sendMapButton.isEnabled = !multipeerSession.connectedPeers.isEmpty
case .mapped:
sendMapButton.isEnabled = !multipeerSession.connectedPeers.isEmpty
}
mappingStatusLabel.text = frame.worldMappingStatus.description
当用户点击发送世界地图按钮时,应用程序调用 getCurrentWorldMapWithCompletionHandler:
从正在运行的ARSession捕获地图,然后使用NSKeyedArchiver将其序列化为数据对象,并将其发送到多对话会话中的其他设备:
sceneView.session.getCurrentWorldMap { worldMap, error in
guard let map = worldMap
else { print("Error: \(error!.localizedDescription)"); return }
guard let data = try? NSKeyedArchiver.archivedData(withRootObject: map, requiringSecureCoding: true)
else { fatalError("can't encode map") }
self.multipeerSession.sendToAllPeers(data)
}
当设备接收多方会话中另一个参与方发送的数据时,session:didReceiveData:fromPeer:
delegate方法提供该数据。 为了使用它,应用程序使用NSKeyedUnarchiver对ARWorldMap对象进行反序列化,然后使用该映射作为 initialWorldMap:
创建并运行一个新的ARWorldTrackingConfiguration
if let unarchived = try? NSKeyedUnarchiver.unarchivedObject(of: ARWorldMap.classForKeyedUnarchiver(), from: data),
let worldMap = unarchived as? ARWorldMap {
// Run the session with the received world map.
let configuration = ARWorldTrackingConfiguration()
configuration.planeDetection = .horizontal
configuration.initialWorldMap = worldMap
sceneView.session.run(configuration, options: [.resetTracking, .removeExistingAnchors])
// Remember who provided the map for showing UI feedback.
mapProvider = peer
}
然后,ARKit尝试重新定位到新的世界地图 - 也就是说,将接收到的空间映射信息与它感知的本地环境进行协调。 为获得最佳效果:
共享世界地图也共享所有现有的锚点。 在这个应用程序中,这意味着只要接收设备重新定位到世界地图,它就会显示发送设备在捕获并发送世界地图之前放置的所有3D角色。 但是,录制和传输世界地图并重新定位到世界地图是耗时的,带宽密集型操作,所以当新设备加入会话时,您只应采取一次这些步骤。
要创建持续的共享增强现实体验,其中每个用户的操作都会影响其他用户可见的增强现实场景,在每个设备重新定位到同一世界地图后,您应该只共享重新创建每个用户操作所需的信息。 例如,在这个应用程序中,用户可以点击在场景中放置一个虚拟3D角色。 该角色是静态的,所以将角色放置在另一个参与设备上需要的只是角色在世界空间中的位置和方向。
这个应用程序通过在对等点之间共享ARAnchor对象来传递虚拟角色位置。 当一个用户点击场景时,应用程序创建一个锚点并将其添加到本地ARSession,然后使用Data将该ARAnchor序列化并将其发送到多对话会话中的其他设备:
// Place an anchor for a virtual character. The model appears in renderer(_:didAdd:for:).
let anchor = ARAnchor(name: "panda", transform: hitTestResult.worldTransform)
sceneView.session.add(anchor: anchor)
// Send the anchor info to peers, so they can place the same content.
guard let data = try? NSKeyedArchiver.archivedData(withRootObject: anchor, requiringSecureCoding: true)
else { fatalError("can't encode anchor") }
self.multipeerSession.sendToAllPeers(data)
当其他对等方从多方会话接收数据时,它们会测试该数据是否包含归档的ARAnchor; 如果是的话,他们解码并将其添加到他们的会话:
if let unarchived = try? NSKeyedUnarchiver.unarchivedObject(of: ARAnchor.classForKeyedUnarchiver(), from: data),
let anchor = unarchived as? ARAnchor {
sceneView.session.add(anchor: anchor)
}
这只是将动态功能添加到共享AR体验中的一种策略 - 其他许多策略都是可能的。 选择一个适合您的应用的用户交互,渲染和网络要求。 例如,用户在AR世界空间投掷投射物的游戏可能会定义具有初始位置和速度等属性的自定义数据类型,然后使用Swift的Codable协议将该信息序列化为通过网络发送的二进制表示形式。
来自世界追踪AR会话的空间映射状态和一组锚。
世界地图中的会话状态包括ARKit对用户移动设备的物理空间的了解(ARKit用于确定设备的位置和方向)以及添加到会话中的任何ARAnchor对象(它可以表示检测到的实时空间)世界功能或由您的应用程序放置的虚拟内容)。
使用 getCurrentWorldMapWithCompletionHandler
之后保存会话的世界地图,可以将其分配给配置的 initialWorldMap
属性,并使用 runWithConfiguration:options:
以相同的空间感知和锚点启动另一个会话。
通过保存世界地图并使用它们开始新的会话,您的应用可以添加新的AR功能: