工場裏のアーカイブス

素人によるiPhoneアプリ開発の学習記

SceneKitにおける画面遷移のテスト

以前にこちらの記事で触れましたように、SpriteKitでは SKTransition を用いて、あるシーンから別のシーンへと、アニメーション付きで移動する処理を容易に実装出来ます。このような画面遷移をSceneKitでも実装したいと考えていたのですが、以前のSceneKitにはこのような仕組みは用意されておらず、もどかしい思いをしていました。

しかし、iOS 9以降ではSceneKitのビュー(SCNView)でもpresentSceneメソッドが利用可能となり、SKTransition を用いた画面遷移が利用可能となったようです。
これでSceneKitでも、タイトル画面などを備えたゲームが作りやすくなる!と思い、早速テストしてみたのですが、どうやらSceneKitのシーン(SCNScene)はSpriteKitのシーン(SKScene)と仕様に色々な違いがあり、SpriteKitと同じ感覚で…というわけには行きませんでした。

それでもいろいろ調べて、何とかSceneKitで SKTransition を用いた画面遷移を(あまりスマートではない方法ですが)実装することが出来ましたので、本記事ではその手順などについてメモします。
 

SCNSceneとSKSceneの違い

例えば、あるシーンに表示されたボタンをタップすることで、別のシーンへと移動する処理を実装したいとします。SpriteKitでは、このような処理はシーン内でタッチイベント系のメソッドを用いて、例えば以下のように実装できます。

//SpriteKitにおける画面遷移の例(touchesBeganメソッド部分のみ抜粋)
//あるシーンから、ボタン(“button” という名前のラベルノード)をタップして、別のシーンへと移動する
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
        for touch in touches{
            let p = touch.locationInNode(self)
            
            //"button”という名前のラベルノードは、別所で既に定義されているものとする
            if let button = self.childNodeWithName("button"){
                if(button.containsPoint(p)){

                    //"SecondScene”シーンは、別所で既に定義されているものとする
                    let secondScene = SecondScene(size: self.size)
                    let transition = SKTransition.crossFadeWithDuration(1.0)
                    self.view?.presentScene(secondScene, transition: transition)
                }
            }
        }
    }

実際に画面遷移を担うのはシーンではなく、それを表示している親ビューの側です。しかし SKScene には自身の親ビューを参照するviewプロパティが存在しており、これを用いて親ビューに処理(presentSceneメソッド)を実行させることが出来ます。

一方、SceneKit のSCNSceneには、このようなviewプロパティが存在しません。そしてSCNSceneは UIResponder を継承していないため、タッチイベント関連のメソッドを利用することも出来ないようです。そのため、SpriteKit と同様の方法で画面遷移を実装することは出来ませんでした。
 

SceneKitにおける画面遷移の実装(あまりスマートではない方法)

SCNSceneではタッチイベント関連のメソッドは利用できませんが、ビュー(ビューコントローラー)側では利用することが出来ます。そこで、タッチイベントの処理は全てビューに任せるという単純な方法で、ひとまずSceneKitにおける画面遷移処理のサンプルを実装してみました。

まず Xcode(本記事執筆時点:Version 7.2)で「Game」テンプレート(Language: をSwiftに、Game Technology: をSceneKitにする)でプロジェクトを作成し、GameViewController.swiftの中身を以下のコードに書き換えます。

import UIKit
import QuartzCore
import SceneKit
import SpriteKit

class GameViewController: UIViewController {
    
    weak var scnView : SCNView?
    var firstScene : SCNScene?
    var secondScene : SCNScene?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        scnView = self.view as? SCNView
        self.firstScene = FirstScene()
        self.secondScene = SecondScene()
        
