概述
学AR也有一小段时间了,今天给大家分享一下如何让两部以上的设备查看到相同的增强现实景象,在这里作者就以苹果官方的示例来进行解析,一定要把代码下载了和文章对照着看,不然会懵。
官方项目代码地址:https://developer.apple.com/documentation/arkit/creating_a_collaborative_session
项目演示
AR联机项目演示
如果这里看不了直接进主页找视频。
需求分析
1.根据视频中所展示的,不同的AR设备处在一个相同环境(可以不连接同一个局域网)
2.当画面出现了其他的AR设备时,在其他的AR设备上会展现一个坐标球。
3.在AR设备中点击可产生一个具有镜面反射效果的一个金属方块实体,并且该实体能实时被其他的AR设备捕捉,不同的AR设备点击产生的实体材质不同。
实现多用户通信需要用到ARKit特有的MultipeerConnectivity网络协议来进行网络通信传输数据,虽然这个示例使用RealityKit绘制图形,但它不使用RealityKit的机制进行网络实体同步。相反,它仅在演示ARKit协作会话所必需的时间内使用RealityKit作为渲染器。
代码解读
本文就只针对核心代码和逻辑步骤来讲解代码,部分基础代码(如定义等)在此不过多赘述,代码中冒出来的函数也先不用纠结,只用知道功能就行不用管怎么实现,最后相信理清所有思路之后再回去看这些代码,整个项目的代码都会弄清楚的。
既然要实现本机和其他设备的数据同步,那么就需要实现正向通信和反向通信(只是个名词,看字面意思就行)通常使用协议MultipeerConnectivity来实现网络传输。
其中我们应当使用三次握手原则,有点类似于ios设备的airdrop(隔空投送)但更先进一些,只需要打开蓝牙或wifi其中一个就行。具体实现就像是存在A和B两个设备(1)A先广播,B进行搜索(2)B搜到之后,会向A发送一个邀请(请求),建立连接(3)A接受之后,会向B发送一个会话(session),若成功则可以互传数据
tips:代码中MCPeerID为自己设备名字,MCSession用于传输数据(文字,文件,图片等)
A先进行广播,B进行搜索,那么就创建一个MCNearbyServiceAdvertiser用来广播,start开始广播,创建一个MCNearbyServiceBrowser用来搜索,start开始搜索,既然是有关通讯的操作,那么就需要写在MutipeerSession文件中
// 1-A先广播, B搜索
serviceAdvertiser = MCNearbyServiceAdvertiser(peer: myPeerID, discoveryInfo: nil, serviceType: MultipeerSession.serviceType)
serviceAdvertiser.delegate = self
serviceAdvertiser.startAdvertisingPeer()
//serviceType的值为“ar-collab”,在这里使用的固定值
serviceBrowser = MCNearbyServiceBrowser(peer: myPeerID, serviceType: MultipeerSession.serviceType)
serviceBrowser.delegate = self
serviceBrowser.startBrowsingForPeers() //调用foundPeer的delegate函数
B搜到之后, 会向A发送邀请(请求), 建立连接
在B的delegate中判断是否搜索到,若搜索到就像A发送请求
extension MultipeerSession: MCNearbyServiceBrowserDelegate {
/// - Tag: FoundPeer
public func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String: String]?) {
// 2-B搜到之后, 会向A发送邀请(请求), 建立连接
let accepted = peerDiscoveredHandler(peerID)
if accepted {
browser.invitePeer(peerID, to: session, withContext: nil, timeout: 10)
}
}
public func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) {
// 此程序对未邀请的玩家不做处理
}
}
A接收B的加入请求,发送一个会话(session), 可开始互传数据
extension MultipeerSession: MCNearbyServiceAdvertiserDelegate {
/// - Tag: AcceptInvite
func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID,
withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void) {
// 3-接收别的玩家的加入请求,发送一个会话(session), 可开始互传数据
invitationHandler(true, self.session)
}
}
在MCSession的delegate里就是用于传数据的在这里实现正向通信和反向通信,若连接上传一个数据,否则执行别的操作,若搜到别人的手机也要接受别人的数据,具体操作通过闭包实现,闭包的定义在viewcontroller中,之所以用闭包是因为闭包可以携带参数值到处跑。
设置好MCSession就可以搞Viewcontroller了,在viewWillappear或viewDidappear中自定义配置configuration,并启用短距离通讯协议,这里就属于正常配置,不懂的可以看作者另一篇文章或查一查官网ARKit入门的项目https://blog.csdn.net/yueliangmua/article/details/127318214?spm=1001.2014.3001.5501
arView.session.delegate = self
// 使用自定义配置
arView.automaticallyConfigureSession = false
setupCoachingOverlay()
// 在世界追踪session中开启合作功能
// 合作session中,ARKit会定期提供数据(环境+别的玩家的锚点)供联机玩家共享
// 这些数据需通过网络协议发送出去--这里使用短距离通信协议MultipeerConnectivity
configuration.isCollaborationEnabled = true
// 环境纹理设为自动(基于图像照明算法,真实地反射周围环境的光线,使虚拟模型更逼真)
configuration.environmentTexturing = .automatic
// 防止屏幕变暗让体验变差--禁用idle timer避免因屏幕无交互后的系统自动睡眠
UIApplication.shared.isIdleTimerDisabled = true
arView.session.run(configuration)
这里需要再设置一个观察者使用观察者模式实时检测SessionID的变化,一旦改变就会通知其他连接中的玩家追踪最新ARAnchor,通过sendARSessionIDTo把id传给其他玩家。(这里不懂先往后看)
// 使用观察者模式(key-value observation)实时监测SessionID的变化--类似属性观察者(willset,didset)
sessionIDObservation = observe(.arView.session.identifier, options: [.new]) { vc, change in
print("SessionID变成了: (change.newValue!)")
// 一旦改变SessionID后,告诉其他联机玩家,以便他们能够追踪到最新的ARAnchor
guard let multipeerSession = self.multipeerSession else { return }
self.sendARSessionIDTo(peers: multipeerSession.connectedPeers)
}
设置isCollaborationEnabled=true了之后,短距离通信就开启了,遵循了ARSessionDelegate之后会每隔一段时间调用一个方法didOutputCollaborationData,而网络传输数据一般是要进行编码的,由于这个data是ios内置的一种数据形式没有遵循从codable协议所以不能转json,需要使用原始的编码形式,并且因通信协议发送数据时要对传输数据的可靠性进行检测故用ARKit提供的proiority属性进行联动,皆是固定用法。
// 一旦开启合作功能,将定期调用此函数
// 获取各玩家的现实环境信息,以便所有人能看到同样的虚拟物体--data参数,CollaborationData类型
func session(_ session: ARSession, didOutputCollaborationData data: ARSession.CollaborationData) {
guard let multipeerSession = multipeerSession else { return }
if !multipeerSession.connectedPeers.isEmpty {
//数据需网络传输,故首先得编码
//因CollaborationData未遵循Codable协议,遵循的是旧的NSCoding协议,故需用旧方法
guard let encodedData = try? NSKeyedArchiver.archivedData(withRootObject: data, requiringSecureCoding: true)
else { fatalError("collaboration data编码失败") }
// 因MultipeerConnectivity通信协议发送数据时需指定数据是否可靠,可通过ARKit提供的priority属性进行联动
let dataIsCritical = data.priority == .critical
multipeerSession.sendToAllPeers(encodedData, reliably: dataIsCritical)
} else {
print("暂未找到新玩家,ARKit之后会再次尝试")
}
}
MC网络连接成功后-新玩家加入
给B发送本机(A)的MCPeerID和SessionID数据--MCSession中处理
func peerJoined(_ peer: MCPeerID) {
messageLabel.displayMessage("一个新玩家想加入游戏,请将双方iPhone并排靠近以面向同一个现实环境", duration: 12.0)
// 向B提供你的SessionID,以便他们可以跟踪你的锚点
sendARSessionIDTo(peers: [peer])
}
ARKit为了知道两个玩家之间的相对位置,它必须要先合并各自的世界地图(相机捕捉到的现实环境)
合并成功(即所有玩家一开始需把iPhone相机面向同一个地方)后可共享玩家们各自的位置以及创建的锚点
通过didAddanchors的delegate方法,提供自己的ARParticipantAnchor(自己的iPhone锚),等合并成功后对方即可看到我的坐标球
//2-collaboration开启后会形成ARParticipantAnchor以提供自己的世界位置+玩家们合并世界地图成功后可放置立方体模型
func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {
for anchor in anchors {
//检测是否是ARParticipantAnchor,若是则判断联机成功
if let participantAnchor = anchor as? ARParticipantAnchor {
messageLabel.displayMessage("和别的玩家联机成功", duration: 6.0)
//将虚拟物体(彩色坐标球体)放置在别的玩家iPhone的位置上以显示别的玩家--实时更新位置和方向
//先添加一个容器anchorEntity
let anchorEntity = AnchorEntity(anchor: participantAnchor)
//渲染设备的坐标系
let coordinateSystem = MeshResource.generateCoordinateSystemAxes()
anchorEntity.addChild(coordinateSystem)
//渲染球体,根据sessionid随机生成颜色
let color = participantAnchor.sessionIdentifier?.toRandomColor() ?? .white
let coloredSphere = ModelEntity(
mesh: MeshResource.generateSphere(radius: 0.03),
materials: [SimpleMaterial(color: color, isMetallic: true)]
)
anchorEntity.addChild(coloredSphere)
arView.scene.addAnchor(anchorEntity)
} else if anchor.name == "Anchor for object placement" {
//当成功合并两个玩家的世界数据后,其中一玩家点击屏幕添加虚拟立方体的同时,立方体也同时加进了另一个玩家的现实环境中了
let anchorEntity = AnchorEntity(anchor: anchor)
let boxLength: Float = 0.05
let color = anchor.sessionIdentifier?.toRandomColor() ?? .white
let coloredCube = ModelEntity(
mesh: MeshResource.generateBox(size: boxLength),
materials: [SimpleMaterial(color: color, isMetallic: true)]
)
//因为中心点(锚点)位于平面,所以正方体有一半是嵌入到里面的,所以需要向上移动正方体高度的一半
coloredCube.position = [0, boxLength / 2, 0]
// 放入立方体模型
anchorEntity.addChild(coloredCube)
arView.scene.addAnchor(anchorEntity)
}
}
至此发送方任务完成,那么接收方如何接收我添加的这些数据呢?接收方任务开始,接收方
此处会接收两种数据:1.系统调用didOutputCollaborationData之后,我们通过MC网络发送的CollaborationData数据 2.通过MC网络手动send的数据(见peerJoined函数)--“SessionID:xxx”,若没有colloaboration则有可能换了一个sessionid即接收第二个数据(因为colloaboration的数据只有连接成功的sessionid才有),所以更新一下sessionid,并把旧的数据删除掉。那么接收方的任务也完成。
func receivedData(_ data: Data, from peer: MCPeerID) {
if let collaborationData = try? NSKeyedUnarchiver.unarchivedObject(ofClass: ARSession.CollaborationData.self, from: data) {
// 接收别的玩家的CollaborationData后需调用此方法以更新session
arView.session.update(with: collaborationData)
return
}
let sessionIDCommandString = "SessionID:"
// data解码后变成的字符串若以“SessionID:”为开头,则取出“SessionID:”后面的字串(详见playground)
if let commandString = String(data: data, encoding: .utf8), commandString.starts(with: sessionIDCommandString) {
let newSessionID = String(
commandString[
commandString.index(commandString.startIndex,offsetBy: sessionIDCommandString.count)...
]
)
// 如果当前过来联机的玩家之前使用的是不同的sessionID,则删除所有相关anchor
if let oldSessionID = peerSessionIDs[peer] {
removeAllAnchorsOriginatingFromARSessionWithID(oldSessionID)
}
peerSessionIDs[peer] = newSessionID
}
}
至此项目整体逻辑就到此为止了,接下来补充一些未解释的坑。
1.玩家断开连接后删除该玩家创造的所有锚点
//4-连接断开(玩家离开)
func peerLeft(_ peer: MCPeerID) {
messageLabel.displayMessage("一个玩家离开了游戏")
// 删除和刚刚离开的玩家相关的所有锚点
if let sessionID = peerSessionIDs[peer] {
removeAllAnchorsOriginatingFromARSessionWithID(sessionID)
peerSessionIDs.removeValue(forKey: peer)
}
}
2.点击事件生成正方体依赖的锚点
@objc func handleTap(recognizer: UITapGestureRecognizer) {
let location = recognizer.location(in: arView)
// 在用户触摸位置的水平面上找到位置数据
// raycast为hit-testing的升级版
let results = arView.raycast(from: location, allowing: .estimatedPlane, alignment: .horizontal)
if let firstResult = results.first {
// 在触摸位置添加一个带有name的ARAnchor,之后将在“session_:didAdd :)”中对应该name
let anchor = ARAnchor(name: "Anchor for object placement", transform: firstResult.worldTransform)
arView.session.add(anchor: anchor)
} else {
messageLabel.displayMessage("没有找到平面无法放置物体n请寻找平坦表面", duration: 6.0)
}
}
其他的一些功能实现就在源代码里找就行,对照着看一看就能懂,指导用户操作的辅助功能也不在此讲解了。
总结一下
1.通过MultipeerConnectivity协议进行多用户的互联(三次握手)
2.启用短距离通讯协议在didOutputCollaborationData中实现数据的传递
3.在didAdd中判断是否连接成功,并多个设备同时更新和渲染画面
4.前三条实现正向通信和反向通信的具体操作均需要在viewcontroller中实现
最后
以上就是搞怪大神为你收集整理的AR联机初探+官方项目代码解析项目演示需求分析代码解读总结一下的全部内容,希望文章能够帮你解决AR联机初探+官方项目代码解析项目演示需求分析代码解读总结一下所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复