首页 iOS.& Swift Books 苹果通过教程增强现实

17
ECS.&协同体验 由克里斯语撰写

在Arkit的早期,它很快就会显然缺少一些重要的东西:在多个用户之间分享增强现实体验的能力。

Later versions of ARKit addressed the issue by introducing ARWorldMap. The map contains a space-mapping state along with a set of anchors from a world-tracking AR session. This map can be shared, allowing multiple users to experience persistent AR anchors within the same space.

With the assistance of a peer-to-peer network, multiple users can share an ARWorldMap in real time, creating a 合作经验. Using ARKit, the process is somewhat painful, requiring vast amounts of manual labor from a coding perspective.

Apple创建了一个梦幻般的Arkit示例项目,您可以探索。找到该项目: //apple.co/31ltm2u

但是,由于IOS 13,您已能够将RealityKit与Arkit配对,以自动化基于Arkit的应用程序通常需要的大多数手动努力。

在本章中,您将在经典TIC-TAC-TOE游戏中创建现代化,并提供基于真正的协作体验。新项目将从Apple的Arkit示例项目借用,但主要关注现实的实际侵略。

是时候去了!

探索项目

有一个初学项目等待着你 起动器/ XOXO 文件夹。该项目是一个基于基于Swift的基本应用程序,使用经典的样式故事板和我。

在Xcode中加载项目,因此您可以快速浏览内部的重要组成部分。

ViewController.swift.

打开 ViewController.swift.. By now, you’re very familiar with the inner workings of the ViewController.

MultipeerController.swift.

打开 MultipeerController.swift..

main.storyboard.

打开 main.storyboard. 并翻转方向景观。

info.plist.

打开 info.plist..

创建AR视图

既然你已经把基础推出了,你就会开始填写缺失的碎片,从AR视图开始。

设置播放器的颜色

将以下变量添加到 特性 section:

var playerColor = UIColor.blue
playerColor = UIColor.blue
playerColor = UIColor.red

发送消息

添加以下帮助函数 辅助功能:

func sendMessage(_ message: String) {
  DispatchQueue.main.async {
    self.message.text = message
  }
}

创建AR配置

You start all AR experiences by creating an ARConfiguration and running the AR session. Well, this AR experience is no different.

initARView()
func initARView() {
  arView.session.delegate = self
  arView.automaticallyConfigureSession = false
  let arConfiguration = ARWorldTrackingConfiguration()
  arConfiguration.planeDetection = [.horizontal]
  arConfiguration.environmentTexturing = .automatic
  arView.session.run(arConfiguration)
}

什么是ECS?

使用RealityKit框架创建AR体验的内容时,重要的是要注意,该框架运行基于CPU的实体 - 组件系统(ECS)来管理物理,动画,音频处理和网络同步。然后框架依赖于基于GPU的多线程渲染的金属。

预定义的实体

使用RealityKit,您可以通过根据您添加的各种组件轻松创建自己的自定义实体,并根据您添加的各种组件。

创建游戏板

既然你有一些背景,现在是时候创建一些自己的实体了,从游戏板开始。

var gridModelEntityX:ModelEntity?
var gridModelEntityY:ModelEntity?
var tileModelEntity:ModelEntity?

创建模型实体

在游戏板上仔细看,您可以看到整个电路板只由三个不同的形状构成:两个网格条和方形瓷砖。您将创建下一个形状。

initModelEntities()
func initModelEntities() {
  // 1
  gridModelEntityX = ModelEntity(
    mesh: .generateBox(size: SIMD3(x: 0.3, y: 0.01, z: 0.01)),
    materials: [SimpleMaterial(color: .white, isMetallic: false)]
  )
  // 2  
  gridModelEntityY = ModelEntity(
    mesh: .generateBox(size: SIMD3(x: 0.01, y: 0.01, z: 0.3)),
    materials: [SimpleMaterial(color: .white, isMetallic: false)]
  )
  // 3  
  tileModelEntity = ModelEntity(
    mesh: .generateBox(size: SIMD3(x: 0.07, y: 0.01, z: 0.07)),
    materials: [SimpleMaterial(color: .gray, isMetallic: true)]
  )
  // 4  
  tileModelEntity!.generateCollisionShapes(recursive: false)
}

