巧用 ARKit 和 SpriteKit 从零开始作 AR 游戏

巧用 ARKit 和 SpriteKit 从零开始作 AR 游戏

这篇文章隶属于 Pusher 特邀做者计划php

ARKit 是一个全新的苹果框架,它将设备运动追踪,相机捕获和场景处理整合到了一块儿,能够用来构建加强现实(Augmented Reality, AR) 的体验。html

在使用 ARKit 的时候,你有三种选项来建立你的 AR 世界:前端

  • SceneKit,渲染 3D 的叠加内容。
  • SpriteKit,渲染 2D 的叠加内容。
  • Metal,本身为 AR 体验构建的视图

在这个教程里,咱们将经过建立一个游戏来学习 ARKit 和 SpriteKit 的基础,游戏是受 Pokemon Go 的启发,添加了幽灵元素,看下下面这个视频吧:node

每几秒钟,就会有一个小幽灵随机出如今场景里,同时在屏幕的左下角会有一个计数器不停在增长。当你点击幽灵的时候,它会播放一个音效同时淡出并且计数器也会减少。react

项目的代码已经放在了 GitHub 上了。android

让咱们首先检查一下开发和运行这个项目的须要哪些东西。ios

你将会须要的

首先,为了完整的 AR 体验,ARKit 要求一个带有 A9 或者更新的处理器的 iOS 设备。换句话说,你至少须要一台 iPhone6s 或者有更高处理器的设备,好比 iPhoneSE,任何版本的 iPad Pro,或者 2017 版的 iPad。git

ARKit 是 iOS 11 的一个特性,因此你必须先装上这个版本的 SDK,并用 Xcode 9 来开发。在写这篇文章的时候,iOS 11 和 Xcode 9 仍然是在测试版本,因此你要先加入到苹果开发者计划,不过苹果如今也向公众发布了免费的开发者帐号。你能够在这里找到更多关于安装 iOS 11 beta 的信息和这里找到关于安装 Xcode beta 的信息。github

为了不以后版本的改动,这个应用的教程是经过 Xcode beta 2 来构建的。
在这个游戏中,咱们须要表示幽灵的图片和它被移除时的音效。OpenGameArt.org 是一个很是棒的获取免费游戏资源的网站。我选了这个幽灵图片 和这个幽灵音效,固然你也能够用任何你想要用的文件。编程

新建项目

打开 Xcode 9 而且新建一个 AR 应用:

输入项目的信息,选择 Swift 做为开发语言并把 SpriteKit 做为内容技术,接着建立项目:

目前 AR 不可以在 iOS 模拟器上测试,因此咱们须要在真机上进行测试。为此,咱们须要开发者帐号来注册咱们的应用。若是暂时没有的话,把你的开发帐号添加到 Xcode 上而且选择你的团队来注册你的应用:

若是你没有一个付过费的开发者帐号的话,你会有一些限制,好比你每七天只可以建立 10 个 App ID 并且你不可以在你的设备上安装超过 3 个以上的应用。

在你第一次在你的设备上安装应用的时候,你可能会被要求信任设备上的证书,就跟着下面的指导:

就像这样,当应用运行的时候,你会被请求给予摄像头权限:

以后,在你触摸屏幕的时候,一个新的精灵会被加到场景上去,而且根据摄像头的角度来调整位置。

如今这个项目已经搭建完成了,让咱们来看下代码吧。

SpriteKit 如何和 ARKit 一块儿工做

若是你打开 Main.storyboard,你会发现有个 ARSKView 填满了整个屏幕:

这个视图未来自设备摄像头的实时视频,渲染为场景的背景,将 2D 的图片(以 SpriteKit 的节点)加到 3D 的空间中( 以 ARAnchor 对象)。当你移动设备的时候,这个视图会根据锚点( ARAnchor 对象)自动旋转和缩放这个图像( SpriteKit 节点),因此他们看上去就像是经过摄像头跟踪的真实的世界。

这个界面是经过 ViewController.swift 这个类来管理的。首先,在 viewDidLoad 方法中,它打开了界面的一些调试选项,而后经过这个自动生成的场景 Scene.sks 来建立 SpriteKit 场景:

