工場裏のアーカイブス

素人による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と同様に、タッチイベントの処理は各シーンにそれぞれ任せる方式に出来ればと思います。この点は課題です。

Swift 1.2におけるタッチイベント系メソッドの変更点

最近は本業の忙しさもあり、少々プログラミングから離れていたのですが、久々にXcodeを(更新があったのでアップデートしてから)起動してみたところ、以前は問題なくビルド出来たプロジェクト(Swift使用)でエラーが出てしまう問題に直面しました。

調べてみると、どうやらXcode 6.3からSwiftのバージョンが1.2にアップグレードし、それに伴っていくつか言語仕様の変更が生じたようです。例えばtouchesBeganなどのタッチイベント系のメソッドでは、以下のような引数touchesの型変更が行われており、前述のエラーはこのことが原因であったようです。

//従来のtouchesBeganメソッド
override func touchesBegan(touches: NSSet, withEvent event: UIEvent){

//Swift 1.2からの新しいtouchesBeganメソッド
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {

このSetというデータ型は、NSSetの代替としてSwift 1.2から新しく追加されたものですが、NSSetのようにanyObject()は利用出来ないようです。そのため例えば、touchesBeganメソッドでタッチ位置の座標を検出する場合、以下のようにSwift 1.2では従来と異なる記述が必要となるようです。

//従来のtouchesBeganメソッドによる、タッチ位置の座標検出例
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
        let p = touches.anyObject()!.locationInView(self.view) //変数pにタッチ位置の座標が入る
        ……

///Swift 1.2からの新しいtouchesBeganメソッドによる、タッチ位置の座標検出例
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
        if let touch = touches.first as? UITouch{
            let p = touch.locationInView(self.view) //変数pにタッチ位置の座標が入る
            ……


(2015/12/29 追記)
2015/9/16にはSwift 2.0がリリースされており、それに伴い、またタッチイベント系のメソッドにわずかな変更が生じたようです。

//Swift 1.2のtouchesBeganメソッド
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {

//Swift 2.0の新しいtouchesBeganメソッド
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {

Swift 2.0で、touchesBeganメソッドによりタッチ位置の座標を検出するためには、以下のようにすればOKのようです。

//Swift 2.0からの新しいtouchesBeganメソッドによる、タッチ位置の座標検出例
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
       if let touch = touches.first{
            let p = touch.locationInView(self.view) //変数pにタッチ位置の座標が入る
            ……

SceneKitにおけるテクスチャの利用(1)

本記事では、SceneKitにおけるテクスチャの利用法についてメモします。

SCNGeometry系の立体モデルへのテクスチャ貼付

SCNBox、SCNSphereなど、SCNGeometryより派生する汎用立体モデルに、以下の画像をテクスチャとして貼り付けてみます。

f:id:fleron:20150330220119p:plain

単純な貼り付けだけなら非常に簡単です。まずプロジェクトにテクスチャ用の画像ファイル(例えば texture.png とします)を追加します。そして以下のコードのように、立体モデルのオブジェクトを作成し、マテリアルのdiffuse.contentsプロパティにUIImageで画像を設定するだけです。

//SCNBoxにテクスチャを貼り付ける例
let box = SCNBox(width: 1.0, height: 1.0, length: 1.0, chamferRadius: 0.0)
box.firstMaterial?.diffuse.contents = UIImage(named: "texture")


それぞれの立体モデルには、あらかじめテクスチャ座標が設定されており、それに従いテクスチャが貼り付けられます。試しに、様々な立体モデルにテクスチャを貼って並べてみました(以下の記事のサンプルコードをベースに作成)。
Introduction To SceneKit – Part 2 - We ❤ Swift
f:id:fleron:20150323235526p:plain
左から順にSphere、Plane、Box、Pyramid、Cylinder、Cone、Torus、Tube、Capsuleです。それぞれの立体モデルでどのようにテクスチャが貼り付けられるのか、よく分かると思います。

このようにSCNGeometry系の立体モデルには手軽にテクスチャを貼れるのですが、その反面融通はきかず、あらかじめ設定されたテクスチャ座標は原則的に変更不能であるようです(特殊なテクニックを使えば変更出来る可能性もありますが、少なくとも標準的なSceneKitの範疇では無理なようです)。

そのため、例えばSCNBoxでは全ての面に同じテクスチャを貼ることしか出来ません。各面に異なるテクスチャを貼ってサイコロを作ったり、特定の面にだけテクスチャを貼ったり…ということは出来ないようです。

(2015/04/07 追記)SCNGeometry系の立体モデルでは、モデルを構成する各面(各要素)に、異なるマテリアルを設定可能であることを知りました(materials プロパティに SCNMaterial の配列を設定)。これを利用すれば、SCNBoxの各面に異なるテクスチャを貼ったり、特定の面にだけテクスチャを貼ったりすることは可能となります。誤った情報を失礼致しました。
 

COLLADA(.dae)ファイルによる、外部モデルデータのインポート

SceneKitで自由にテクスチャを貼ったモデルを扱いたい場合は、外部の3Dモデリングソフトでモデルを作成してテクスチャを貼り、そのデータをSceneKitにインポートするのが確実な方法のようです。SceneKitはCOLLADAファイル(拡張子 .dae)形式のモデルデータのインポートに対応しているようです。
COLLADA - ウィキペディア

3Dモデリングソフトは、COLLADAファイルの出力に対応していれば何でも良いのですが、本記事では「SketchUp」というソフトを利用する例について紹介してみたいと思います。SketchUpは非常に直感的で分かりやすい操作によるモデリングが可能であり、非商用目的であれば無償版(SketchUp Make)を利用することが出来ます。無償版でも充分な機能が備わっており、テクスチャの貼り付けやCOLLADAファイルの出力も可能です。
SketchUp公式サイト
SketchUp - ウィキペディア

ここではSketchUpで下図のように、立方体の各面に異なるテクスチャを貼ったサイコロのモデルを作成したとします。このモデルをSceneKitにインポートする手順についてメモします。
f:id:fleron:20150331001058p:plain
 

  • まずSketchUpのメニューで「ファイル」→「エクスポート」→「3Dモデル…」を選択し、モデルデータをCOLLADAファイルとして出力します(ここでは「dice」という名前で出力)。下図のようにCOLLADAファイルと、テクスチャ画像が格納されたフォルダが生成されます。

f:id:fleron:20150331003346p:plain
 

  • 新しいフォルダを作成し、生成したCOLLADAファイルとテクスチャ画像フォルダを格納します。そしてこの新しいフォルダの名前を「〜.scnassets」という名前に変更します(ここでは「dicemodel.scnassets」とします)。下図のように確認ダイアログが表示されますが「追加」ボタンをクリックすればOKです。

f:id:fleron:20150331003832p:plain
 

  • SceneKitのプロジェクトに、メニューの「File」→「Add File to 〜」などから、先程作成した「〜.scnassets」フォルダを追加します。これでこのフォルダが、モデルデータとテクスチャをまとめたアセットとしてプロジェクトに認識されます(現在のところ、Xcode上だけで .scnassets 形式のアセットを作成することは出来ず、このように名前を変えたフォルダを外部から追加する手順が必要なようです)。

f:id:fleron:20150331005000p:plain
 

  • あとは必要なコードを書くだけです。以下にサンプルコードを示します。
//COLLADAファイルをSCNSceneとしてインポート
let diceScene = SCNScene(named: "dicemodel.scnassets/dice.dae")!

//インポートしたシーンから、サイコロのモデルをノードとして抽出
//SketchUpの出力では、デフォルトのノード名が「SketchUp」となっている。後述するエディタで編集も可能
let diceNode = diceScene.rootNode.childNodeWithName("SketchUp", recursively: true)!

//後は、普通のノードと同様に、プロパティ設定やシーンへの追加をすればOK
diceNode.position = SCNVector3(x: 0.0, y : 30.0, z : 0.0)
self.scnView?.scene?.rootNode.addChildNode(diceNode)  
diceNode.physicsBody = SCNPhysicsBody.dynamicBody()


実はCOLLADAファイルは3Dモデルだけではなく、カメラや光源などの情報も含めることが可能な形式であり、そのままSCNSceneとしてインポートして、シーンとして利用することが可能です。しかし、必要なシーンは既に準備済みであり、ただ3Dモデルのみを利用したい場合は、サンプルコードのように3Dモデルをノードとして抽出することが出来ます。

後は通常のノードと同様に、必要なプロパティを設定してシーンに追加すればOKです。もちろん物理ボディの設定なども可能です。実際にCOLLADAファイルから、サイコロのモデルを抽出して表示させてみた例を下図に示します(適当な床の上に乗せてみました)。
f:id:fleron:20150331015011p:plain
 

  • なお補足的ですが、プロジェクト上でCOLLADAファイル(〜.dae)をクリックすると、下図のようなエディタが起動します。このエディタでは3Dモデルの観察、ノードの名前変更、更には簡易的なデータ編集も行うことが出来るようです。

f:id:fleron:20150331011126p:plain

SceneKitにおけるパーティクルシステムの利用

SceneKitにはパーティクルシステムが用意されており、炎、煙、爆発、雨などのエフェクトを手軽に美しく表現することが可能です。本記事ではその利用法の例について簡単にメモします。
 

パーティクルファイルを利用する方法

ファイルをプロジェクトへ追加する
  • Xcode(本記事執筆時点:Version 6.1.1)には、下図のようにリソースとして「SceneKit Particle System」というファイルが用意されています。これを利用すると、比較的簡単にパーティクルシステムを利用することが可能になります。まずは新規ファイルとして、これをSceneKitのプロジェクトに追加して行きます。

f:id:fleron:20150210000250p:plain

  • まず下図のダイアログが表示されます。SceneKitではパーティクルシステムの様々なテンプレートが用意されており、ここではそれらを選択することが出来ます。本記事では「Fire」を選択します。なお、テンプレートの一覧については本記事の末尾にまとめてあります。

f:id:fleron:20150210001013p:plain

  • 続いて、ファイル名を入力するダイアログが表示されますので、分かりやすい名前を付けます(本記事ではデフォルトの「MyParticleSystem」という名前のままにします)。これでプロジェクトに「〜.scnp」というパーティクルファイル、およびパーティクル表示用の画像ファイル(テンプレートにより異なる。Fireの場合は「Spark.png」)が追加されます。

  • パーティクルファイルをプロジェクト上で選択すると、下図のようなエディタが起動します。右側のパネルで様々なプロパティを設定し、その効果を視覚的に確認しながら、パーティクルシステムにより望みのエフェクトが得られるよう調節することが出来ます(本記事ではデフォルトのままにします)。

f:id:fleron:20150210222342p:plain

実際にパーティクルを表示させる

先程のリソースファイルを用いて、実際にパーティクルを表示させる簡単なサンプルコードを以下に示します。

//GameViewController.swiftの中身を書き換え
import UIKit
import QuartzCore
import SceneKit

class GameViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let scene = SCNScene()
        
        let cameraNode = SCNNode()
        cameraNode.camera = SCNCamera()
        scene.rootNode.addChildNode(cameraNode)
        cameraNode.position = SCNVector3(x: 0, y: 0, z: 20)
        
        let pyramid = SCNPyramid(width: 2.0, height: 2.0, length: 2.0)
        pyramid.firstMaterial!.diffuse.contents = UIColor(red: 0.8, green: 0.2, blue: 0.2, alpha: 1.0)
        let pyramidNode = SCNNode()
        pyramidNode.geometry = pyramid
        pyramidNode.position = SCNVector3(x: 0.0, y: 0, z: 0)
        scene.rootNode.addChildNode(pyramidNode)
        pyramidNode.runAction(SCNAction.repeatActionForever(
            SCNAction.rotateByX(0.1, y: 0.2, z: 0.0, duration: 2.0)
        ))
        
        //パーティクルシステムのオブジェクト生成、およびノードへの追加
        let fire = SCNParticleSystem(named: "MyParticleSystem.scnp", inDirectory: "")
        pyramidNode.addParticleSystem(fire)

        let scnView = self.view as SCNView
        scnView.scene = scene
        scnView.allowsCameraControl = true
        scnView.autoenablesDefaultLighting = true
        scnView.backgroundColor = UIColor(red: 0.6, green: 0.6, blue: 0.8, alpha: 1.0)
    }
    
    override func shouldAutorotate() -> Bool {
        return true
    }
    
    override func prefersStatusBarHidden() -> Bool {
        return true
    }
    
    override func supportedInterfaceOrientations() -> Int {
        if UIDevice.currentDevice().userInterfaceIdiom == .Phone {
            return Int(UIInterfaceOrientationMask.AllButUpsideDown.rawValue)
        } else {
            return Int(UIInterfaceOrientationMask.All.rawValue)
        }
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}


ポイントとなるのはこの部分です。

let fire = SCNParticleSystem(named: "MyParticleSystem.scnp", inDirectory: "")
pyramidNode.addParticleSystem(fire)

パーティクルを表示させるには、まずはSCNParticleSystemメソッドでファイルを読み込み、パーティクルシステムのオブジェクトを生成します。なお、このメソッドには「inDirectory」という引数がありますが、プロジェクトに追加したファイルを利用する場合は空欄(””)で問題ないようです。

そしてオブジェクトを、立体モデルなどのノードにaddParticleSystemメソッドで取り付けることにより、そのノードからパーティクルが放出されるようになります。SceneKitではこのような形でパーティクルシステムを利用します。

サンプルコードを実行すると、ピラミッド型の立体モデルから「Fire」テンプレートのパーティクルが放出され、あたかもピラミッドが炎上するようなシーンが表示されます。
f:id:fleron:20150210224702p:plain
 

コード上で設定を行う方法

パーティクルファイルを用いず、以下のようにコード上でオブジェクト生成、およびプロパティ設定を全て行うことも可能です。

let parSys = SCNParticleSystem()

parSys.birthRate = 100
parSys.particleColor = UIColor.yellowColor()
parSys.emissionDuration = 0.1
parSys.particleSize = 0.05
//…………以下省略


もちろんパーティクルファイルを用いる場合でも、同様にオブジェクトのプロパティ設定をコード上で変更することが出来ます。例えばパーティクルの放出をオフにしたい場合は、「birthRate」プロパティの値を0に変更するという方法が使えます。
 

パーティクルシステムのテンプレート一覧

最後にパーティクルシステムのテンプレート(XcodeのVersion 6.1.1時点)の概要についてまとめます。

  • Bokeh
    • 黄色い光の粒が、湧き上がって消えていくようなエフェクトです。
    • パーティクル表示用の画像ファイル:bokeh.png

f:id:fleron:20150211000423p:plain

  • Confetti
    • 紙吹雪が舞い落ちるエフェクトです。なぜかパーティクルエディタ上ではグラフィックが表示されないようです(バグ?情報不足により不明)。ただし実際のプロジェクト上で用いると、下図のようにきちんと表示されます。
    • パーティクル表示用の画像ファイル:confetti.png

f:id:fleron:20150211001118p:plain

  • Fire
    • 炎が燃え上がるエフェクトです。
    • パーティクル表示用の画像ファイル:spark.png(Rain、Reactorテンプレートと共通)

f:id:fleron:20150211002233p:plain

  • Leafs
    • このテンプレートは選択しても、何のファイルも生成されないようです (バグ?情報不足により不明)


  • Rain
    • 雨が降り落ちるエフェクトです。
    • パーティクル表示用の画像ファイル:spark.png(Fire、Reactorテンプレートと共通)

f:id:fleron:20150211002807p:plain

  • Reactor
    • 激しい閃光のようなエフェクトです。
    • パーティクル表示用の画像ファイル:spark.png(Fire、Rainテンプレートと共通)

f:id:fleron:20150211002910p:plain

  • Smoke
    • 煙が立ち昇るエフェクトです。
    • パーティクル表示用の画像ファイル:smoke.png

f:id:fleron:20150211003009p:plain

  • Stars
    • 画面奥から手前に向かって、光の粒(星?)が飛んでくるようなエフェクトです。
    • パーティクル表示用の画像ファイル:star.png

f:id:fleron:20150211003058p:plain

SceneKitに触れてみる(2)

本記事はSceneKitに触れてみる(1)の続きとなります。こちらの記事で公開した拙作のSceneKitサンプルアプリについて、本記事ではソースコードの要点などをまとめてみます。

SceneKitのテンプレートプロジェクトについて

ソースコードの前に、まずはSpriteKitのテンプレート(Gameテンプレートで選択)で生成されるプロジェクトを眺めてみます。storyboardを見ると、あらかじめビューコントローラー(GameViewController)が用意されており、「SceneKit View」という種類のビューが貼り付けられています。

SceneKit Viewは下図のようにカスタムクラスが「SCNView」となっており、これがSceneKitのコンテンツを描画するための基本ビューとなります。SceneKitではこのSCNViewの上にシーン(SCNScene)を配置し、そこに立体モデル、カメラ、光源などの様々なノード(SCNNode)を配置することによって画面を作っていきます。この辺りの考え方はSpriteKitとほぼ同様です。
f:id:fleron:20150122223651p:plain

シーン(SCNScene)の作成

それでは順番に、ソースコードの要点についてまとめていきます。まずは冒頭の viewDidLoad メソッドの周辺です。

    weak var scnView : SCNView?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let scene = SCNScene()
        scnView = self.view as? SCNView
        scnView?.scene = scene
        scnView?.backgroundColor = UIColor(red: 0.2, green: 0.2, blue: 0.6, alpha: 1.0)
        scnView?.allowsCameraControl = true;
        
        setObject()
        setCamera()
        setLight()
    }

ここではscnViewというプロパティを用意してビューへの参照を取得し、空のシーン(SCNScene)を作成してビューに配置しています(sceneプロパティへの代入)。また背景色の設定もここで行っています。

そしてビュー(SCNView)には「allowsCameraControl」というプロパティがあり、これをtrueにするだけで、画面タッチ操作でカメラ(視点)を色々操作する機能が備わるようになります(以下は操作の例です。他にもあるかもしれません)。

  • 1本指ドラッグ:視点の3次元回転
  • 2本指ドラッグ:パン(視点の上下左右への移動)
  • ピンチ:ズームイン or ズームアウト
  • 回転:視点の軸回転
  • ダブルタップ:視点のリセット

細かい設定(特定の操作のみ無効にするなど)は残念ながら出来ないようですが、このような操作を自前実装するのは結構大変なので、かなり便利です。

そして、シーンへ各種ノードを配置する処理については、ソースを見やすくするために、それぞれ別々のメソッドにまとめています。以降で順番に触れていきます。

シーンへの立体図形の配置

シーンに初期の立体モデル(本アプリでは、半透明の箱および地面)を配置する処理は、setObjectメソッドで行っています。

func setObject(){
        let generator = SCNBox(width: 6.0, height: 6.0, length: 6.0, chamferRadius: 0.0)
        generator.firstMaterial?.diffuse.contents = UIColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 0.7)
        let generatorNode = SCNNode()
        generatorNode.geometry = generator
        generatorNode.position = SCNVector3(x: 0, y: 30, z: 0)
        generatorNode.name = "generator"
        self.scnView?.scene?.rootNode.addChildNode(generatorNode)
        
        let ground = SCNBox(width: 200.0, height: 2.0, length: 200.0, chamferRadius: 0.0)
        ground.firstMaterial?.diffuse.contents = UIColor(red: 0.8, green: 0.8, blue: 0.3, alpha: 1.0)
        let groundNode = SCNNode()
        groundNode.geometry = ground
        groundNode.position = SCNVector3(x: 0, y: 0, z: 0)
        groundNode.name = "ground"
        groundNode.physicsBody = SCNPhysicsBody.staticBody()
        groundNode.physicsBody?.physicsShape = SCNPhysicsShape(node: groundNode, options: nil)
        self.scnView?.scene?.rootNode.addChildNode(groundNode)
    }

SpriteKitではSKSpriteNode、SKLabelNode、SKShapeNodeなど、用途に応じて様々な種類のノードを使い分けていました。しかしSceneKitでは、まず空のノード(SCNNode)を生成し、そこに立体モデル、カメラ、光源など必要なオブジェクトを「取り付ける」ようなイメージでノードを扱うようです。

ここではまず、箱型の立体モデルのオブジェクトをSCNBoxクラスにより生成します。SceneKitにはこのSCNBoxのように、箱、球、円柱などの汎用的な立体モデルを生成するクラスが用意されています(SCNGeometryクラスより派生、他の立体も後で登場します)。そしてfirstMaterialプロパティで色(厳密には拡散光の色)を設定します。ここで色のアルファ値を1.0未満にすると、立体モデルを半透明化することが出来ます。

あとはオブジェクトを各ノードのgeometryプロパティに代入して取り付け、ノードの必要なプロパティ(位置、名前など)を設定して行きます。

ここで、地面ノード(groundNode)に対しては物理ボディを設定し、物理エンジンによる制御が働くようにしています。地面用なので、重力や他ボディの衝突影響などを受けないよう、staticBodyメソッドで物理ボディを生成します。そして、SCNPhysicsShapeで物理ボディの形状を規定します(ここではgroundNodeの形状をそのまま適用しています)。

そしてビューに配置したシーンに対して、addChildNodeメソッドでノードを配置します。この辺りはSpriteKitと同じ要領です。

シーンへのカメラの配置

シーンに視点となるカメラを配置する処理は、setCameraメソッドで行っています。

func setCamera(){
        let cameraNode = SCNNode()
        cameraNode.camera = SCNCamera()
        self.scnView?.scene?.rootNode.addChildNode(cameraNode)
        cameraNode.position = SCNVector3(x: 0, y: 50, z: 50)
        cameraNode.rotation = SCNVector4(x: 1, y: 0, z: 0, w: -0.5)
    }

先程と同様まずは空のノードを生成します。そして今度はSCNCameraクラスによってカメラオブジェクトを生成し、ノードのcameraプロパティに代入して取り付けて、シーンに配置します。そしてカメラはデフォルトではZ軸のマイナス方向を向いているので、カメラノードの位置および向きを調節して、先ほど配置した立体モデルがアプリ起動時にうまく画面に収まるようにしています。

シーンへの光源の配置

シーンに光源を配置する処理は、setLightメソッドで行っています。

func setLight(){
        let ambientLightNode = SCNNode()
        ambientLightNode.light = SCNLight()
        ambientLightNode.light?.type = SCNLightTypeAmbient
        ambientLightNode.light?.color = UIColor.darkGrayColor()
        self.scnView?.scene?.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: 0, y: 50, z: 10)
        
        if let ground = self.scnView?.scene?.rootNode.childNodeWithName("ground", recursively: false){
            let cons = SCNLookAtConstraint(target: ground)
            cons.influenceFactor = 1.0
            lightNode.constraints = [cons]
        }
        
        self.scnView?.scene?.rootNode.addChildNode(lightNode)
    }

まず空のノードを生成し、光源オブジェクトを生成(SCNLightクラス)してノードに取り付け、シーンに配置する…という要領はこれまで同様ですが、ここでは種類(typeプロパティ)の異なる光源2つを用いています。

まず、typeプロパティにSCNLightTypeAmbientを設定した光源は、環境光として働きます。これは空間全体に一定の明るさを与えるような光であり、位置や方向に依存しないので、光の色(colorプロパティ)のみを設定します。

また、typeプロパティにSCNLightTypeSpotを設定した光源は、スポットライトとして働きます。これは特定の方向へ円錐状に広がる光であり、図にすると以下のようになります(非常に雑ですが)。
f:id:fleron:20150125002332p:plain
spotOuterAngleプロパティで円錐の広がる角度を設定します。また、castsShadowプロパティをtrueにすると、これだけでスポットライトにより生じる立体モデルの影が、他の立体モデルに投影されるようになります(個人的には、この機能には結構感激しました)。

ここで、スポットライトもデフォルトではマイナスZ軸方向を向いていますが、この向きを変えて、光源の位置(x: 0, y: 50, z: 10)から地面の中央を照らすようにすることを考えます。カメラのときと同様にrotationプロパティで向きを調節しても良いのですが、実はSceneKitには、あるノードがターゲットとなるノードの方向を常に向くよう束縛する仕組みが用意されています。ソース中にあるSCNLookAtConstraintがそれです。

ここではシーンに配置された地面ノードへの参照を取得し、それをターゲットとする方向束縛をスポットライトノードに設定しています(constraintプロパティへの代入)。これでスポットライトが常に地面の中央を照らすようになります。

なお、今回ターゲットとした地面は固定されており動きませんが、動き回るようなノードをターゲットにすることも出来ます。例えばカメラに方向束縛を設定し、動き回るキャラクターをカメラが常に追いかけるような状況設定も可能です。influenceFactorはそのような場合に影響するプロパティであり、値が1.0から小さくなるのに従って、束縛されたノードがターゲットの移動方向へ向き直るまでにタイムラグが生じるようになります。

画面タップ時の処理

本アプリでは、配置された半透明の箱がタップされると、ランダムな種類・色の立体モデルを新たに生成します。また、この新たな立体モデルはタップされると、フェードアウトして消滅します。これらの処理はtouchesBeganメソッドを用いて実装しています。

override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
        let p = touches.anyObject()!.locationInView(self.scnView)
        if let hitResults = self.scnView?.hitTest(p, options: nil) {
            if hitResults.count > 0 {
                let result = hitResults.first?.node
                
                if result?.name? == "generator"{
                    let geom = generateRandomGeometry()
                    geom.firstMaterial?.diffuse.contents =
                        UIColor(red: colorValue(), green: colorValue(), blue: colorValue(), alpha: 1.0)
                    let geomNode = SCNNode(geometry: geom)
                    geomNode.position = SCNVector3(x: 0, y: 30, z: 0)
                    geomNode.name = "geometry"
                    geomNode.physicsBody = SCNPhysicsBody.dynamicBody()
                    geomNode.physicsBody?.physicsShape = SCNPhysicsShape(node: geomNode, options: nil)
                    self.scnView?.scene?.rootNode.addChildNode(geomNode)
                }else if result?.name? == "geometry"{
                    result!.runAction(SCNAction.sequence([
                        SCNAction.fadeOutWithDuration(0.5),
                        SCNAction.removeFromParentNode()
                        ]))
                }
            }
        }
    }
    
    func generateRandomGeometry() -> SCNGeometry{
        var kind = arc4random() % 6
        
        switch kind{
            case 0:
                let pyramid = SCNPyramid(width: 3.0, height: 3.0, length: 3.0)
                return pyramid
            case 1:
                let cylinder = SCNCylinder(radius: 1.5, height: 3.0)
                return cylinder
            case 2:
                let sphere = SCNSphere(radius: 1.5)
                return sphere
            case 3:
                let torous = SCNTorus(ringRadius: 1.5, pipeRadius: 0.5)
                return torous
            case 4:
                let chamferBox = SCNBox(width: 3.0, height: 3.0, length: 3.0, chamferRadius: 0.8)
                return chamferBox
            default:
                let box = SCNBox(width: 3.0, height: 3.0, length: 3.0, chamferRadius: 0.0)
                return box
        }
    }
    
    func colorValue() -> CGFloat{
        let rand = CGFloat(arc4random() % 10000)
        return rand / 20000.0 + 0.5
    }


立体モデルへのタップ検出

iPhone画面(すなわちビュー)へのタッチイベントが発生し、touchesBeganメソッドが呼び出されたら、まずはビュー上のタップ位置(座標)を取得します。そして、このタップ位置をビューのhitTestメソッドに引数として与えると、タップ位置と重なった全ての立体モデル(SCNGeometryの派生クラスのオブジェクト)のノード一覧を、配列として取得することが出来ます。

hitTestメソッドの動作は、与えられた位置から画面内の3D空間へ垂直に光線を飛ばし、その光線が貫く立体モデルを順次検出するようなイメージです。一覧配列には、検出順にノードが格納される(すなわち、視点に一番近い立体モデルノードが先頭要素になる)ようです。また、実際は一覧配列の型はSCNNodeそのものではなく、SCNHitTestResultという他の様々な情報も含んだクラスであり、このnodeプロパティからノードへの参照を取得します。

そして一覧配列の要素数が0ではなく、タップ位置に立体モデルが存在する場合は、一覧配列の先頭のノード(前述のように、視点に一番近い立体モデル)をresult変数に取得しています。あとはこのノードの名前(nameプロパティ)によって、処理の場合分けを行います。

半透明の箱タップ時の処理(新規立体モデルの追加生成)

半透明の箱(名前:”generator”)へのタップが検出されたら、generateRandomGeometryという自作メソッドを呼び出して、SCNGeometry系列の様々な汎用立体モデルオブジェクト(ピラミッド、円柱、球、トーラス、箱)をランダムに選んで生成しています。なお箱(SCNBox)については、chamferRadius引数の値を大きくすると、それに応じて角を丸めることが可能です。また、立体モデルの色(拡散光の色)は、0.5〜1.0の実数値をランダムに返すcolorValueという自作メソッドを用いて設定しています。

そして、この立体モデルをノードに取り付けて必要なプロパティを設定します。初期位置は半透明の箱と同じ位置とし、名前は”geometry”とします。そして物理ボディを設定するのですが、ここでは重力や衝突の影響を受けるようdynamicBodyメソッドを用います。

新規立体モデルタップ時の処理(フェードアウト消滅するアクション)

生成された新規立体モデル(名前:”geometry”)へのタップが検出されたら、そのノードにアクション(SCNActionクラス)をrunActionメソッドで与えて実行させます。

ここではfadeOutwithDuration、removeFromParentNodeアクションをシーケンス化して「ノードを指定秒数でフェードアウトさせた後に、親(この場合はシーン)から除去する」というアクションを与えています。これによりタップされた新規立体モデルは消滅します。

SceneKitにおけるアクションは、ほとんどSpriteKitと同じような感覚で扱うことが出来るようです。

SceneKitに触れてみる(1)

iOS 8に対応したXcode 6では、プロジェクトテンプレートの種類が変わり、OpenGL ES2 や SpriteKit を用いたアプリ開発は「Game」テンプレートに統合され、下図のようにプロジェクト作成時に「Game Technology」欄でフレームワークを選択する方式となりました。
f:id:fleron:20150120230541p:plain

そして、ここには「SceneKit」「Metal」という、これまでに無かったフレームワークも新たに登場しています。このうちSceneKitは、言わば3Dグラフィックス版のSpriteKitであり、前々から「SpriteKitと同じような手軽さで、3Dゲームなどのアプリ作れるツールは無いかな…」と願っていた身としては、まさに待望のツールです。

早速色々と触れてみたのですが、SceneKit では 3D空間のシーンにカメラ、光源、立体図形などをノードとして配置することによって、比較的手軽に3Dグラフィックスを用いたアプリを作成することが可能です(少なくともOpenGL ES2 + GLKitなどよりもよっぽど)。

またSpriteKitと同様に、ノードへの様々なアクションの設定や、物理エンジンやパーティクルシステムの利用も可能であり、上手く使いこなせばかなり凝った3Dゲームを作成することも夢ではなさそうです。

SceneKitの自作サンプル公開

まだまだSceneKitはリリースされてからの日が浅く、ネットの参考情報も限られている状況(日本語は特に)のようです。そこで本記事では、どこかで誰かの役に立つこともあるかもと願い、拙いながらも自作のサンプルアプリを公開してみたいと思います(下図、言語はSwift)。
f:id:fleron:20150121232136p:plain

このアプリでは画面内に半透明の箱が浮かんでおり、これをタップすると様々な形状、色の立体モデルがランダムに生成されます。立体モデルは物理エンジンによって落下し、互いに接触して積み重なったり散らかったりします。そして立体モデルをタップするとフェードアウトして消滅します。また、斜め上方から地面中央を照らすスポットライトによって、各立体モデルの影が投影されます。ただこれだけのアプリですが、SceneKitの機能を色々と盛り込んでいます。

ソースコードを以下に示します。実際に動かすためには、まずXcode(本記事執筆時点:Version 6.1.1)で「Game」テンプレートを選択し、「Language」欄は「Swift」、「Game Technology」欄は「SceneKit」を選択してプロジェクトを作成します。あとは「GameViewController.swift」の中身をこのソースコードに書き換えればOKです。

import UIKit
import QuartzCore
import SceneKit

class GameViewController: UIViewController {

    weak var scnView : SCNView?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let scene = SCNScene()
        scnView = self.view as? SCNView
        scnView?.scene = scene
        scnView?.backgroundColor = UIColor(red: 0.2, green: 0.2, blue: 0.6, alpha: 1.0)
        scnView?.allowsCameraControl = true;
        
        setObject()
        setCamera()
        setLight()
    }
    
    func setObject(){
        let generator = SCNBox(width: 6.0, height: 6.0, length: 6.0, chamferRadius: 0.0)
        generator.firstMaterial?.diffuse.contents = UIColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 0.7)
        let generatorNode = SCNNode()
        generatorNode.geometry = generator
        generatorNode.position = SCNVector3(x: 0, y: 30, z: 0)
        generatorNode.name = "generator"
        self.scnView?.scene?.rootNode.addChildNode(generatorNode)
        
        let ground = SCNBox(width: 200.0, height: 2.0, length: 200.0, chamferRadius: 0.0)
        ground.firstMaterial?.diffuse.contents = UIColor(red: 0.8, green: 0.8, blue: 0.3, alpha: 1.0)
        let groundNode = SCNNode()
        groundNode.geometry = ground
        groundNode.position = SCNVector3(x: 0, y: 0, z: 0)
        groundNode.name = "ground"
        groundNode.physicsBody = SCNPhysicsBody.staticBody()
        groundNode.physicsBody?.physicsShape = SCNPhysicsShape(node: groundNode, options: nil)
        self.scnView?.scene?.rootNode.addChildNode(groundNode)
    }
    
    func setCamera(){
        let cameraNode = SCNNode()
        cameraNode.camera = SCNCamera()
        self.scnView?.scene?.rootNode.addChildNode(cameraNode)
        cameraNode.position = SCNVector3(x: 0, y: 50, z: 50)
        cameraNode.rotation = SCNVector4(x: 1, y: 0, z: 0, w: -0.5)
    }
    
    func setLight(){
        let ambientLightNode = SCNNode()
        ambientLightNode.light = SCNLight()
        ambientLightNode.light?.type = SCNLightTypeAmbient
        ambientLightNode.light?.color = UIColor.darkGrayColor()
        self.scnView?.scene?.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: 0, y: 50, z: 10)
        
        if let ground = self.scnView?.scene?.rootNode.childNodeWithName("ground", recursively: false){
            let cons = SCNLookAtConstraint(target: ground)
            cons.influenceFactor = 1.0
            lightNode.constraints = [cons]
        }
        
        self.scnView?.scene?.rootNode.addChildNode(lightNode)
    }
    
    override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
        let p = touches.anyObject()!.locationInView(self.scnView)
        if let hitResults = self.scnView?.hitTest(p, options: nil) {
            if hitResults.count > 0 {
                let result = hitResults.first?.node
                
                if result?.name? == "generator"{
                    let geom = generateRandomGeometry()
                    geom.firstMaterial?.diffuse.contents =
                        UIColor(red: colorValue(), green: colorValue(), blue: colorValue(), alpha: 1.0)
                    let geomNode = SCNNode(geometry: geom)
                    geomNode.position = SCNVector3(x: 0, y: 30, z: 0)
                    geomNode.name = "geometry"
                    geomNode.physicsBody = SCNPhysicsBody.dynamicBody()
                    geomNode.physicsBody?.physicsShape = SCNPhysicsShape(node: geomNode, options: nil)
                    self.scnView?.scene?.rootNode.addChildNode(geomNode)
                }else if result?.name? == "geometry"{
                    result!.runAction(SCNAction.sequence([
                        SCNAction.fadeOutWithDuration(0.5),
                        SCNAction.removeFromParentNode()
                        ]))
                }
            }
        }
    }
    
    func generateRandomGeometry() -> SCNGeometry{
        var kind = arc4random() % 6
        
        switch kind{
            case 0:
                let pyramid = SCNPyramid(width: 3.0, height: 3.0, length: 3.0)
                return pyramid
            case 1:
                let cylinder = SCNCylinder(radius: 1.5, height: 3.0)
                return cylinder
            case 2:
                let sphere = SCNSphere(radius: 1.5)
                return sphere
            case 3:
                let torous = SCNTorus(ringRadius: 1.5, pipeRadius: 0.5)
                return torous
            case 4:
                let chamferBox = SCNBox(width: 3.0, height: 3.0, length: 3.0, chamferRadius: 0.8)
                return chamferBox
            default:
                let box = SCNBox(width: 3.0, height: 3.0, length: 3.0, chamferRadius: 0.0)
                return box
        }
    }
    
    func colorValue() -> CGFloat{
        let rand = CGFloat(arc4random() % 10000)
        return rand / 20000.0 + 0.5
    }
    
    override func shouldAutorotate() -> Bool {
        return true
    }
    
    override func prefersStatusBarHidden() -> Bool {
        return true
    }
    
    override func supportedInterfaceOrientations() -> Int {
        if UIDevice.currentDevice().userInterfaceIdiom == .Phone {
            return Int(UIInterfaceOrientationMask.AllButUpsideDown.rawValue)
        } else {
            return Int(UIInterfaceOrientationMask.All.rawValue)
        }
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Release any cached data, images, etc that aren't in use.
    }
}