克隆模型实体

现在您已经创建了三种主要形状,您将使用它们来构建游戏板。您将克隆原始实体,而不是重新创建每个元素,而不是重新创建每个元素。

func cloneModelEntity(_ modelEntity: ModelEntity,
  position: SIMD3<Float>) -> ModelEntity {
  let newModelEntity = modelEntity.clone(recursive: false)
  newModelEntity.position = position
  return newModelEntity
}

添加网格

现在,您将使用上面的帮助函数来创建网格。添加以下功能 模型实体功能:

func addGameBoardAnchor(transform: simd_float4x4) {
  // 1
  let arAnchor = ARAnchor(name: "XOXO Grid", transform: transform)
  let anchorEntity = AnchorEntity(anchor: arAnchor)
  // 2
  anchorEntity.addChild(cloneModelEntity(gridModelEntityY!,
    position: SIMD3(x: 0.05, y: 0, z: 0)))
  anchorEntity.addChild(cloneModelEntity(gridModelEntityY!,
    position: SIMD3(x: -0.05, y: 0, z: 0)))
  anchorEntity.addChild(cloneModelEntity(gridModelEntityX!,
    position: SIMD3(x: 0.0, y: 0, z: 0.05)))
  anchorEntity.addChild(cloneModelEntity(gridModelEntityX!,
    position: SIMD3(x: 0.0, y: 0, z: -0.05)))
}

添加瓷砖

随着网格的方式,您需要将瓷砖添加到游戏板。有九个插槽填充。

anchorEntity.addChild(cloneModelEntity(tileModelEntity!, 
  position: SIMD3(x: -0.1, y: 0, z: -0.1)))
anchorEntity.addChild(cloneModelEntity(tileModelEntity!, 
  position: SIMD3(x: 0, y: 0, z: -0.1)))
anchorEntity.addChild(cloneModelEntity(tileModelEntity!, 
  position: SIMD3(x: 0.1, y: 0, z: -0.1)))
anchorEntity.addChild(cloneModelEntity(tileModelEntity!, 
  position: SIMD3(x: -0.1, y: 0, z: 0)))
anchorEntity.addChild(cloneModelEntity(tileModelEntity!, 
  position: SIMD3(x: 0, y: 0, z: 0)))
anchorEntity.addChild(cloneModelEntity(tileModelEntity!, 
  position: SIMD3(x: 0.1, y: 0, z: 0)))
anchorEntity.addChild(cloneModelEntity(tileModelEntity!,
  position: SIMD3(x: -0.1, y: 0, z: 0.1)))
anchorEntity.addChild(cloneModelEntity(tileModelEntity!, 
  position: SIMD3(x: 0, y: 0, z: 0.1)))
anchorEntity.addChild(cloneModelEntity(tileModelEntity!, 
  position: SIMD3(x: 0.1, y: 0, z: 0.1)))

添加锚点

现在您现在已完成网格和所有瓷砖,下一步是将游戏板添加到AR场景。

// 1
anchorEntity.anchoring = AnchoringComponent(arAnchor)
// 2
arView.scene.addAnchor(anchorEntity)
// 3
arView.session.add(anchor: arAnchor)

放置内容

既然你的游戏板准备就可以放在场景中,你需要一些用户输入来了解放置它的位置。所有用户需要做的是点击水平表面,游戏板应出现在该位置。您的下一步是确保应用程序识别用户的点击。

创建一个点击手势

您将首先创建一个基本的敲击手势来处理触摸用户输入。

