【SpriteKit】Xcode8のGameテンプレートを再現する

昨日（2016/9/14）、遂にSwift3, iOS10, Xcode8が正式リリースとなりました。
いろいろ変更や追加がありすぎて、何からキャッチアップしていけばいいかわかりませんが、楽しいことから始めていければいいなと思い、SpriteKitのことを改めて理解することにしました。

まず、Xcode8の新規プロジェクト作成時に選べるテンプレートで久しぶりに「Game」を選択すると、何やら今までと違う気がします。そのままの状態で実行すると、なんかキレイなエフェクトが拝めるアプリになっていたので、これを「Single View」テンプレートから再現することにします。

#この記事で体験できること
- SpriteKitがビューコントローラ上で、どう構成されているか？
- シーンをGUIで作成する方法（コードを最小限にする）
- アクションをGUIで作成する方法
- アクションをコードで記述する方法
- タッチを検出する
- マルチタッチに対応する
- デバイス種類を判断して、サポートする画面方向を切り替える

#実行環境
- OSX El Capitan (ver 10.11.6)
- Xcode8
- Swift3

#SpriteKitについて
ハイパフォーマンスな2Dゲームを作るためのフレームワークです。
衝突や重力など物理的な挙動や、接触判定、光源処理、エフェクト（パーティクルなど）も用意されています。ゲームコンテンツを強化したいAppleの意向もあってか毎年強化されている印象があり、ちょっと目を離しているといつの間にか便利になってたりします。


#SpriteKitを使えるようにする
シングルテンプレートからプロジェクト作成
プロジェクトナビゲータ > Linked Framework and Libraries > + > SpriteKit
Deployment Info > Devices > Universal
Device Orientation: 全てにチェックする

GameViewControllerクラスファイル
ファイル名を「ViewController.swift」から「GameViewController.swift」に変更
クラス名をGameViewControllerに変更

Main.storyboard
ビューコントローラのIdentity Inspector: Custom Classを「GameViewController」に変更
Attributes Inspectorから、ルートビューをSKViewクラスに変更


#シーンを表示する
シーンファイルを作成
新規ファイル「iOS > Resource > SpriteKit Scene」を追加（GameScene.sks）
Custom Class Inspectorから、Custom Classを「GameScene」に変更
背景色を変更
ラベルを配置（Position x: 0, y: 0）
ラベルテキスト変更（Sprite Kit!, フォントサイズ: 100 ~, カラー: 任意）
ラベルを選択「Attributes Inspector > name: greetingLabel」 に変更

ゲームシーンを表示させる

```GameViewController.swift
    override func viewDidLoad() {
        super.viewDidLoad()

        if let view = self.view as! SKView? {
            if let gameScene = SKScene(fileNamed: "GameScene") {
                gameScene.scaleMode = .aspectFill
                view.presentScene(gameScene)
            }
        }
    }
```

##*ビルド*
* ゲームシーンが表示されれば、OK

#ラベルにスプライトアクションをさせる
シーンクラスを作成
メニューバーから「File > New > File」を選択
「iOS > Resource > Cocoa Touch Class」を追加（name: GameScene, subclass of: SKScene）

シーンクラスにコードを記述
Sprite Kitフレームワーク組み込み
ラベルを取得
メンバプロパティgreetingLabelを宣言（private）
ラベルの出現

```GameScene.swift
import Sprite Kit

    private var greetingLabel: SKLabelNode?

    override func didMove(to view: SKView) {
        
        // ラベルを取得
        self.greetingLabel = self.childNode(withName: "//greetingLabel") as? SKLabelNode
        if let label = self.greetingLabel {
            label.alpha = 0.0
            label.run(SKAction.fadeIn(withDuration: 2.0))
        }
    }
```


```GameViewController.swift
    override func viewDidLoad() {
        super.viewDidLoad()
        
        if let skView = self.view as! SKView? {
            if let gameScene = SKScene(fileNamed: "GameScene") {
                gameScene.scaleMode = .aspectFill
                skView.presentScene(gameScene)
            }

            skView.showsFPS = true
            skView.showsNodeCount = true
        }
    }

    override var prefersStatusBarHidden: Bool {
        return true
    }
```

##*ビルド*
* ゲームシーンが表示されると、ラベルがフェードインすることを確認
* ノード数を確認
* フレームレートを確認
* ステータスバーがないことを確認

#複雑なアクションを作成する
アクションファイルを作成します。
新規ファイル「iOS > Resource > SpriteKit Action」を追加（Action.sks）
「+」で新規アクションのタイムラインを追加（Pulse）
オブジェクトライブラリから各種アクションをタイムラインにドラッグ
一番上の段には、[Scale, Scale to]
その下の段に、[Fadeout, Fadein]を並べる。
ScaleとFadeを同時に開始させるように編集します。
タイムライン上の各アクションを選択したら、アトリビュートインスペクタで編集する。
* Scale Action追加（Duration: 0.3, Timing Function: 任意, Scala Amount: 0.1 ~ 0.8）
* Scale to Action追加（Duration: 0.3, Timing Function: 任意, Scala Amount: 1）
* Fade out Action追加（Duration: 0.3）
* Fade in Action追加（Duration: 0.3）

GameScene.swiftクラスファイル
touchesBegan(:with:)メソッドをオーバーライド

        if let label = self.label {
            label.run(SKAction.init(named: "Pulse")!, withKey: "fadeInOut")
        }

##*ビルド*
* タップをすると、ラベルがフェードイン・アウトすることを確認
* Action.sksファイルのアクションをカスタムして、いろいろ試してみる