ソースコードの要点などについては、次回の記事にまとめたいと思います。


(2015/05/27 追記)

Xcode 6.3からSwiftのバージョンが1.2となり、それに伴ってtouchesBegansなどのタッチイベント系メソッドに変更が生じました。Swift 1.2以降を使用する場合は、上記ソースのtouchesBegansメソッド部分を以下のように書き換えてください。

//Swift 1.2に対応したtouchBegansメソッドの修正
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
        
    if let touch = touches.first as? UITouch{
        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 == "generator"{
                    let geom = generateRandomGeometry()
                    geom.firstMaterial?.diffuse.contents =
                        UIColor(red: colorValue(), green: colorValue(), blue: colorValue(), alpha: 1.0)
                    let geomNode = SCNNode(geometry: geom)
                    geomNode.position = SCNVector3(x: 0, y: 30, z: 0)
                    geomNode.name = "geometry"
                    geomNode.physicsBody = SCNPhysicsBody.dynamicBody()
                    geomNode.physicsBody?.physicsShape = SCNPhysicsShape(node: geomNode, options: nil)
                    self.scnView?.scene?.rootNode.addChildNode(geomNode)
                }else if result?.name == "geometry"{
                    result!.runAction(SCNAction.sequence([
                        SCNAction.fadeOutWithDuration(0.5),
                        SCNAction.removeFromParentNode()
                    ]))
                }
            }
        }
    }
}