initGestures()
func initGestures() {
  // 1
  let tap = UITapGestureRecognizer(
    target: self, 
    action: #selector(handleTap))
  // 2
  self.arView.addGestureRecognizer(tap)
}

处理龙头手势

But hang, on there’s an error. You still need to define handleTap().

@objc func handleTap(recognizer: UITapGestureRecognizer?) {
}

获取触摸位置

在屏幕水龙头后,您将把射线投入场景,看看曲面在何处实际发生。这让你在你想要的地方的游戏中。

guard let touchLocation = 
  recognizer?.location(in: self.arView) else { return }

挖掘表面

现在,将实际的射线投入到场景中。

let results = self.arView.raycast(
  from: touchLocation,
  allowing: .estimatedPlane,
  alignment: .horizontal)
    
if let firstResult = results.first {
  self.addGameBoardAnchor(transform: firstResult.worldTransform)
} else {
  self.message.text = "[WARNING] No surface detected!"
}

点击瓷砖

好的,现在游戏板在现场可见,下一步是什么?嗯,当用户触摸瓷砖时,该瓷砖应该改变为播放器的颜色。

if let hitEntity = self.arView.entity(at: touchLocation) {
  let modelEntity = hitEntity as! ModelEntity
    modelEntity.model?.materials = [
      SimpleMaterial(color: self.playerColor,
      isMetallic: true)]
  return
}

协同体验

当多个人与他们自己的个人观点上分享一个增强的现实体验时,它被称为a 合作经验。为了实现这样的经验,所有设备应通过本地网络或蓝牙彼此连接。该设备共享AR世界地图,该地图本地化了同一空间内的每个设备。在活动协作会话期间,增强空间中的实体在所有其他设备上同步。

使用MCSession创建多对等网络

Thankfully, all the hard work is already done, thanks to MultipeerSession, which is part of your project. It acts as a basic wrapper class for MCSession, which is the network session class that connects multiple peers.

添加多对等连接

既然网络已准备就绪,您将创建多对待会话。

var multipeerSession: MultipeerSession?
var peerSessionIDs = [MCPeerID: String]()
var sessionIDObservation: NSKeyValueObservation?
initMultipeerSession()
func initMultipeerSession()
{
    multipeerSession = MultipeerSession(
      receivedDataHandler: receivedData,
      peerJoinedHandler: peerJoined,
      peerLeftHandler: peerLeft,
      peerDiscoveredHandler: peerDiscovered)
}

func receivedData(_ data: Data, from peer: MCPeerID) {
}
  
func peerDiscovered(_ peer: MCPeerID) -> Bool {
}
  
func peerJoined(_ peer: MCPeerID) {
}
  
func peerLeft(_ peer: MCPeerID) {
}

处理会话ID更改

当对等体连接或会话ID更改时,您需要通知连接当前对等体ID的对等体。

private func sendARSessionIDTo(peers: [MCPeerID]) {
  guard let multipeerSession = multipeerSession else { return }
  let idString = arView.session.identifier.uuidString
  let command = "SessionID:" + idString
  if let commandData = command.data(using: .utf8) {
    multipeerSession.sendToPeers(commandData,
      reliably: true,
      peers: peers)
  }
}
sessionIDObservation = observe(\.arView.session.identifier,
  options: [.new]) { object, change in
    print("Current SessionID: \(change.newValue!)")
    guard let multipeerSession = self.multipeerSession else 
    { return }
    self.sendARSessionIDTo(peers: multipeerSession.connectedPeers)
}

处理“同行”发现的事件

When the network session discovers a new peer, it triggers peerDiscovered(_:), asking it for permission to allow the new peer to connect.

guard let multipeerSession = multipeerSession else 
{ return false }

sendMessage("Peer discovered!")

if multipeerSession.connectedPeers.count > 2 {
  sendMessage("[WARNING] Max connections reached!")
  return false
} else {
  return true
}

处理“同行加入”活动

When the peer is allowed to connect, the network session will trigger peerJoined(_:).

sendMessage("Hold phones together...")
sendARSessionIDTo(peers: [peer])

处理“同行留下”事件

When a peer leaves, you need to update peerSessionIDs. To do this, add the following to peerLeft(_:):

sendMessage("Peer left!")
peerSessionIDs.removeValue(forKey: peer)

配置RealityKit以进行协作

嗯,这就是你需要做的只是创建一个多对等网络,但你还没有完成。您仍然需要配置RealityKit以进行协作。

启用合作

To use collaboration, you need to enable it when you create the AR configuration. Do this by adding the following line of code to initARView(), just before running the AR session:

arConfiguration.isCollaborationEnabled = true

设置同步服务

使用RealityKit时,必须将其所有实体及其组件与所有连接的对等体同步。

extension MultipeerSession {
  public var multipeerConnectivityService:
    MultipeerConnectivityService? {
      return try? MultipeerConnectivityService(
        session: self.session)
  }
}
// 1
guard let multipeerConnectivityService =
  multipeerSession!.multipeerConnectivityService else {
    fatalError("[FATAL ERROR] Unable to create Sync Service!")
  }
// 2    
arView.scene.synchronizationService = multipeerConnectivityService
self.message.text = "Waiting for peers..."

处理成功的连接

Now that everything’s in place, once a new peer successfully joins, RealityKit will create an ARParticipationAnchor for that peer.

func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {
  for anchor in anchors {
    if let participantAnchor = anchor as? ARParticipantAnchor {
      self.message.text = "Peer connected!"
      let anchorEntity = AnchorEntity(anchor: participantAnchor)
      arView.scene.addAnchor(anchorEntity)
    }
  }
}

请求网络权限

哦,你还没有完成。您必须做的最后一件事以及请求网络权限。

管理所有权

在合作经验期间,当您创建一个实体时,您将成为该实体的所有者。如果另一种同行尝试修改属于您的实体,他们将被阻止。

启用自动所有权

为了保持简单,当另一个对等方请求属于您的实体的所有权时,您将自动将所有权转移到该同行。

anchorEntity.synchronization?.ownershipTransferMode = .autoAccept

要求所有权

现在,您需要确保您在所有权上点击瓷砖时请求。

if let hitEntity = self.arView.entity(at: touchLocation) {
  if hitEntity.isOwner {
    let modelEntity = hitEntity as! ModelEntity
    modelEntity.model?.materials = [
      SimpleMaterial(color: self.playerColor,
        isMetallic: true)]
  } else {
    hitEntity.requestOwnership { result in
      if result == .granted {
        let modelEntity = hitEntity as! ModelEntity
        modelEntity.model?.materials = [
          SimpleMaterial(color: self.playerColor, 
            isMetallic: true)]
      }
    }
  }  
  return
}

去除锚点

作为最后一个触摸,当你和朋友一起玩了几场比赛时,清除播放领域会很高兴,以便你可以玩更多。

func removeAnchors() {
  guard let frame = arView.session.currentFrame else { return }
  for anchor in frame.anchors {
    arView.session.remove(anchor: anchor)
  }
  sendMessage("All anchors removed!")
}
removeAnchors()

关键点

恭喜,您已达到本章的末尾和部分。您可以在最终状态下找到项目的副本 最终/ xoxo..

有一个技术问题?想报告一个错误吗? 您可以向官方书籍论坛中的书籍作者提出问题和报告错误 这里.

有反馈分享在线阅读体验吗? 如果您有关于UI,UX,突出显示或我们在线阅读器的其他功能的反馈,您可以将其发送到设计团队,其中表格如下所示:

© 2021 Razeware LLC

您可以免费读取,本章的部分显示为 混淆了 文本。解锁这本书,以及我们整个书籍和视频目录,带有Raywenderlich.com的专业订阅。

现在解锁

要突出或记笔记,您需要在订阅中拥有这本书或自行购买。