        scnView?.scene = self.firstScene
        scnView?.backgroundColor = UIColor(red: 0.2, green: 0.2, blue: 0.4, alpha: 1.0)        
    }
    
    override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
        if let touch = touches.first{
            let p = touch.locationInView(self.scnView)
            
            if let hitResults = self.scnView?.hitTest(p, options: nil){
                if hitResults.count > 0{
                    let result = hitResults.first?.node
                    
                    if result?.name! == "switch1"{
                        let transition = SKTransition.pushWithDirection(SKTransitionDirection.Left, duration:1.0)
                        self.scnView?.presentScene(self.secondScene!, withTransition: transition,
                                                                incomingPointOfView: nil, completionHandler: nil)
                        scnView?.backgroundColor = UIColor(red: 0.4, green: 0.4, blue: 0.2, alpha: 1.0)
                        
                    }
                    if result?.name! == "switch2"{
                        let transition = SKTransition.doorsOpenHorizontalWithDuration(1.0)
                        self.scnView?.presentScene(self.firstScene!, withTransition: transition,
                                                                incomingPointOfView: nil, completionHandler: nil)
                        scnView?.backgroundColor = UIColor(red: 0.2, green: 0.2, blue: 0.4, alpha: 1.0)
                    }
                }
            }
        }
    }
    
    override func shouldAutorotate() -> Bool {
        return true
    }
    
    override func prefersStatusBarHidden() -> Bool {
        return true
    }
    
    override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
        if UIDevice.currentDevice().userInterfaceIdiom == .Phone {
            return UIInterfaceOrientationMask.AllButUpsideDown
        } else {
            return UIInterfaceOrientationMask.All
        }
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Release any cached data, images, etc that aren't in use.
    }
}

 
続いて、プロジェクトにFirstScene.swift、SecondScene.swiftというソースファイルを追加します。そして、それぞれの中身を以下のコードに書き換えます。

//FirstScene.swift(最初に表示するシーン)
import SceneKit

class FirstScene: SCNScene{
    
    var cameraNode : SCNNode?
    
    override init(){
        super.init()
        
        self.setObject()
        self.setCamera()
        self.setLight()
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func setObject(){
        let box = SCNBox(width: 5.0, height: 5.0, length: 5.0, chamferRadius: 0.0)
        box.firstMaterial?.diffuse.contents = UIColor(red: 0.4, green: 1.0, blue: 0.4, alpha: 1.0)
        
        let boxNode = SCNNode()
        boxNode.geometry = box
        boxNode.position = SCNVector3(x: 0.0, y: 0.0, z: 0.0)
        boxNode.name = "switch1"
        self.rootNode.addChildNode(boxNode)
        boxNode.runAction(SCNAction.repeatActionForever(
            SCNAction.rotateByX(0.1, y: 0.2, z: 0.1, duration: 2.0)
        ))
    }
    
    func setCamera(){
        self.cameraNode = SCNNode()
        self.cameraNode?.camera = SCNCamera()
        self.cameraNode?.name = "camera"
        self.cameraNode?.position = SCNVector3(x: 0, y: 30, z: 30)
        self.cameraNode?.camera?.zFar = 1000.0
        self.rootNode.addChildNode(self.cameraNode!)
        
        if let center = self.rootNode.childNodeWithName("switch1", recursively: false){
            let cons = SCNLookAtConstraint(target: center)
            cons.influenceFactor = 1.0
            self.cameraNode!.constraints = [cons]
        }
    }
    
    func setLight(){
        let ambientLightNode = SCNNode()
        ambientLightNode.light = SCNLight()
        ambientLightNode.light?.type = SCNLightTypeAmbient
        ambientLightNode.light?.color = UIColor.darkGrayColor()
        self.rootNode.addChildNode(ambientLightNode)
        
        let lightNode = SCNNode()
        lightNode.light = SCNLight()
        lightNode.light?.type = SCNLightTypeSpot
        lightNode.light?.spotOuterAngle = 120
        lightNode.light?.color = UIColor.whiteColor()
        lightNode.light?.castsShadow = true
        lightNode.position = SCNVector3(x: 2, y: 5, z: 10)
        
        self.rootNode.addChildNode(lightNode)
    }
}


//SecondScene.swift(画面遷移後に表示するシーン)
import SceneKit

class SecondScene: SCNScene {
    
    var cameraNode : SCNNode?
    
