巧用 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前端後端產品設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索