翻译自:http://texnotes.me/post/5/

苹果 刚刚宣布 了他们的增强现实的开发平台,ARKit. 在秋季iOS 11最终发布后用户将能够使用到带有AR功能的应用. 对于我们开发者来说,有个好消息是, 我们可以在beta中优先体验这些应用.

我实现了一个简单的基于 ARKit SceneKit的AR 第一人称射击游戏,  项目在我的 GitHub. 如果你感到困惑或者没有太多的时间, 那么你可以下载本项目. 本指南将会涵盖创建类似项目的所有步骤, 希望可以提供足够多的理解来帮助你开始创建你自己的AR 项目. 这里的 GIF图片 展示了运行后的应用效果.

准备开始

必备环境:

  • Xcode 9 beta
  • 安装iOS 11 beta 系统的 iOS 设备

你可以下载这些必备环境 下载地址 (只要你是苹果开发者计划的一员).

首先, 在Xcode 9 中创建一个项目 (single view app) 并为项目起个你认为合适的名字. 打开 Main.storyboard 同时 拖拽一个 ARKit Scenekit View 控件到 single view controller. 你应该拖拽控件边缘或者设置约束使控件覆盖整个屏幕. 接着, ctrl + 拖拽添加的 View 到 ViewController.swift 用于添加一个对应的实例变量. 命名为 sceneView.

注意: 打开 Info.plist 添加 使用 照相机的权限:

<key>NSCameraUsageDescription</key>
<string>This application will use the camera for Augmented Reality.</string>

ARKit

重点来了. 打开 ViewController.swift 导入 ARKit. 同时, 给 view controller 添加 ARSCNViewDelegate 和 SCNPhysicsContactDelegate协议.:

//  ViewController.swift

import UIKit
import ARKit

class ViewController: UIViewController, ARSCNViewDelegate, SCNPhysicsContactDelegate {

    @IBOutlet weak var sceneView: ARSCNView!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }

}

此时, 在 viewDidLoad() 中 设置ARSCNView的代理

// Set the view's delegate
sceneView.delegate = self

// Show statistics such as fps and timing information
sceneView.showsStatistics = true

// Create a new (and empty) scene
let scene = SCNScene()

// Set the scene to the view
sceneView.scene = scene
sceneView.scene.physicsWorld.contactDelegate = self

在 viewWillAppear() 中 设置  session

// Create a session configuration
let configuration = ARWorldTrackingSessionConfiguration()
configuration.planeDetection = ARWorldTrackingSessionConfiguration.PlaneDetection.horizontal

// Run the view's session
sceneView.session.run(configuration)

第一个检查点: 非常好! 如果你在你的设备上编译并运行,你将会看到全屏的照相机控件,底部显示FPS. 但并不令人兴奋!

让我们添加一个 漂浮的立方体. 要做到这一点, 我们创建一个类 (可以在同一个文件中或者单独创建文件, 随你选择). 值得一提的是 ARKit 同 SpiteKit, Unity, Unreal 和 SceneKit是兼容的. 所有这些有助于开发2D和3D图形开发. 然而, SceneKit 是如此的简单,本项目中我选择他开发基本的3D图像应用.

因此, 因为我们使用 SceneKit, 我们的  ship 是  SCNNode 的子类. 我们将会赋予它一个简单盒子的形状,所以我们使用 SCNBox 几何体. 此外, 我们赋予一个物理实体,便于将来进行注册关联.

import UIKit
import SceneKit

class Ship: SCNNode {
    override init() {
        super.init()
        let box = SCNBox(width: 0.1, height: 0.1, length: 0.1, chamferRadius: 0)
        self.geometry = box
        let shape = SCNPhysicsShape(geometry: box, options: nil)
        self.physicsBody = SCNPhysicsBody(type: .dynamic, shape: shape)
        self.physicsBody?.isAffectedByGravity = false
        self.physicsBody?.categoryBitMask = CollisionCategory.ship.rawValue // See below for what CollisionCategory is! 
        self.physicsBody?.contactTestBitMask = CollisionCategory.bullets.rawValue
     }
}

你将会注意到你的代码不在需要编译因为出现了一个叫做CollisionCategory的神秘结构. 比我们预计的要早,是时候了解碰撞了.

简短的位屏蔽偏移(A Brief Bit Mask Excursion)

我们花点时间谈谈碰撞. 很重要的一点是,我们的app 应当理解当子弹打中飞船时做出的响应. 总的来说, 我们都知道当子弹撞击子弹或者飞船撞击飞船时的物理现象. 换句话说, 子弹应该反弹,飞船也是一样. 然而, 我们只想我们的代理处理 飞船-子弹碰撞来引起爆炸效果 (回忆之前的SCNPhysicsContactDelegate). 因此, 我们可以创建一个类似如下的表:

//  Contact should be delegated by SceneKit? T/F
       Bullet | Ship
Bullet  False | True
Ship     True | False

位屏蔽提供了一中直接的方式来记录这样的表. 让我们谈谈整型. 通常, 整型占用4个字节, 但是我们假设它只占一个字节而不失一般性. 因此,如果我们要给每个类型的节点一个唯一的整数标识符看起来就只是一个比特链. 假设我们的第一类型的节点标识符1。在位层面是这样子:

0 0 0 0 0 0 0 1

因此, 让我们给出第二类型的节点标识符:

0 0 0 0 0 0 1 0

如果我们做这些标识符的按位与操作我们会得到一串0. 有意思. SceneKit 设置每个结点的 categoryBitMask,当它同另一个结点碰撞后,它就会与碰撞结点的contactTestBitMask进行按位与操作来决定是否处理本次碰撞. .