override func viewDidLoad() {
      super.viewDidLoad()

      // 设置视图的代理
      sceneView.delegate = self

      // 展现数据,好比 fps 和节点数
      sceneView.showsFPS = true
      sceneView.showsNodeCount = true

      // 从 'Scene.sks' 加载 SKScene
      if let scene = SKScene(fileNamed: "Scene") {
        sceneView.presentScene(scene)
      }
    }复制代码

接着,viewWillAppear 方法经过 ARWorldTrackingSessionConfiguration 类来配置这个会话。这个会话( ARSession 对象)负责管理建立 AR 体验所须要的运动追踪和图像处理:

override func viewWillAppear(_ animated: Bool) {
      super.viewWillAppear(animated)

      // 建立会话配置
      let configuration = ARWorldTrackingSessionConfiguration()

      // 运行视图的会话
      sceneView.session.run(configuration)
    }复制代码

你能够用 ARWorldTrackingSessionConfiguration 类来配置该会话经过六个自由度(6DOF)中追踪物体的移动。三个旋转角度:

  • Roll,在 X-轴 的旋转角度
  • Pitch,在 Y-轴 的旋转角度
  • Yaw,在 Z-轴 的旋转角度

和三个平移值:

  • Surging,在 X-轴 上向前向后移动。
  • Swaying,在 Y-轴 上左右移动。
  • Heaving,在 Z-轴 上上下移动。

或者,你也能够用 ARSessionConfiguration ,它提供了 3 个自由度,支持低性能设备的简单运动追踪。

往下几行,你会发现这个方法 view(_ view: ARSKView, nodeFor anchor: ARAnchor) -> SKNode? 。当一个锚点被添加的时候,这个方法为即将添加到场景上的锚点提供了一个自定义节点。在当前的状况下,它会返回一个 SKLabelNode 来展现这个面向用户的 emoji :

func view(_ view: ARSKView, nodeFor anchor: ARAnchor) -> SKNode? {
      // 为加上视图会话的锚点增长和配置节点
      let labelNode = SKLabelNode(text: "👾")
      labelNode.horizontalAlignmentMode = .center
      labelNode.verticalAlignmentMode = .center
      return labelNode;
    }复制代码

可是这个锚点何时建立的呢?

它是在 Scene.swift 文件中完成的,在这个管理 Sprite 场景(Scene.sks)的类中,特别地,这个方法中:

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
      guard let sceneView = self.view as? ARSKView else {
        return
      }

      // 经过摄像头当前的位置建立锚点
      if let currentFrame = sceneView.session.currentFrame {
        // 建立一个往摄像头前面平移 0.2 米的转换
        var translation = matrix_identity_float4x4
        translation.columns.3.z = -0.2
        let transform = simd_mul(currentFrame.camera.transform, translation)

        // 在会话上添加一个锚点
        let anchor = ARAnchor(transform: transform)
        sceneView.session.add(anchor: anchor)
      }
    }复制代码

就像你从注释中能够看到的,它经过摄像头当前的位置建立了一个锚点,而后新建了一个矩阵来把锚点定位在摄像头前 0.2m 处,并把它加到场景中。

ARAnchor 使用一个 4×4 的矩阵 来表明和它相对应的对象在一个三维空间中的位置,角度或者方向,和缩放。

在 3D 编程的世界里,矩阵用来表明图形化的转换好比平移,缩放,旋转和投影。经过矩阵的乘法,多个转换能够链接成一个独立的变换矩阵。

这是一篇关于转换背后的数学很好的博文。一样的,在核心动画指南中关于操做 3D 界面中层级一章 中你也能够找到一些经常使用转换的矩阵配置。

回到代码中,咱们以一个特殊的矩阵开始(matrix_identity_float4x4):

1.0   0.0   0.0   0.0  // 这行表明 X
0.0   1.0   0.0   0.0  // 这行表明 Y
0.0   0.0   1.0   0.0  // 这行表明 Z
0.0   0.0   0.0   1.0  // 这行表明 W复制代码

若是你想知道 W 是什么:

若是 w == 1,那么这个向量 (x, y, z, 1) 是空间中的一个位置。

若是 w == 0,那么这个向量 (x, y, z, 0) 是一个方向。

www.opengl-tutorial.org/beginners-t…

接着,Z-轴列的第三个值改成了 -0.2 表明着在这个轴上有平移(负的 z 值表明着把对象放置到摄像头以前)。
若是你这个时候打印了平移矩阵值的话,你会看见它打印了一个向量数组,每一个向量表明了一列。

[ [1.0, 0.0,  0.0, 0.0 ],
  [0.0, 1.0,  0.0, 0.0 ],
  [0.0, 0.0,  1.0, 0.0 ],
  [0.0, 0.0, -0.2, 1.0 ]
]复制代码

这样子可能看起来更简单一点:

0     1     2     3    // 列号
1.0   0.0   0.0   0.0  // 这一行表明着 X
0.0   1.0   0.0   0.0  // 这一行表明着 Y
0.0   0.0   1.0  -0.2  // 这一行表明着 Z
0.0   0.0   0.0   1.0  // 这一行表明着 W复制代码

接着,这个矩阵会乘上当前摄像头帧的平移矩阵获得最后用来放置新锚点的矩阵。举个例子,假设是以下的相机转换矩阵(以一个列的数组的形式):

[ [ 0.103152, -0.757742,   0.644349, 0.0 ],
  [ 0.991736,  0.0286687, -0.12505,  0.0 ],
  [ 0.0762833, 0.651924,   0.754438, 0.0 ],
  [ 0.0,       0.0,        0.0,      1.0 ]
]复制代码

那么相乘的结果将是:

[ [0.103152,   -0.757742,   0.644349, 0.0 ],
  [0.991736,    0.0286687, -0.12505,  0.0 ],
  [0.0762833,   0.651924,   0.754438, 0.0 ],
  [-0.0152567, -0.130385,  -0.150888, 1.0 ]
]复制代码

这里是关于矩阵如何相乘的更多信息,这是一个矩阵乘法计算器

如今你知道这个例子是如何工做的了,让咱们修改它来建立咱们的游戏吧。

构建 SpriteKit 的场景

在 Scene.swift 的文件中,让咱们加上以下的配置:

class Scene: SKScene {

      let ghostsLabel = SKLabelNode(text: "Ghosts")
      let numberOfGhostsLabel = SKLabelNode(text: "0")
      var creationTime : TimeInterval = 0
      var ghostCount = 0 {
        didSet {
          self.numberOfGhostsLabel.text = "\(ghostCount)"
        }
      }
      ...
    }复制代码

咱们增长了两个标签,一个表明了场景中的幽灵的数量,控制幽灵产生的时间间隔,和幽灵的计数器,它有个属性观察器,每当它的值变化的时候,标签就会更新。

接下来,下载幽灵移除时播放的音效,并把它拖到项目中:

把下面这行加到类里面:

let killSound = SKAction.playSoundFileNamed("ghost", waitForCompletion: false)复制代码

咱们稍后调用这个动做来播放音效。

didMove 方法中,咱们把标签加到场景中:

override func didMove(to view: SKView) {
      ghostsLabel.fontSize = 20
      ghostsLabel.fontName = "DevanagariSangamMN-Bold"
      ghostsLabel.color = .white
      ghostsLabel.position = CGPoint(x: 40, y: 50)
      addChild(ghostsLabel)

      numberOfGhostsLabel.fontSize = 30
      numberOfGhostsLabel.fontName = "DevanagariSangamMN-Bold"
      numberOfGhostsLabel.color = .white
      numberOfGhostsLabel.position = CGPoint(x: 40, y: 10)
      addChild(numberOfGhostsLabel)
    }复制代码

你能够用像 iOS Fonts 的站点来可视化的选择标签的字体。

这个位置坐标表明着屏幕左下角的部分(相关代码稍后会解释)。我选择把它们放在屏幕的这个区域是为了不转向的问题,由于场景的大小会随着方向改变而变化,可是,坐标保持不变,会引发标签显示超过屏幕或者在一些奇怪的位置(能够经过重写 didChangeSize 方法或者使用 UILabels 替换 SKLabelNodes 来解决这一问题)。