ポップアップするビューの作り方(2、Swift編)

※本記事はポップアップするビューの作り方(1)の続きとなります。

Swift で MJPopupViewControllerを使ってみる

最近は Swift にも触ってみたのですが、Objective-C に比べると個人的には色々書きやすく、参考にしている情報サイトなどもソースコードSwiftに移行している所がちらほらありますので、私も今後はSwift使いに転向しようと考えております。

ただしSwfitにはObjective-Cと互換性があり、Objective-Cで書かれたコードをSwiftから呼び出して利用することは容易に行えるそうです。そこで本記事では、前記事で紹介したMJPopupViewController(Objective-Cのコード)を、Swiftで利用してみたいと思います。
 

  • まずXcode(本記事ではVer 6.1)で、新しい「Single View Application」のプロジェクトを作成します(下図のように、言語をSwiftにするのを忘れずに)。

f:id:fleron:20150112165639p:plain

  • 前記事と同様に、MJPopupViewController の「source」フォルダに入っていた4ファイルをプロジェクトに追加します。ここで、SwiftのプロジェクトにObjective-Cのファイル(.m)を初めて追加するときには、下図のようなダイアログが表示されて、YESを選択すると「(プロジェクト名)-Bridging-Header.h」という名前のヘッダファイルが自動生成されます。これは「ブリッジングヘッダ」といい、Objective-Cファイル側の情報をSwiftへ橋渡しするためのものです。