所以, 我们按照思路创建一个结构体 (将结构体放到一个单独的文件或者当前类文件的下边):

struct CollisionCategory: OptionSet {
    let rawValue: Int

    static let bullets  = CollisionCategory(rawValue: 1 << 0)
    static let ship = CollisionCategory(rawValue: 1 << 1)
}

如上所示两个完全相同的标识符. 现在, 让我们参考一下Ship.swift中的代码.

self.physicsBody?.categoryBitMask = CollisionCategory.ship.rawValue
self.physicsBody?.contactTestBitMask = CollisionCategory.bullets.rawValue

现在, 假设两个飞船发生了碰撞: 执行按位与操作 (1 << 0) & (1 << 1) == 0. 因此, 代理操作不被触发.

注意 contactTestBitMask 和 collisionBitMask的不同. 前者决定两个接触后的物理实体是否被委托处理. 后者决定一个物理的可视的碰撞是否发生.

一旦你有许多的结点类别伴随着各种各样的碰撞操作, 理解这些将使你的开发更加容易.

好处: 因为二进制补码普遍采用带符号整型表示, 如果你设置所有的接触位屏蔽为-1 (所有位为 1), 那么所有的碰撞将会被委托. 事实上, 默认值为 -1,由于我们没有改变collisionBitMask, 所有的碰撞发生了 (但并不是所有都会被委托)

返回到 ViewController

非常好, 我们已经把  Ship 类关联在一起了, 让我们创建一个并显示到屏幕上. 在 viewDidLoad中, 添加调用代码 self.addNewShip() ,此函数定义如下

func addNewShip() {
    let cubeNode = Ship()

    let posX = floatBetween(-0.5, and: 0.5)
    let posY = floatBetween(-0.5, and: 0.5  )
    cubeNode.position = SCNVector3(posX, posY, -1) // SceneKit/AR coordinates are in meters
    sceneView.scene.rootNode.addChildNode(cubeNode)
}

我们希望每次出现的飞船同用户的距离相同, 但是会出现在用户之前1米的随机位置上. 因此上边的方法调用可以生成一个飞船并添加到 sceneView 上. 因此, floatBetween 方法的作用是返回某个范围内的一个随机浮点数

func floatBetween(_ first: Float,  and second: Float) -> Float {
    return (Float(arc4random()) / Float(UInt32.max)) * (first - second) + second
}

第二个检查点: 漂亮, 现在再次编译运行,此时在你之前1米的地方可以看到一个立方体.

现在让我们击落飞船:

回到 storyboard 并给 view controller添加一个 轻拍手势识别器 . ctrl + 拖拽在ViewController.swift中添加一个 action .这将使我们知道用户何时发射子弹. 我们完善action

@IBAction func didTapScreen(_ sender: UITapGestureRecognizer) { // fire bullet
    let bulletsNode = Bullet()
    bulletsNode.position = SCNVector3(0, 0, -0.2) // SceneKit/AR coordinates are in meters

    let bulletDirection = self.getUserDirection()
    bulletsNode.physicsBody?.applyForce(bulletDirection, asImpulse: true)
    sceneView.scene.rootNode.addChildNode(bulletsNode)

}

显然, 我们需要定义 Bullet 类. 上边的方法看起来如此的简单. 我们创建一个结点, 并将其放到照相机前边的正确位置

class Bullet: SCNNode {
    override init () {
        super.init()
        let sphere = SCNSphere(radius: 0.025)
        self.geometry = sphere
        let shape = SCNPhysicsShape(geometry: sphere, options: nil)
        self.physicsBody = SCNPhysicsBody(type: .dynamic, shape: shape)
        self.physicsBody?.isAffectedByGravity = false
        self.physicsBody?.categoryBitMask = CollisionCategory.bullets.rawValue
        self.physicsBody?.contactTestBitMask = CollisionCategory.ship.rawValue
    }
    }

我们仍需要获取用户方向的方法

func getUserDirection() -> SCNVector3 {
    if let frame = self.sceneView.session.currentFrame {
    let mat = SCNMatrix4FromMat4(frame.camera.transform)
        return SCNVector3(-1 * mat.m31, -1 * mat.m32, -1 * mat.m33)
    }
    return SCNVector3(0, 0, -1)
}

第三个检查点: 编译运行应用, 我们可以向飞船(实际上是一个立方体)发射子弹bullets (实际是一个小球体). 我们注意到, 球体将简单地从立方体中反弹 (并推动立方体飞行, 这是非常酷).

我们需要做一些销毁工作! 以下是我们之前提到的在交互代理中实现的.

// MARK: - Contact Delegate

func physicsWorld(_ world: SCNPhysicsWorld, didBegin contact: SCNPhysicsContact) {
        contact.nodeA.removeFromParentNode()
        contact.nodeB.removeFromParentNode()
        print("Hit ship!")
        self.addNewShip()

}

因为我们阻止了 子弹-子弹 和 飞船-飞船 的碰撞, 我们知道的所有接触就只有 子弹-飞船碰撞. 注意, 我们需要一个更细致入微的功能, 如果不是这样的话. 然而,  因为我们的应用 如此简单, 我们可以简单的删除两个结点并且调用 addNewShip 方法. 哈!

最后一个检查点: 运行应用并愉快的射击立方体! 这是相当令人兴奋. 可以随意扩展到一个真实的游戏中.

非常好, 像Apache 许可允许一样自由.

总结

这就是我关于苹果 ARKit 的简短指南. 我希望你喜欢他并学到一些东西! 如果你有任何问题在下边发表评论.

本指南会继续更新. 这意味着没有计划添加更多的代码, 但是有进一步添加更多的解释来进一步澄清事情的计划. 谢谢你的支持.