38-构建多人 AR 游戏

1,095 阅读10分钟

ARKit系列文章目录

2019年WWDC的《 Session 610 - Building Collaborative AR Experiences 》 主要内容速览:

  • 多人协作会话(Collaborative Session)
  • 多用户 AR 的 ARAnchor 最佳实践
  • SwiftStrike : 一个新的多人 AR 游戏

多人协作会话(Collaborative Session)

在介绍这个之前,先来回顾一下去年 ARKit 2 中推出的世界地图保存和加载。去年的 ARKit 2 需要一个主用户扫瞄建立世界地图,然后自己通过代码经过网络分享给其他用户。在此之后不管是主用户 User1 还是其他用户 User2 ,所有人新扫瞄到的特征点,新建立的世界地图部分,都只能在自己本地使用,无法共享出去。

而今年的 Collaborative Session 就是为了解决这些问题。它可以自动建立网络连接,自动管理其他用户的加入,自动共享新特征点,新地图扩展。

  • 实时多用户 AR 体验
  • 持续共享 ARAnchors 和地图数据
  • 无需区分主用户与其他用户

比如下面的 Demo,一开始左边和右边的两个不同用户,看到了不同场景,各自添加虚拟物体后,也只能看到自己添加的物体。 互相移动后,看到对方的场景, Collaborative Session 会自动同步世界地图和锚点信息,以及别人添加的 3D 虚拟物体。

这样两个用户的世界地图就合并成了同一个,两个用户也处于同一个网络中,后续再进行任何添加操作,对方也能自动看到。

这其中的原理,如下图所示:一开始,两个用户建立了各自的世界坐标和世界地图,并通过网络进行了交流,没有发现共同之处。当一个用户扫瞄到另一个用户扫瞄过的地方时,世界地图的内容就自动进行了合并。但是,每个用户自己地图的坐标原点仍然在原处,没有改变,只是增加了地图范围和其他用户的锚点信息,并持续更新

在 ARKit 3 中,想要用上 Collaborative Session ,首先,你必须让这些设备处于同一个网络层中(MultipeerConnectivity 或者其他网络技术)。然后你需要启用自己的 Collaboration,并传输相关数据到其他用户那里

在 ARKit 3中,只需要下面的代码就可以了。

// Collaborative Session
 // Create world tracking configuration
 let config = ARWorldTrackingConfiguration()
 // Enable collaborative session
 config.isCollaborationEnabled = true
 // Run the configuration
  session.run(config)

如果你用的不是 RealityKit,那还需要实现另外两个方法:

// ARSession delegate function to output ARCollaborationData
func session(_ session: ARSession, 

didOutputCollaborationData data: ARSession.CollaborationData) {
 // Transmit Data representation of the data to all other participants using MPC
 do {
 try self.mpcSession.send(data.data, toPeers: self.peerIds, with: .reliable)
 } catch {
 // Re-transmit the data if failed
 }
}


// MPC delegate function when receiving collaboration data from other users
func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
 // Pass the received data to ARSession
 self.arSession.update(data: ARSession.CollaborationData(data: data))
}

Collaborative Session 中的 ARAnchor

ARAnchor 具有:

  • 同步的生命周期:当你添加或删除锚点时,其他用户也会同步添加或删除
  • 带有会话标识(Session identifier):用来区分是谁创建了锚点
  • ARAnchor 的子类不会被共享:只共享用户创建的锚点,不包括 ARImageAnchor 等,也不包括用户自己实现的子类,比如用来携带某些数据等

那么在代码中,如何使用 ARAnchor?我们需要区分是谁在操作锚点:

// ARAnchor in collaborative session
// ARSession delegate function when an ARAnchor is added.
func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {
 for anchor in anchors {
  // Use session identifier to determine creator of the ARAnchor
  if anchor.sessionIdentifier == session.identifier {
   // Self-placed ARanchor
  } else {
   // ARAnchor from another participant
  }
 }
}