f:id:fleron:20150112170806p:plain

  • ※プロジェクトにObjective-Cのファイルを「Add Files to 〜」のメニューから追加した場合、ダイアログが表示されずブリッジングヘッダが生成されないようです。この場合は自力でブリッジングヘッダを作成することになります。プロジェクトにヘッダファイル(名前は何でもOK。例えば「Bridging-Header.hなど」)を追加し、下図のようにプロジェクト設定の「Build Settings」の「Swift Compiler - Code Generation」にある「Objective-C Bridging Header」項目に、追加したヘッダファイルの名前を指定すればOKです。

f:id:fleron:20150112172258p:plain

  • ブリッジングヘッダでは、Swiftから呼び出したいObjective-Cコード用のヘッダファイルをインポートします。MJPopupViewControllerの場合は、以下の2つが必要となります。これだけでコード呼び出しの前準備はほぼ完了です。
#import "MJPopupBackgroundView.h"
#import "UIViewController+MJPopupViewController.h"

  • あとはプロジェクトにxibファイル、および操作用ビューコントローラーのファイルを追加して、ポップアップさせるUIView関連の設定を行います。この手順は前記事と同様です(ただしビューコントローラーのファイルは、当然Swiftコードで作成することを忘れずに)

  • これで、SwiftでもMJPopupViewControllerを利用する準備は全て整いました。あとはポップアップビューを呼び出したい場所で、以下のコードを実行するだけです。ブリッジングヘッダを用意さえしておけば、それ以上の特別な設定は必要なく、プロジェクト内のどのSwiftコードからでも呼び出すことが可能です。