如今,为了在固定的时间间隔建立幽灵,咱们须要一个定时器。这个更新方法会在每一帧(平均 60 次每秒)渲染以前被调用,能够像下面这样帮助咱们:

override func update(_ currentTime: TimeInterval) {
      // 在每一帧渲染以前调用
      if currentTime > creationTime {
        createGhostAnchor()
        creationTime = currentTime + TimeInterval(randomFloat(min: 3.0, max: 6.0))
      }
    }复制代码

参数 currentTime 表明着当前应用中的时间,因此若是它大于 creationTime 所表明的时间,一个新的幽灵锚点会建立, creationTime 也会增长一个随机的秒数,在这个例子里面,是在 3 到 6 秒。

这是 randomFloat 的定义:

func randomFloat(min: Float, max: Float) -> Float {
      return (Float(arc4random()) / 0xFFFFFFFF) * (max - min) + min
    }复制代码

createGhostAnchor 方法中,咱们须要获取场景的界面:

func createGhostAnchor(){
      guard let sceneView = self.view as? ARSKView else {
        return
      }

    }复制代码

接着,由于在接下来的函数中咱们都要与弧度打交道,让咱们先定义一个弧度的 360 度:

func createGhostAnchor(){
      ...

      let _360degrees = 2.0 * Float.pi

    }复制代码

如今,为了把幽灵放置在一个随机的位置,咱们分别建立一个随机 X-轴旋转和 Y-轴旋转矩阵:

func createGhostAnchor(){
      ...

       let rotateX = simd_float4x4(SCNMatrix4MakeRotation(_360degrees * randomFloat(min: 0.0, max: 1.0), 1, 0, 0))

      let rotateY = simd_float4x4(SCNMatrix4MakeRotation(_360degrees * randomFloat(min: 0.0, max: 1.0), 0, 1, 0))

    }复制代码

幸运的是,咱们不须要去手动地建立这个旋转矩阵,有一些函数能够返回一个表示旋转,平移或者缩放的转换信息矩阵。

在这个例子中,SCNMatrix4MakeRotation 返回了一个表示旋转变换的矩阵。第一个参数表明了旋转的角度,要用弧度的形式。在这个表达式 _360degrees * randomFloat(min: 0.0, max: 1.0) 中获得一个在 0 到 360 度中的随机角度。

剩下的 SCNMatrix4MakeRotation 的参数,表明了 X,Y 和 Z 轴各自的旋转,这就是为何咱们第一次调用的时候把 1 做为 X 的参数,而第二次的时候把 1 做为 Y 的参数。

SCNMatrix4MakeRotation 的结果经过 simd_float4x4 结构体转换为一个 4x4 的矩阵。

若是你正在使用 Xcode 9 Beta 1 的话,你应该用 SCNMatrix4ToMat4 ,在 Xcode 9 Beta 2 中它被 simd_float4x4 替换了。

咱们能够经过矩阵乘法来组合两个旋转矩阵:

func createGhostAnchor(){
      ...
      let rotation = simd_mul(rotateX, rotateY)

    }复制代码

接着,咱们建立一个 Z-轴是 -1 到 -2 之间的随机值的转换矩阵。

func createGhostAnchor(){
      ...
      var translation = matrix_identity_float4x4
      translation.columns.3.z = -1 - randomFloat(min: 0.0, max: 1.0)

    }复制代码

组合旋转和位移矩阵:

func createGhostAnchor(){
      ...
      let transform = simd_mul(rotation, translation)

    }复制代码

建立并把这个锚点加到该会话中:

func createGhostAnchor(){
      ...
      let anchor = ARAnchor(transform: transform)
      sceneView.session.add(anchor: anchor)

    }复制代码

而且增长幽灵计数器:

func createGhostAnchor(){
      ...
      ghostCount += 1
    }复制代码

如今惟一剩下没有加的就是当用户触摸一个幽灵并移动它的代码。首先重写 touchesBegan 来获取到触摸的物体:

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
      guard let touch = touches.first else {
        return
      }

    }复制代码