// ARSession delegate function when an ARAnchor is removed.
func session(_ session: ARSession, didRemove anchors: [ARAnchor]) {
 for anchor in anchors {
  // Use session identifier to determine creator of the ARAnchor
  if anchor.sessionIdentifier == session.identifier {
   // Self-placed ARanchor
  } else {
   // ARAnchor from another participant
  }
 }
}

ARParticipantAnchor

为了解决在多人 AR 中,如何显示其他用户的问题,我们今年引入了ARParticipantAnchor类型:

  • 代表其他用户的位置
  • 以高帧率更新
  • 在本地化其他用户的地图后,才会创建(这意味着,我们可以用它来确定多人共享是否已经开始了)

关于本地化其他用户,我们有一些建议

多人共享 AR 能够开启的前提,是大家看到了同一块区域。并且,如果你想要加快识别速度,最好是让多人处于相近的视角中。 还有就是,保持地图追踪状态ARFrame.WorldMappingStatus.mapped。它会让追踪到的 3D 地图数据不断更新,范围更大,准确度也更高。以便后面的其他用户匹配到同一个区域。

多用户 AR 的 ARAnchor 最佳实践

首先我们需要先了解一下 ARWorldMap:

  • 包含了压缩后的 3D 地形(Landmark)
  • 包含了相机姿态(特征点和地形被观察到时的相机姿态)

地形数据是根据相机姿态,分成不同组的,如下图 View 0,View 1,View 2 等 同时还要注意的是,ARAnchor 的绝对位置是依赖于世界坐标系的,即相对图中 World 原点的。同时这个位置又是相对世界地图的,即相对于整个地形中特征点的相对位置。

了解了这些原理后,我们就可以采取措施,优化 ARAnchor 的使用:

  • 实现 anchor 更新的代理方法:这样随着相机的移动,地形特征点也会发生校准移动,Anchor 的位置也会随着离它最近的 View 里面的地形数据而调整
  • 将 3D 内容放置靠近在 ARAnchor 的地方:如果离得太远,当 Anchor 随着地形数据更新时,3D 内容的位置会发生很大的移动
  • 对于每个独立的 3D内容,使用独立的不同的锚点:除非几个 3D 物体本身离得不远,且需要保持严格的相对距离,这样才能共用一个锚点

SwiftStrike : 一个新的多人 AR 游戏

这是今年的一个新的多人 AR 游戏,是受到去年的 SwiftShot 多人游戏启发而形成的。

下面主要讲解一下,RealityKit 中的网络、物理效果、游戏性设计等方面。

RealityKit 网络

RealityKit 中的网络特点:

  • 基于实体-组件架构
  • 所有数据同步,包括物理效果!
  • 自定义逻辑组件
  • 使用 MultipeerConnectivity 作为网络层
  • 创建一个网络会话,移交给 ARView 详情参考Building Apps with RealityKit

不同角色

虽然从网络角度,所有接入的用户都是一样的。但在 SwiftStrike 游戏中,我们需要将第一个设备作为“host”,用它来控制游戏状态,物理模拟等。其它设备都是参与者。

在 RealityKit 中自定义组件

定义你自己的组件来储存应用程序的状态 在初始化 ARView 之前就注册组件 实现Codable以启用同步功能

用例--启动游戏

匹配物体追踪,是否有足够玩家准备开始游戏 状态是保存在 host 上,同步到 client 上 组件维护了全部日志

如下图,当两个玩家进入指定位置后,游戏才会触发开始,球体才会出现。

代码如下:

// Custom component for game start
struct MatchStateComponent: Component, Codable {
 struct Transition: Codable {
  var date: Date
  var state: MatchOutput
 }
 var transitions = [Transition]()
}

// Registering the component, in application(_:didFinishLaunchingWithOptions:)
MatchStateComponent.self.registerComponent()
// On client(这里用到新的 Combine 框架)
class MatchObserver {
 var matchOutputEvents: AnyPublisher<MatchOutput, Never>
}