//Swift版 MJPopupViewControllerの呼び出し
let popupView = PopupViewController(nibName:"PopupView", bundle:nil)
presentPopupViewController(popupView, animationType:MJPopupViewAnimationSlideBottomBottom)

 

  • ここで、前記事Objective-C版のコードと比較してみます。Objective-Cで定義されたクラスやメソッドを、そのままSwiftの文法で呼び出して利用出来ている様子が良く分かると思います。
//Objective-C版 MJPopupViewControllerの呼び出し
PopupViewController *popUpView = [[PopupViewController alloc]initWithNibName:@"PopupView" bundle:nil];
[self presentPopupViewController:popUpView animationType:MJPopupViewAnimationSlideBottomBottom];

ポップアップするビューの作り方(1)

iPhoneアプリ上で、必要なときだけピッカー(UIPickerView)などを出現させてユーザー入力を受け付け、入力が済んだら引っ込ませる…という動作を実装することを考えます。
 

アクションシートを用いる方法(iOS 8では面倒)

実装法の1つとして、ピッカーなどをUIActionSheetに乗せるという方法があり、以前の記事でも触れたことがあります。しかし、iOS 8の環境ではUIActionSheet(ついでにUIAlertViewも)は deprecated となり、この方法は使えなくなってしまいました(アクションシート自体は表示されるようですが、addSubview で乗せたものは表示されなくなってしまうようです)。