接着获取该触摸在 AR 场景中的位置:

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
      ...
      let location = touch.location(in: self)

    }复制代码

获取在该位置的全部节点:

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
      ...
      let hit = nodes(at: location)

    }复制代码

获取第一个节点(若是有的话),检查这个节点是否是表明着一个幽灵(记住标签一样也是一个节点):

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
      ...
      if let node = hit.first {
        if node.name == "ghost" {

        }
      }
    }复制代码

若是就这个节点的话,组合淡出和音效动做,建立一个动做序列并执行它,同时减少幽灵的计数器:

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
      ...
      if let node = hit.first {
        if node.name == "ghost" {

          let fadeOut = SKAction.fadeOut(withDuration: 0.5)
          let remove = SKAction.removeFromParent()

          // 组合淡出和音效动画
          let groupKillingActions = SKAction.group([fadeOut, killSound])
          // 建立动做序列
          let sequenceAction = SKAction.sequence([groupKillingActions, remove])

          // 执行动做序列
          node.run(sequenceAction)

          // 更新计数
          ghostCount -= 1

        }
      }
    }复制代码

到这里,咱们的场景已经完成了,如今咱们开始处理 ARSKView 的视图控制器。

构建视图控制器

在 viewDidLoad 中,再也不加载 Xcode 为咱们建立的场景,让咱们经过这种方式来建立咱们的场景:

override func viewDidLoad() {
      ...

      let scene = Scene(size: sceneView.bounds.size)
      scene.scaleMode = .resizeFill
      sceneView.presentScene(scene)
    }复制代码

这会确保咱们的场景能够填满整个界面,甚至整个屏幕(在 Main.storyboard 中定义的 ARSKView 填满了整个屏幕)。这一样也有助于把游戏的标签订位在屏幕的左下角,根据场景中定义的位置坐标。

如今,如今是时候添加幽灵图片了。在个人例子中,图片的格式原来是 SVG ,因此我转换到了 PNG ,而且为了简单起见,只加了图片中的前 6 个幽灵,建立了 2X 和 3X 版本(我没看见建立 1X 版本的地方,所以采用了缩放策略的设备不可以正常的运行这个应用)。

把图片拖到 Assets.xcassets 中:

注意图像名字最后的数字 - 这会帮咱们随机选择一个图片建立 SpriteKit 节点。用这个替换 view(_ view: ARSKView, nodeFor anchor: ARAnchor) 中的代码:

func view(_ view: ARSKView, nodeFor anchor: ARAnchor) -> SKNode? {
      let ghostId = randomInt(min: 1, max: 6)

      let node = SKSpriteNode(imageNamed: "ghost\(ghostId)")
      node.name = "ghost"

      return node
    }复制代码

咱们给全部的节点一样的名字 ghost ,因此在移除它们的时候咱们能够识别它们。

固然,不要忘了 randomInt 方法:

func randomInt(min: Int, max: Int) -> Int {
      return min + Int(arc4random_uniform(UInt32(max - min + 1)))
    }复制代码

如今咱们已经完成了全部工做!让咱们来测试它吧!

测试应用

在真机上运行这个应用,赋予摄像头权限,而且开始在全部方向中寻找幽灵:

每 3 到 6 秒就会出现一个新的幽灵,计数器也会更新,每当你击中一个幽灵的时候就会播放一个音效。

试着让计数器归零吧!

结论

关于 ARKit 有两个很是棒的地方。第一是只须要几行代码咱们就能建立神奇的 AR 应用,第二个,咱们也能学习到 SpriteKit 和 SceneKit 的知识。 ARKit 实际上只有不多的量的类,更重要的是去学会如何运用上面提到的框架,并且稍加调整就能创造出 AR 体验。

你能够经过增长游戏规则,引入奖励分数或者改变图像和声音来扩展这个应用。一样的,使用 Pusher,你能够同步游戏状态来增长多人游戏的特性。

记住你能够在这个 GitHub 仓库中找到 Xcode 项目。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOSReact前端后端产品设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