#タップしたら場所にノードを出現させる
GameScene.swiftクラスファイル
メンバプロパティを追加

```GameScene.swift
    private var spinnyNode: SKShapeNode?
```


```GameScene.swift
 > didMove(to:)メソッド追記
        let length = (self.size.width + self.size.height) * 0.05
        let rectangle = CGSize(width: length, height: length)
        self.spinnyNode = SKShapeNode(rectOf: rectangle, cornerRadius: length * 0.3)
```

```GameScene.swift>>タッチ開始メソッドに追記
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        if let label = self.greetingLabel { label.run(SKAction(named: "Pulse")!) }        
        if let node = self.spinnyNode?.copy() as? SKShapeNode {
            let position = touches.first?.location(in: self)
            node.position = position!
            self.addChild(node)
        }
    }
```

##*ビルド*
タッチダウンのタイミングで、その位置にノードが出現することを確認！

#ノードをアニメーションさせる

```GameScene.swiftクラスファイル

    override func didMove(to view: SKView) {

        //ラベルの出現アニメーション
        self.greetingLabel = self.childNode(withName: "/greetingLabel") as? SKLabelNode
        if let label = greetingLabel {
            label.alpha = 0.0
            label.run(SKAction.fadeIn(withDuration: 3.0))
        }
        
        // タッチで出現するノード
        let length = (self.size.width + self.size.height) * 0.05
        let rectangle = CGSize(width: length, height: length)
        self.spinnyNode = SKShapeNode(rectOf: rectangle, cornerRadius: 0.3)    // 角マル
        
        // ノードのアニメーション
        if let node = self.spinnyNode {
            node.lineWidth = 2.5
            node.run(SKAction.repeatForever(.rotate(byAngle: CGFloat(M_PI), duration: 1.0)))
            node.run(SKAction.sequence([.wait(forDuration: 0.5), .fadeOut(withDuration: 0.5), .removeFromParent()]))
        }
    }
```

##*ビルド*
* タッチダウンでノードが出現することを確認
* ノードのアニメーションを確認

#タッチムーブでノードを出現させる
```GameScene.swiftの編集

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        if let label = self.greetingLabel {
            label.run(SKAction(named: "Pulse")!)    //"Pulse"はファイル名じゃなくて、アクション名
        }
        
        if let node = self.spinnyNode?.copy() as? SKShapeNode {
        // ノードは一個しかないからcopyして使う
            let position = touches.first?.location(in: self)
            node.position = position!
            node.strokeColor = SKColor.blue
            self.addChild(node)
        }
    }
    
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        if let node = self.spinnyNode?.copy() as? SKShapeNode {
            let position = touches.first?.location(in: self)
            node.position = position!
            node.strokeColor = SKColor.green
            self.addChild(node)
        }
    }
    
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        if let node = self.spinnyNode?.copy() as? SKShapeNode {
            let position = touches.first?.location(in: self)
            node.position = position!
            node.strokeColor = SKColor.red
            self.addChild(node)
        }
    }
```

重複部分を別メソッドdrawSpinnyNode(atPoint:withColor)にする

```GameScene.swift
    func drawSpinnyNode(atPoint pos: CGPoint, withColor color: SKColor) {
        if let node = self.spinnyNode?.copy() as? SKShapeNode {
            node.position = pos
            node.strokeColor = color
            self.addChild(node)
        }
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        if let label = self.greetingLabel { label.run(SKAction(named: "Pulse")!) }
        
        let position = touches.first?.location(in: self)
        drawSpinnyNode(atPoint: position!, withColor: .blue)
    }
    
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        let position = touches.first?.location(in: self)
        drawSpinnyNode(atPoint: position!, withColor: .green)
    }
    
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        let position = touches.first?.location(in: self)
        drawSpinnyNode(atPoint: position!, withColor: .red)
    }
```

##*ビルド*
* タッチダウンで青いノードが出現することを確認する
* タッチムーブで緑のノードが出現することを確認する
* タッチアップで赤いノードが出現することを確認する

#マルチタッチに対応する#
Main.storyboard
GameViewControllerのルートビューを選択
Attributes InspectorのInteraction: Multiple Toucheにチェックを入れる

```GameScene.swift

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        if let label = self.greetingLabel { label.run(SKAction(named: "Pulse")!) }
        for touche in touches {
            drawSpinnyNode(atPoint: touche.location(in: self), withColor: .blue)
        }
     }
    
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        for touche in touches {
            drawSpinnyNode(atPoint: touche.location(in: self), withColor: .green)
        }
    }
    
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        for touche in touches {
            drawSpinnyNode(atPoint: touche.location(in: self), withColor: .red)
        }
    }
```

##*ビルド*
シミュレータでマルチタッチして、ノードが複数出現することを確認する

#iPadだけ全デバイス方向に対応する
プロジェクトナビゲータ
Deployment Info > Devices > Universal
Device Orientation: 全てにチェックする

```GameViewController.swiftに追記

    override var shouldAutorotate: Bool {
        return true
    }
    
    override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
        switch UIDevice.current.userInterfaceIdiom {
        case .phone:
            return .allButUpsideDown
        default:
            return .all
        }
    }
```

##*ビルド*
シミュレータがiPadの場合だけ、全方向に対応することを確認する

#感想
SingleViewテンプレートから再現することで、SpriteKitの仕組みや使い方が少しわかりました。
SceneKitのテンプレートも、SingleViewテンプレートで再現してみたいです。
でも、その前にSpriteKitの新しい機能がたくさんあるので、そちらも試したいです。
いやいや、その前に一旦、SpriteKitでカンタンなゲームを一つ、作ってみるべきでしょうか...。