    override init(){
        super.init()
        
        self.setObject()
        self.setCamera()
        self.setLight()
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func setObject(){
        let sphere = SCNSphere(radius: 3.0)
        sphere.firstMaterial?.diffuse.contents = UIColor(red: 1.0, green: 0.4, blue: 0.4, alpha: 1.0)
        
        let sphereNode = SCNNode()
        sphereNode.geometry = sphere
        sphereNode.position = SCNVector3(x: 0.0, y: 0.0, z: 0.0)
        sphereNode.name = "switch2"
        self.rootNode.addChildNode(sphereNode)
    }
    
    func setCamera(){
        self.cameraNode = SCNNode()
        self.cameraNode?.camera = SCNCamera()
        self.cameraNode?.name = "camera"
        self.cameraNode?.position = SCNVector3(x: 0, y: 30, z: 30)
        self.cameraNode?.camera?.zFar = 1000.0
        self.rootNode.addChildNode(self.cameraNode!)
        
        if let center = self.rootNode.childNodeWithName("switch2", recursively: false){
            let cons = SCNLookAtConstraint(target: center)
            cons.influenceFactor = 1.0
            self.cameraNode!.constraints = [cons]
        }
    }
    
    func setLight(){
        let ambientLightNode = SCNNode()
        ambientLightNode.light = SCNLight()
        ambientLightNode.light?.type = SCNLightTypeAmbient
        ambientLightNode.light?.color = UIColor.darkGrayColor()
        self.rootNode.addChildNode(ambientLightNode)
        
        let lightNode = SCNNode()
        lightNode.light = SCNLight()
        lightNode.light?.type = SCNLightTypeSpot
        lightNode.light?.spotOuterAngle = 90
        lightNode.light?.color = UIColor.whiteColor()
        lightNode.light?.castsShadow = true
        lightNode.position = SCNVector3(x: 2, y: 5, z: 10)
        
        self.rootNode.addChildNode(lightNode)
    }
}


FirstScene.swiftでは最初に表示するシーンを定義します。ここでは回転する立方体(”switch1”という名前のノード)を画面中央に表示します。SecondScene.swiftには画面遷移後に表示するシーンを定義します。ここでは球体(”switch2”という名前のノード)を画面中央に表示します。これらの立体モデルがボタン代わりとなります。

そしてGameViewController.swiftのtouchBeganメソッドで、シーンの種類を問わずまとめてタッチイベントを処理します。現在ビューに表示されているのがFirstSceneであれば、立方体のタップ時に”switch1”ノードがヒットするので、そのときpresentScene メソッドで SecondScene へと移動します(なお、背景色の変更もここで行っています)。そしてSecondSceneが表示されているときは、球体のタップ時に”switch2”ノードがヒットするので、同様の手順でFirstScene へと移動します。

なお、SceneKitにおけるpresentSceneメソッドは以下のような形になっています。移動先のシーンとSKTransitionを設定するのはSpriteKitと同様ですが、それ以外に2つ引数があります。

//SceneKitにおけるpresentSceneメソッド
func presentScene(scene: SCNScene, withTransition transition: SKTransition, incomingPointOfView pointOfView: SCNNode?, completionHandler: (() -> Void)?)

pointOfView引数に、カメラ(SCNCamera)をセットしたノードを設定すると、移動先のシーンにおける視点をそのカメラに変更出来るようです(本サンプルでは必要ないので nil にしてあります)。またcompletionHandlerに、ブロックとして何らかの処理を設定すると、画面遷移の完了後にそれを実行してくれるようです(本サンプルでは nil にしてあります)。

このサンプルを実行すると下図のようになります。立方体、球体をタップする度に、2つのシーン間をアニメーション付きで移動します。
f:id:fleron:20151230230450p:plain

このように、SceneKitでも SKTransition を用いた画面遷移を実装することが出来ました。…しかしながら、やはりビューコントローラーのtouchBeganで全てのタッチイベントを一元処理するという方式はスマートではありません(シーンの数がもっと増えた場合、面倒な事になるのは想像がつきます)。やはりSpriteKitと同様に、タッチイベントの処理は各シーンにそれぞれ任せる方式に出来ればと思います。この点は課題です。