代わりに、iOS 8ではUIAlertControllerという、UIActionSheetおよびUIAlertViewの後継となる新しいクラスが追加されています。UIAlertControllerにもサブビューの追加は可能であり、ピッカーなどを乗せることは可能なようですが、当然これはiOS 7以前の環境では動作しません。iOS 7以前とiOS 8の両方に対応する場合は、iOSのバージョン次第でUIActionSheetとUIAlertControllerのどちらを用いるか場合分けをする…というやや面倒な手順が必要となってしまいます。
 

ポップアップビューを自作する方法(MJPopupViewController使用)

現環境で目的のような動作を実装するためには、ポップアップビュー(必要に応じて、飛び出したり引っ込んだりするビュー)を作り、そこにピッカーなどを載せる方法の方が楽だと思います。

Xcode 6.1ではポップアップビュー作成用のライブラリなどは用意されておらず、基本的には自力実装となりますが、有志による高品質なサンプルコードなどもネット上では多数公開されているようです。中でも「MJPopupViewController」は、自分で設定したUIViewをポップアップさせることが可能であり、比較的容易に、見栄え良くカスタマイズ性の高いポップアップビューが作成可能です(下図は付属デモサンプルのスクリーンショット)。
f:id:fleron:20141216235520p:plain

ここでは、この使用法についてメモします。

  • まず、「GitHub」からMJPopupViewControllerの一式をzipファイルとしてダウンロードします(リンク先の画面右側にある「Download ZIP」を選択)。zipファイルを解凍すると、中に様々なファイルが入っていますが、使うのは「source」フォルダに入っている以下の4ファイルです。
    • MJPopupBackgroundView.h
    • MJPopupBackgroundView.m
    • UIViewController+MJPopupViewController.h
    • UIViewController+MJPopupViewController.m

  • 続いて、Xcodeで新しいプロジェクトを作成し(ここではSingle View Applicationとして作成)、先ほどの4ファイルをプロジェクトに追加します。

  • 続いて、プロジェクトにxibファイルを追加します。以下のように「User Interface」の「View」を選択すると、あらかじめUIViewが配置されたxibファイルが追加されます(ここでは名前を「PopupView.xib」とします)。ここでデザインしたUIViewをポップアップさせます。