我们这个项目和去年的 SwiftShot 相比非常类似,因此也从去年的项目中借用了大量代码。但是如下图所示,因为 RealityKit 能完成组件状态和物理效果的自动同步,因此大量的网络同步类代码就不需要了;同时,因为物理效果自动同步,游戏状态和信息也只在游戏开始时同步一次,这样一来所有的 BitStream 编码相关代码也就不需要了。 最终,我们通过使用 RealityKit 减少了 15000 行代码。

物理模拟

物理状态同步是由 RealityKit 处理的。通过组件来配置物理属性:

  • 刚体形体(Rigid body)
  • 碰撞掩码
  • 质量,摩擦,回弹

在 SwiftStrike 游戏中,Host 设备拥有着 simulation 的所有权。Client 设备则根据更新结果进行插值显示。

比如,游戏中保龄球瓶的设计,外观非常复杂,有很多曲线和曲面连接,下面是模型的线框图 但是,在物理引擎中进行物理效果模拟时,这样的形状就太复杂了,严重消耗了物理引擎的计算能力。所以,我们用内置的基础几何形状进行组合来形成物体的物理形状,如下图,使用球体,圆锥体等组合

这样,外观看上去是精美的球瓶,在处理碰撞等效果时,又不会有复杂运算。

游戏设计

SwiftStrike 的游戏设计体现在:

  • 人体遮挡预先设计
  • 可现场体验
  • 控制机制

当了解了今年 ARKit 3 中的人体遮挡后,我们在设计游戏时,就想到了充分利用这些特性,来做一个完整的游戏。 游戏从一开场,就启用了人体遮挡技术,如下图:

人体遮挡技术的运用,也让游戏不再像以前一样,需要小心避免有人出现在手机和游戏场景之间,去年的 SwiftShot 就是因此只能在桌子上玩。今年的 SwiftStrike 则可以直接在地面上玩,它是全尺寸的游戏。 下面图中,就是我们的进行游戏的场景,木质纹理地板有助于 AR 平面的识别,地面上的贴纸则是图片锚点,用来标定球出现的位置。

下面讲控制机制: 手机就是你的控制器,更快的移动意味着更大的推力。场景中的物理形体设定:

  • 球会弹开人体
  • 球瓶和球互弹
  • 球瓶不会和人体碰撞

要完成这些功能,我们需要给游戏玩家设置一个不可见的圆柱体,让这个圆柱体能够和球、球瓶互相碰撞,但圆柱体之间不会互相碰撞。

但是,还有一个问题:如何处理其他玩家的输入呢?也就是说,物理模拟是在 Host 用户手机上进行的,那么当其他玩家与球发生碰撞时, Host 该如何获取并处理这些事件呢?

这就是下面要说的玩家操控杆的所有权问题。

玩家操控杆的所有权(Ownership of Player Paddle)

其中的原理如下图所示:当 Host 开启游戏时,会创建一个AnchorEntity,并拥有对此的所有权,AR 中的所有实体都会是它的子元素。而当另外的玩家 Client加入游戏时,会创建一个PlayerLocationEntity并拥有所有权,这个 entity 会根据玩家 Client 的位置自动更新。 我们还需要创建另一个 Client 的子元素:PaddleEntity。它的父实体就是玩家 Client,但它的所有权却是归 Host 所有的。

这样,就完美解决所有的冲突。尽管物理效果是在 Host 上处理的,但是每个人都能看到正确的游戏显示

最终完成的游戏效果如下:

黑暗模式

今年的 iOS 引入了黑暗模式,我们在制作游戏时,也考虑到了充分利用这些特性。给游戏也加入了黑暗模式,效果如下:

SwiftStrike 这个游戏代码的下载链接叫Creating a Game with Reality Composer,本文末尾已给出

参考资料

相关视频,WWDC2019