WWDC2017のデモアプリみたいに、ARKitとSpriteKitをつかってSKNodeをタップしてみた


この記事では、ARKitとSpriteKitをつかってAR空間に配置したSKNodeをタップする方法を紹介します。

WWDC2017の『Going Beyond 2D with SpriteKit』セッションで、つぎのようなデモアプリを紹介していました。

ARKitをつかって表示した画面をタップすると、AR空間上に絵文字が置かれます(この絵文字はARAnchorというアンカーで固定されていて、カメラを動かしてもAR空間の同じ位置に表示されます)。さらに、置いた絵文字をタップすると、絵文字が爆発して消えるというものです(先ほど紹介したセッション動画の13分57秒からデモがはじまります。興味がある方はご覧ください)。

タップした場所に絵文字(SpriteKitのSKLabelNode)を置く処理は、Xcode9のテンプレートで『Augmented Reality App』を選択すると最初から組み込まれているので、この記事では割愛します。

ここからは、AR空間上に置いた絵文字(SKLabelNode)をタップしたときに、処理を実行する(デモアプリでは絵文字が爆発していました)方法を紹介します。

SKNodeをタップできるようにする方法

先ほどすこし触れたように、Xcode9のテンプレートには『Augmented Reality App』が追加されています(個人的には、このあたりにもAppleのARに対する力の入れようが感じられますが、それはさておき)。

このテンプレートで作成したプロジェクトでは、画面をタップするとAR空間上にインベーダーの絵文字を置くアプリを実行できます(スクリーンショットを載せたほうがわかりやすいのですが、NDAに引っかかるので、残念ながら割愛します。ディベロッパーアカウントをお持ちの方は、ぜひXcode9をダウンロードしてお試しください)。このプロジェクトをもとにして、WWDCのデモアプリのように絵文字をタップしたときに処理を実行できるようにしてみましょう。

変更するのは一箇所だけで、Scene.swiftファイルのtouchesBeganメソッドの中です。
『MARK: Original Code』の部分は、もともとあったコードをコメントにしています。その代わりに、『MARK: Added Code』の部分を追加しています。

以下に、Scene.swiftのコードを記載します。

class Scene: SKScene {

    override func didMove(to view: SKView) {
        // Setup your scene here
    }

    override func update(_ currentTime: TimeInterval) {
        // Called before each frame is rendered
    }

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

        // MARK: Original Code
        /*
        // Create anchor using the camera's current position
        if let currentFrame = sceneView.session.currentFrame {

            // Create a transform with a translation of 0.2 meters in front of the camera
            var translation = matrix_identity_float4x4
            translation.columns.3.z = -0.2
            let transform = simd_mul(currentFrame.camera.transform, translation)

            // Add a new anchor to the session
            let anchor = ARAnchor(transform: transform)
            sceneView.session.add(anchor: anchor)
        }
        */

        // MARK: Added Code
        if let touchLocation = touches.first?.location(in: sceneView), let touchNode = nodes(at: sceneView.convert(touchLocation, to: self)).first {
            touchNode.removeFromParent()
        } else {
            // Create anchor using the camera's current position
            if let currentFrame = sceneView.session.currentFrame {

                // Create a transform with a translation of 0.2 meters in front of the camera
                var translation = matrix_identity_float4x4
                translation.columns.3.z = -0.2
                let transform = simd_mul(currentFrame.camera.transform, translation)

                // Add a new anchor to the session
                let anchor = ARAnchor(transform: transform)
                sceneView.session.add(anchor: anchor)
            }
        }        
    }
}

『MARK: Added Code』の部分では、まず、タップした座標にSKNodeが存在するか調べています。

SKNodeが存在する場合は、そのSKNodeを削除しています。存在しない場合は、ARAnchor(AR空間上の特定の点)を追加しています(あれ? SKNodeを追加していないじゃないか? と思われたかもしれませんね。それについては、今回の記事の主旨からはずれるので、この記事の最後で補足します)。

タップした座標を使用するときの注意点

この記事で紹介しているアプリを開発しているときに、絵文字をタップしても削除できなくて、原因を調べていました。
どうも座標が怪しそうだということに気がついて、ハッとしました。勘の良い方はすでにお気づきかと思います。そうです、ARKitとSpriteKitで座標のとらえかたがことなるのです(座標のちがいについては、こちらの記事『UIViewの座標とSKNodeの座標の違い』をご覧ください)。

こんかいのアプリでは、タップした座標にあるSKNodeを取得する

nodes(at p: CGPoint) -> [SKNode]

というメソッドを使用しました。

これは、SpriteKitのメソッドなので、SpriteKitの座標を渡す必要があります。しかし、ARKitで取得した座標を渡していたため、座標がずれてしまって絵文字を削除することができませんでした。

対応方法は、ふたつあります。
1.ARKitの座標をSpriteKitの座標に変換してメソッドに渡す。
2.タップした座標をSpriteKitで取得する。

先ほど紹介したコードは、『1.ARKitの座標をSpriteKitの座標に変換してメソッドに渡す』を使用しています。具体的には、以下の部分で

let touchNode = nodes(at: sceneView.convert(touchLocation, to: self)).first

convert(_ point: CGPoint, to scene: SKScene)メソッドを使って座標を変換しています。

上記の座標を変換する方法で対応できていますが、参考までに『2.タップした座標をSpriteKitで取得する』も紹介します。

2.タップした座標をSpriteKitで取得する

『2.タップした座標をSpriteKitで取得する』場合のコードは、以下の通りです。

if let touchLocation = touches.first?.location(in: self), let touchNode = nodes(at: touchLocation).first {
    touchNode.removeFromParent()
}

ポイントは

let touchLocation = touches.first?.location(in: self)

の『self』です。このメソッドを書いているのはSpriteKitの『SKScene』のサブクラスです。つまり、selfを指定するということは、SpriteKitの座標を取得するということになります。

let touchNode = nodes(at: touchLocation).first

ですので、上記のコードでは『touchLocation』を変換せずにそのままメソッドに渡しています。

おまけ)追加したARAnchor上にSKNodeを置く方法

蛇足になりますが、追加したARAnchor上にSKNodeを置く方法を紹介します。

ARKitにはいくつかdelegateメソッドがありまして、そのひとつをつかいます。ViewControllerで『ARSKViewDelegate』を採用して、以下のデリゲートメソッドを書きます。

func view(_ view: ARSKView, nodeFor anchor: ARAnchor) -> SKNode?

このデリゲートメソッドは、ARAnchorを追加したとき呼ばれます。

    func view(_ view: ARSKView, nodeFor anchor: ARAnchor) -> SKNode? {
        // Create and configure a node for the anchor added to the view's session.
        let labelNode = SKLabelNode(text: "👾")
        labelNode.horizontalAlignmentMode = .center
        labelNode.verticalAlignmentMode = .center
        return labelNode;
    }

上記のように、メソッドの中でSKNode(今回はインベーダーの絵文字のSKLabelNode)を返してあげると、追加したARAnchorの場所にSKNodeを追加してくれます。

おわりに

こんかい紹介したコードは こちらに置いてあります。

ARKit楽しいですね。
以前、『パンダクリンAR』という、ARをつかってお片づけするアプリをリリースしましたが、そのときにはARKitが出るなんて想像もしていませんでした。
ARKitは、アイデア次第でとてもおもしろいアプリをつくれるのではないかと感じています。

それでは、よきARKitライフを!