f:id:fleron:20141230222150p:plain

  • また、このUIViewを操作するためのクラスをプロジェクトに追加します。UIViewControllerのサブクラスとして、新しいクラスファイル(.hおよび.m)を作成してプロジェクトに追加します(ここでは名前を「PopupViewController」とします)。

  • 先ほどのPopupView.xibをクリックして表示させ、まず下図【1】のように「File’s Owner」をクリックします。そして、下図【2】のようにFile’s Ownerのカスタムクラスとして、先ほど作成したPopupViewControllerを設定します。

f:id:fleron:20141231141154p:plain

  • 更に、File’s Ownerの立方体状アイコンを右クリックし、クリックしたまま、xibに配置されたUIViewへとドラッグ&ドロップします(ドラッグ中は青線が表示)。すると下図のように「Outlets / view」というポップアップが表示されるので、viewの部分をクリックします。

f:id:fleron:20141231141758p:plain

  • ここまでで、xibファイル側で必要な下準備は完了しているのですが、このままでは大きく真っ白なポップアップが表示されるだけですので、多少UIViewのデザインをいじってみます。まず、xibに配置されたUIViewをクリックし、下図のようにサイズ設定を「Freeform」に変更すると、UIViewのサイズを自由に変更可能になります。

f:id:fleron:20141231142802p:plain

  • ここではUIViewのサイズを 200 x 200 に変更し、背景色を薄青にし、ラベルを貼り付けた以下のようなUIViewをポップアップさせてみます。

f:id:fleron:20141231143702p:plain

  • あとは、ポップアップビューを呼び出すクラス(ここでは UIViewController.m とします)に、「UIViewController+MJPopupViewController.h」および、xibのUIView操作用に追加したクラスのヘッダファイルをインポートします。
#import "UIViewController+MJPopupViewController.h"
#import "PopupViewController.h" //こちらのヘッダファイル名は、人によって異なる

  • そして、ポップアップビューを呼び出したい場所(例えばUIButtonタップ時のアクションなど)に、以下のコードを記述すればOKです。xibファイルからUIViewのインスタンスを生成し、それをpresentPopupViewController:メソッドでポップアップさせるだけです。またanimationType:引数を変更すると、ポップアップビューが飛び出してくる方向および引っ込む方向を色々変更することが出来ます。
PopupViewController *popUpView = [[PopupViewController alloc]initWithNibName:@"PopupView" bundle:nil];
[self presentPopupViewController:popUpView animationType:MJPopupViewAnimationSlideBottomBottom];

f:id:fleron:20141231145646p:plain

iTunes Connectにおける自作アプリ提出手続きの変更点メモ

以前の記事(自作アプリのApp Storeへのリリース手順メモ(2))で、自作アプリのリリース時にiTunes Connect上で行う提出手続きについてまとめました。しかし、あれから時間が経ち、現在(2014/11/20)ではiTunes Connectのレイアウトが当時と比べて大幅に変化しております(日本語表示にかなりの部分が対応しており、分かりやすさが改善しています)。

また、提出手続きの内容自体は基本的には当時と同様なのですが、細かな変更点が生じている箇所があります。そこで本記事では、以前の記事と比較したそれらの変更点についてメモします。
 
 

  • まずiTunes Connectにサインインすると下図の画面が表示されます。ここでは「マイ App」を選択します。

f:id:fleron:20141119220134p:plain
 

  • 「マイ App」のページに移動します。新規アプリの提出を行う場合は、下図の「+」ボタンをクリックし、表示されるポップアップから「新規 iOS App」を選択します(なお、このページにはリリース済アプリの一覧もあり、これらを選択すると登録情報の編集や、新規バージョンの提出手続きなどを行うことが出来ます)。

f:id:fleron:20141119220929p:plain
 

  • 「新規 iOS App」ポップアップが表示されますので、各項目を埋めます(項目の内容は、以前の記事における「App Information」ページの項目とほぼ同様です。ただし、アプリのバージョン入力がここに追加されているので、新規であれば「1.0」とでも入力すれば良いと思います)。完了したら右下の「作成」ボタンをクリックします。
  • ※アプリの名前は、他人に同じ名前を既に使われている場合には弾かれてしまいます。この場合は別名を考えたり、サブタイトルを付け足したりするしかないようです。

f:id:fleron:20141119222055p:plain
 

  • 先程の項目入力に問題が無ければ、下図の画面(収まりきっていないですが)に移動します。ここで具体的なアプリの登録情報を入力します。以前はまず下図の(1)〜(3)について説明します。

f:id:fleron:20141119224301p:plain

  • (1)の「保存」ボタンでは、これまでの入力内容を一時保存することが可能です。最終的にアプリをレビューへ提出するまでは、入力内容は何度でも書き直し可能ですので、こまめに利用した方が良いと思います。

  • (2)ではローカライズの設定が可能です。先程の「新規 iOS App」ポップアップではプライマリ言語を英語(English)に設定しておき、ここで日本語(Japanese)を追加することで、日本のApp Storeではアプリの説明などが日本語で表示され、それ以外の国では英語で表示されるようにすることが可能です。以前の記事でも言及した手法ですが、より設定方法が分かりやすくなっていると思います。

  • (3)ではスクリーンショットを追加します。2014/11/20現在、必要となるスクリーンショットの種類およびサイズは下表の通りであり、それぞれ最大5枚まで追加可能です。なお、アプリのプロモーション動画(Appビデオプレビュー)も登録可能であるようですが、こちらはオプションであり必須ではないようです。
  • ローカライズを利用する場合は、設定した各言語それぞれにスクリーンショットを追加する必要があります。言語の切り替えは先程の(2)のタブで行います。
機種 インチ サイズ
iPhone 4/4s 3.5 960 x 640
iPhone 5/5s 4 1136 x 640
iPhone 6 4.7 1334 x 750
iPhone 6 Plus 5.5 2208 x 1242
iPad - 1024 x 768

 

  • 続いて、先程の画面を下へスクロールすると下図の項目があります。ここでは以前の記事と同様にApp Storeに表示されるアプリの説明文、キーワード、サポートサイトのURLを入力します。
  • ローカライズを利用する場合は、設定した各言語それぞれの説明文およびキーワードを用意する必要があります。

f:id:fleron:20141119233756p:plain
 

  • 先程の画面を更に下へスクロールすると、下図の項目があります。最低限入力する必要があるのは赤枠の(4)〜(7)であり、いずれも以前からある項目です。以下ではそれぞれのポイントについて説明します。

f:id:fleron:20141120000300p:plain

  • (4)では1024 x 1024サイズの、アプリのアイコン画像を設定します。
  • ※このときPNG画像を用いると「イメージにアルファチャンネルや透過を含めることはできません。」というエラーメッセージが出て弾かれることがあります。その場合はアイコン画像をMac上でダブルクリックしてプレビューを開き、メニューの「ファイル」→「書き出し」を選択します。そしてダイアログの「フォーマット:PNG」の下にある「アルファ」のチェックを外して保存したものを用いると回避出来るようです。

  • (5)ではアプリのバージョン(既に入力済み)、カテゴリ、レーティングを設定します。レーティングでは「編集」をクリックするとチェック項目が表示されるので、当てはまるものがなければ、全て「なし or いいえ」を選択すればOKです。
  • (6)ではアプリの著作権所有者を入力します。ここは以前と変更はなく、自分の名前やハンドルネームなどを入れれば良いと思います。
  • (7)はアプリ著作権所有者の情報(名前、住所、電話番号など)を入力します。もしかしたら日本語で入力しても構わないのかもしれませんが、念のため自分は全て英語入力しています。

 

  • 更に、画面を下へスクロールすると以下の画面となります。以前は全ての登録情報を入力した後に「Ready to Upload Binary」でアプリ本体をアップロードする準備をしましたが、現在ではこの「ビルド」項目でアップロード済みのアプリを選択設定することが可能です。また、その下の「App レビュー審査に関する情報」では、Apple社が(必要があれば)アプリ作者に連絡を取るための情報を入力します。自分はここも念のため英語入力しています。

f:id:fleron:20141120223445p:plain

なお、アプリ本体のアップロード手順は自作アプリのApp Storeへのリリース手順メモ(3)と特に変更はありません。正しくアップロードしてしばらく待つと「ビルド」項目の中身が下図のように変化し「+」ボタンが表示されるので、クリックするとアップロードされたアプリを選択設定出来ます。
f:id:fleron:20141120224855p:plain
 

  • 最後に「バージョンのリリース」という項目があります。ここは「自動的にリリースする」を選んでおいて問題はないと思います…が、この項目には分からない点が一つあります。これについては後述します。

f:id:fleron:20141120225757p:plain
 

  • ページ最上部へ戻ると、下図のように他にも項目を選択するタブがあるのが分かります。このうち「価格」をクリックすると、アプリの価格を(無料を含め)設定する画面に移動します。設定内容は以前の記事とほぼ同様です。

f:id:fleron:20141120233331p:plain

f:id:fleron:20141120233904p:plain

-※しかし以前の記事ではApp Storeの新着欄対策として、Availability Date(配信開始日) をかなり先の日付に設定しておくテクニックを紹介しましたが、これが現在にも当てはまる話であるのかは未確認です。また先程の「バージョンのリリース」項目で「自動的にリリースする」を選択した場合、この配信開始日の有効性はどうなるのか、などは分かっていません(調べたのですが、はっきりしませんでした)。自分は一応、相変わらずこのテクニックを使っていますが、もし新情報が得られたら追記したいと思います。
 

  • 必要な登録情報の入力(アプリ本体のアップロードも含め)が全て完了したら、(1)の保存ボタンの横にある「レビューへ提出」ボタンをクリックします。問題がないようであれば下図の画面に移動します。いくつか質問がありますので回答し(特殊なアプリでなければ、全て「いいえ」で問題ないと思います)、「送信」ボタンが有効となったらクリックします。これで手続きは完了となります。

f:id:fleron:20141120235828p:plain