Swiftで遊ぼう! on Hatena

あしたさぬきblogでやってた初心者オヤジのiOSプログラミング奮闘記がHatenaに来ました

Swiftで遊ぼう! - 721 - SpriteKit 初心者チュートリアル


2018年6月9日:Swift4.2でも動くようにしました(Xcode10.0 betaで動きます)
2016年10月28日:本家RayさんのチュートリアルもSwift 3対応になっているので改訂します。

Protocol-Oriented Programmingの勉強をしていてどうしてSprite Kitの勉強をしているのか不思議に思うかもしれませんね。節操なしで勉強しているので思うがままです。

久しぶりにRayさんところのチュートリアル眺めてます。

実はSprite Kit、2Dゲームをコントロールする汎用APIフレームワークの事です。前々からゲーム開発に興味がないと公言している50オヤジの私がどうしてSprite Kitに興味を持ったのか?

実は、Sprite Kitのシーンの扱いが、UIKitのビューの扱いと類似していてProtocol-Oriented Programmingのデモに使われていたのです。ゲームのために用意された特殊効果をUIViewで扱えると素敵ですね。というわけで、Sprite Kitの勉強をします。

www.raywenderlich.com

このチュートリアルの前半でゲーム開発に「SpriteKit」を使うか、「Unity」を使うか、利点と欠点の説明から入っていますが、私はSwift言語以外の選択肢はないので前半の説明はパスします。

私のような初心者に難しいのは専門用語(ジャーゴン/jargon)です。

読んでいると簡単そうに説明は続きますが、はっきり言って混乱しました(^_^;)

「Sprite」「SpriteNode」「SKView」「Scene」など、関係性が少し分からなくなったので整理します。

  1. ベースビューは「SKView」で、これをコントロールするのはUIViewと同じUIViewControllerですが、Xcodeで新規プロジェクトを作成する時に「Game」を選択するf:id:yataiblue:20161028092827j:plainと、UIViewControllerを継承したGameViewControllerがデフォルトで作られます。ベースビューはUIViewではなく、「SKView」になっていますが、SKViewのスーパークラスはUIViewなんで、UIViewの機能はすべて使えると言うことです。
  2. Scene」はSKViewで表示するコンテンツ・オブジェクトのことです。しかし、まだちゃんと理解できていません。「GameSceneイニシャライザーの存在」、「SKSceneクラス?」、「SKNodeクラス?」まだおぼろげな理解しかできていません。sceneインスタンスを作っても、SKViewの「presentScene」メソッドを使わないと画面には表示されません。
  3. Sceneは、表示されるコンテンツを制御するんです。SKViewで表示する具体的なコンテンツオブジェクトはSpriteです。しかし、このSpriteは、パラパラ漫画でいう1枚の絵のようなオブジェクトであり、動きとして扱うためにSKSpriteNodeを使って集合体として扱います。まだ、この「ノード」という概念がいまいち分かって無いんです。「つなぎ目」「分節」という概念のように思われます。理解しにくい!
  4. そして「SKAction」を利用して、スプライトの集合体であるスプライトノードに動きをつけるという考え方です。

f:id:yataiblue:20160704192748j:plain

初期セットアップ

まずXcode10.0 beta*1のメニューから「File/New/Project...」を選択して、テンプレートから、ターゲット・デバイスを「iOS」、Applicationから「Game」を選択します。

f:id:yataiblue:20161028100804j:plain

Product Nameに「SpriteKitSimpleGame」を入力して、Languageを「Swift」、Game Technologyを「Sprite Kit」にします。Deviceは「iPhone」にして後のオプションはアンチェックです。「Next」ボタンを押して、適切な場所にセーブすればテンプレートファイルが作られます。

f:id:yataiblue:20161028101334j:plain

Xcodeが立ち上がると、プロジェクト・ナビゲータに色々なファイルが並んでいます。

f:id:yataiblue:20161028105915j:plain

ここでこのまま新規プロジェクトをランさせてみます。これはデフォルトで用意されていて画面上でタップするとカラフルな円筒が広がります。

f:id:yataiblue:20161028111348j:plain

「GameScene.sks」を選択すると、ビジュアルにSKSpriteNodeを編集することができます。実は、新規プロジェクトを作るとデフォルトで用意されているGameScene.sksの内容が表示されています。しかし、このチュートリアルに必要無いので消してください(Delete -> Move to Trash)。

GameViewControllerのコーディング

次にGameViewController.swiftを選択します。Xcode 8以降に新しいAPI「GamePlayKit」が導入されましたが、このチュートリアルでは扱いません。全てコードをを消去して次のコードにします。

import UIKit
import SpriteKit

class GameViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        if let skView = self.view as! SKView? {
            let scene = GameScene(size: view.bounds.size)
            skView.showsFPS = true
            skView.showsNodeCount = true
            skView.ignoresSiblingOrder = true
            scene.scaleMode = .resizeFill
            skView.presentScene(scene)
        }
    }
    
    override var prefersStatusBarHidden: Bool {
        return true
    }
} 

ここで知らないプロパティが出てきました。SKViewが持っている「showsFPS」と「showsNodeCount」です。

  • showsFPS:1秒間に描画したフレーム数。デフォルトは60fpsの設定になっていますが、負荷がかかると60以下になります。
  • showsNodeCount:表示しているスプライトノードの合計数です。

こういう基本的なことも知らないんです。このプロパティはBool型で「true」にすれば、画面の右隅に表示されます。

f:id:yataiblue:20161028171227j:plain

  • ignoresSiblingOrder:Sprite Kitは2D世界を表現するためX軸、Y軸で位置情報を扱いますが、[http://yataiblue.hatenablog.com/entry/2016/08/10/000000:title=Z軸でノードの重なり順も制御できます]。親ノードは子ノードの前にレンダリングされ、同じノードでも作られた順番で古いものから新しいものへレンダリングされていきます。
  • 「scene.scaleMode = .resizeFill:scene」は、GameSceneのインスタンスですが、GameSceneクラスはテンプレート作成時に自動的に作られているSKSceneを継承したカスタムクラスです。まだコードを書いていないのでここでは無視してください。このプロパティはスーパークラスのSKSceneが持っているenum型の「SKSceneScaleMode」です。定数は4つ用意されています。
  1. case fill
  2. case aspectFill
  3. case aspectFil
  4. case resizeFill

.resizeFill」は、sceneの大きさの比率を変えず、画面に合わせて縮小拡大をさせます。

そして、skViewのメソッド「presentScene()」を使ってsceneを画面に表示します。

ここまでがGameViewControllerの仕組みです。

Spriteの組み込み

Spriteは、何のことは無いただの画像です。しかし、画像を自分で用意するのは時間がかかるので、本家Rayさんのページから拝借してきます*2。直リンクは無礼なのでこのページから「resources for this project」を探してリソースをダウンロードします。

プロジェクト・ナビゲータから「Assets.xcassets」を開き、ダウンロードしたフォルダの「sprites.atlas」フォルダにあるイメージを全てドラッグして組み込みます。

f:id:yataiblue:20161028182915j:plain

リソースフォルダには「Sounds」もあります。プロジェクト・ナビゲータにある「SpriteKitSimpleGame」プロジェクト内にフォルダーをドラッグ&ドロップすると次のダイアログが出現します。

f:id:yataiblue:20161028183754j:plain

必ず「Destination」の「Copy items if needed」にチェックを入れます。するとプロジェクト・ナビゲータにSoundsフォルダーが出現します。これでリソースの準備はできました。

f:id:yataiblue:20161028184019j:plain

GameSceneのコーディング

GameScene.swiftのコードはGame用のテンプレートですが、Xcode 8から劇的に変更が加わっています。このコードを読むだけでもかなり勉強になると思いますが、次のコードに変更します。

import SpriteKit

class GameScene: SKScene {

    let player = SKSpriteNode(imageNamed: "player")  // 1
    
    override func didMove(to view: SKView) {
        backgroundColor = SKColor.white  // 2
        player.position = CGPoint(x: size.width * 0.1 , y: size.height * 0.5)   // 3
        self.addChild(player)  // 4
    }
}

コードを1つずつみていきます。本家チュートリアルで説明は無いのですが、didMove(to:)は、sceneの初期設定をさせる定番メソッドです。ViewControllerで言えば、viewDidLoad()メソッドのような位置づけですね*3

  1. これでスプライトノードのインスタンスが作られます。「あれれ?」という程単純です。この「SKSpriteNode(imageNamed: "player")」イニシャライザーはプロジェクトにダウンロードした1枚のイメージファイル("player"名)を指定してノードを作成しました。
  2. sceneのバックグラウンド色を白くします。
  3. オブジェクトの画面位置をCGPoint型で設定します。画面の相対的な位置にしています。X軸の左から1/10、そしてY軸の中央になります。
  4. そしてノードオブジェクトをSceneに追加するaddChild()メソッドを使って表示させます。

f:id:yataiblue:20161029084217j:plain

モンスター出現!

画面の左にニンジャが表示されています。画面右端から左に向かって動いていく動きのあるモンスターをコーディングしていきます。次のコードをGameScene.swiftに追加します。

func addMonseter() { //1
    let monster = SKSpriteNode(imageNamed: "monster") //2
    let actualY = CGFloat.random(in: monster.size.height/2 ... size.height - monster.size.height/2) //3
    monster.position = CGPoint(x: size.width + monster.size.width/2, y: actualY) //4
    addChild(monster) //5
    let actualDuration = CGFloat.random(in: 2.0 ... 4.0) // 6

    let actionMove = SKAction.move(to: CGPoint(x: monster.size.width/2, 
                                                                                 y: actualY), 
                                                                     duration: TimeInterval(actualDuration)) // 7
    let actionMoveDone = SKAction.removeFromParent()  //7
    monster.run(SKAction.sequence([actionMove, actionMoveDone])) //7
}

じつはオリジナルのRayさんのページの説明では、乱数発生のため「arc4randm()」を使っていましたが、Swift 4.2から乱数の扱いが変わり、型に囚われず、範囲も簡単に指定できるようになったので、arc4random()を使ったヘルプメソッドは全て消去しました*4

  1. 動きのあるモンスターをSceneに追加するメソッドをカスタムに用意
  2. playerと同じで、ファイル名("monster")を使用して1枚の画像からスプライトノードを作成
  3. モンスター出現時のY軸位置をランダムに調整
  4. モンスター出現時のX軸位置決め。ちょうど右端のオフスクリーンから出現
  5. 位置決めができたら画面に表示(オフスクリーンに出現するので実際は見えません)
  6. モンスターの動くスピードを乱数で指定
  7. Action」の設定

アクションはゲームの要になるので詳しく説明していきます。
このActionでSpriteに多彩な動きを加えることができます。ここで使用するメソッドを説明します。

  • SKAction.move(to:duration:) まずtoパラメータで動いていく方向を指定します。右端から左に向かうのでモンスターの出現X軸の反対(マイナス)をxパラメーターに、ランダムに設定したY軸をそのままyパラメーターに渡します。TimeIntervalは単なるDoubleです。typealiasを使って分かり安くするプログラミングテクニックです。2から4秒で動かします。
  • SKAction.removeFromParent() 画面から見えなくなったノードをsceneから消去するメソッドです*5。効率よく消さないとメモリーを大量に消費するので、かなり重要なメソッドです。
  • SKAction.sequence(_:) sequenceメソッドはActionメソッドを数珠つなぎで実行させるメソッドです。引数はArray型で与えられ、1つのActionが完結したら次のアクションを実行します。

ここまでのコードの復習です。カスタムメソッド「addMonster()」メソッドを呼ぶと、ランダムなY軸で右端の見えないところでモンスターが出現して、左端に向かって移動して画面から見えなくなったところで消えてしまう一連の動きが実行されます。

次は、このメソッドをどのように使用いたらいいのでしょう。sceneの初期設定をするメソッドは「didMove(to:)」です。このメソッド内で、無限ループを使ってaddMonster()メソッドを呼べばいいんです。

self.run(SKAction.repeatForever(
            SKAction.sequence([
                SKAction.run(addMonster),
                SKAction.wait(forDuration: 1.0)
                ])
))

これを実行すると次のようにランダムにモンスターが出現して動いて消えていきます。

f:id:yataiblue:20161031165255j:plain

手裏剣を投げる

モンスターが現れましたが、ニンジャは突っ立ったままです。ここからニンジャに手裏剣を投げさせるアクションを実装していきます。色々方法があるようですが私にはさっぱり。黙ってチュートリアルを続けます。画面にタップした方向に手裏剣を投げさせます。お馴染みのmove(to:duration:)メソッドを使えばタッチした位置に手裏剣は飛んでいきますが、タッチした位置に手裏剣を到達させるのが目的じゃ無く、タッチした位置はあくまでも方向なので、手裏剣はモンスターに当たらなければオフスクリーンまで飛んで消えなければなりません。ここで必要になるのがベクトル計算と三角関数ですね。ピタゴラスの定理など中学生の頃に習った公式をブラシュアップしましょう。

本家チュートリアルと同じようなイメージを作りました。

f:id:yataiblue:20161101173701j:plain

ニンジャの位置を基準(origin)としてタッチした位置を頂点にした三角形を作ります。これが基準三角形なのでこの比率で三角形を大きくして頂点をオフスクリーンまで動かしてやります。基準三角形のX軸とY軸の値がオフセットX値とオフセットY値になり、CGPoint型として扱います。比率を保って値を計算するためにベクトル計算が必要になるのですが、SpriteKitにはベクトル計算がデフォルトで組み込まれていません。機能を拡張する必要があるため、オペレーション・オーバーロードというSwift言語の機能を使います。

GameSceneクラス宣言前に次のメソッドを加えます。クラス宣言内でなく外です。

func + (left: CGPoint, right: CGPoint) -> CGPoint {
    return CGPoint(x: left.x + right.x, y: left.y + right.y)
}

func - (left: CGPoint, right: CGPoint) -> CGPoint {
    return CGPoint(x: left.x - right.x, y: left.y - right.y)
}

func * (point: CGPoint, scalar: CGFloat) -> CGPoint {
    return CGPoint(x: point.x * scalar, y: point.y * scalar)
}

func / (point: CGPoint, scalar: CGFloat) -> CGPoint {
    return CGPoint(x: point.x / scalar, y: point.y / scalar)
}

これを利用してCGPoint型のベクトル計算がデフォルトのように使えます。

そして次に32ビットOSでCGFloat型を扱えないsqrt()メソッドをプリプロセッサを使って拡張します。

#if !(arch(x86_64) || arch(arm64))
func sqrt(a: CGFloat) -> CGFloat {
  return CGFloat(sqrtf(Float(a)))
}
#endif

そして次に「Extension」が出てきます。Extensionは非常に便利な機能ですが、存在理由を理解するために2年かかりました(^^;) CGFloat型にlength()とnormalized()メソッドを加えます。

extension CGPoint {
  func length() -> CGFloat {
    return sqrt(x*x + y*y)
  }
 
  func normalized() -> CGPoint {
    return self / length()
  }
}

直角三角形の辺の関係は「ピタゴラスの定理」を使います。数学の講義はしないので、ノーマリゼーションの説明もしません。こういう知識は数学的基本です。プログラミングとは全く関係ない知識なので分からない人は学校の数学教育を受けて下さい。

次はユーザーの画面タッチを認識する仕組みを加えていきます。SKViewはUIViewを継承していて、UIViewはUIResponderを継承しています。そういうわけで、UIResponderのメソッドをGameSceneクラス内でオーバライドします。

// 1
override func touchesEnded(_ touches: Set<UITouch>, 
                          with event: UIEvent?) {
        
    // 2
    guard let touch = touches.first else {
        return
    }
    let touchLocation = touch.location(in: self)
        
    // 3
    let projectile = SKSpriteNode(imageNamed: "projectile")
    projectile.position = player.position
        
    // 4
    let offset = touchLocation - projectile.position
        
    // 5
    if (offset.x < 0) { return }
        
    // 6
    addChild(projectile)
        
    // 7
    let direction = offset.normalized()
        
    // 8
    let shootAmount = direction * 1000
        
    // 9
    let realDest = shootAmount + projectile.position
        
    // 10
    let actionMove = SKAction.move(to: realDest, 
                             duration: 2.0)
    let actionMoveDone = SKAction.removeFromParent()
    projectile.run(SKAction.sequence([actionMove, actionMoveDone]))
        
}

複雑なコードが並んでいるので1つずつ確認していきます。

touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
  1. このパラメータ「Set」が何だったが忘れているならこのページで復習します。さて、ジェスチャーの1つ、「タッチ」を認識するメソッドです。
  2. Set型のelementはオプショナル型なのでguardを使ってUITouch型のインスタンスを取りだし、タッチをした位置をCGPoint型のtouchLoacationで保持します。
  3. まず、プレーヤーの位置に手裏剣ノードインスタンスを作成します。まだ画面上に出現はしません。
  4. ここからベクトル計算が入ります。SKViewの座標システムはUIViewの座標システムと異なり、画面の左下隅が(0, 0)です。タッチした位置がプレーヤーの位置からみて何処にあたるか(相対的)を示した値をオフセットと呼びます。「-」はオペレーション・オーバーロードで機能を拡張しているので、ベクトル計算が入り向き情報がoffset値に加わります。
  5. プレーヤーより左側をタッチしても無反応です。
  6. プレーヤーより右側をタッチしたら手裏剣」は表示されます。
  7. offsetはCGPoint型でニンジャからの相対位置を示します。Extensionで拡張した「normalized()」メソッドを使ってノーマライズして利用可能な向き情報(direction)にします。
  8. ちょうどオフスクリーンになる位置を指定するのではなく、右画面の端から十分離れた距離まで飛ぶように指定します。
  9. 相対的な位置に実際の手裏剣の位置を加えて実際に飛ぶ位置を決めます。
  10. アクションを加えます。実際に飛んで行く位置に2.0秒で動くように設定します。アクション終了の挙動も設定して、連続的に発生させます。

これで手裏剣の動きが実装されました。この段階でランしても結構楽しめます。

f:id:yataiblue:20161102174639j:plain

衝突判定と物理世界の実現

個々の「ノード」のインタラクションを実装していく時期にきました。このインタラクションは物理的ルールを導入していかなければなりません。UIViewの世界ではUIDynamicAnimatorという物理世界があります。しかし、SKViewでは異なる物理世界を実装していきます。どうしてかと言えば、UIViewとSKViewでは座標システムが異なるからです。こういう問題を今後Protocol-Oriented Programmingで解決することができるのかもしれません。

SKViewにおける物理世界の概要

  1. まずPhysics Worldを設定します。
  2. Physics Worldで物理法則を受けるために、ノードにPhysics Bodyを組み込みます。
  3. そして理解し難い言葉が出てきました。「カテゴリー」です。Physics Bodyのプロパティなのですが、これが「ビットマスク」なんだそうです。ビットマスク? また知らない単語が出てきました... 私のような素人のプログラミング勉強が捗らない理由の1つが基礎力の無いところです(^^;) ビットマスクの解説は後でします。
  4. ノードに物理空間を設定しても、デリゲーションメソッドを実装しないかぎり物理空間は動きません*6

ゲームでノード間の衝突を実装するために、SKPhysicsContractDelegateプロトコールに準拠します。

class GameScene: SKScene, SKPhysicsContactDelegate {

didMove(to view: SKView)メソッドに次のコードを加えてデリゲーションの設定準備はできました。

physicsWorld.gravity = CGVector.zero
physicsWorld.contactDelegate = self        

ノードにphysics bodyというボディースーツをまとわせます。しかし、このボディスーツ、形は単純で「四角」か「丸」しかないようですが、問題にはならないそうです。

そしてボディースーツにはカテゴリーがあり、どういう状態なのかビットを使って設定します。このビットをグローバル定数として最初に宣言しておきます。

チュートリアルは下記のようなstruct型を使って設定しています。

struct PhysicsCategory {
    static let None      : UInt32 = 0
    static let All       : UInt32 = UInt32.max
    static let Monster   : UInt32 = 0b1       // 1
    static let Projectile: UInt32 = 0b10      // 2
}

しかし、よく考えて見ると、こういうカテゴリーを扱う場合、struct型よりもenum型を使った方がいいような気がします。

enum PhysicsCategory {
    static let none      : UInt32 = 0
    static let all       : UInt32 = UInt32.max
    static let monster   : UInt32 = 0b1       // 1
    static let projectile: UInt32 = 0b10      // 2
}

ノードに重力空間をまとわせるためにSKPhysicsBodyを設定して、状態を判定するためのカテゴリーも設定しているのですが、カテゴリーは単なる32ビットの整数でビットマスクとして機能します。

まずモンスターに物理世界を組み込みカテゴリーを設定します。addMonster()メソッド内のmonsterを作った後に次のコードを加えます。

// ここからphysicsBodyの設定
 monster.physicsBody = SKPhysicsBody(rectangleOf: monster.size) // 1
 monster.physicsBody?.isDynamic = true                          // 2
 monster.physicsBody?.categoryBitMask = PhysicsCategory.monster // 3
 monster.physicsBody?.contactTestBitMask = 
                                     PhysicsCategory.projectile // 4
 monster.physicsBody?.collisionBitMask = PhysicsCategory.none   // 5
  1. スプライトを作成した直後に「physicsBody」を着せます。monsterのイメージの大きさから四角形の衝突空間が形成されます。
  2. 「isDynamic」はデフォルトで「true」なんで書く必要はないのですが明示的に分かり易く記述します。
  3. さて「categoryBitMask」になっていますが、これがビットで立てたフラグですね。最初はモンスターとしてフラグを立てます。UInt32なので、「0b0000 0000 0000 0000 0000 0000 0000 0001」ということになりますね。カテゴリーは32種類というのは、それぞれ1個だけ「1」に変更しているということです。
  4. 「contactTestBitMask」は、2つの「カテゴリービットマスク」をANDオペレーションで評価して、「0」以外なら衝突が生じてSKPhysicsContactオブジェクトが作られるってことです。
  5. これと他のphysicsBodyのカテゴリービットマスクとANDオペレーションをして「0」以外だったら衝突がこのノードに影響を及ぼすのですが、ここで「0」を設定しているので、衝突は常に「0」で影響は出ません。

SpriteKitを使って物理空間を実装する時に戸惑った概念が「BitMask」です。

チュートリアルを読んでも詳しい説明はありません。プログラミングの基礎が分かっている人はさほど理解に苦しまないのかもしれませんが、私のようにプログラミングの基礎のない者はかなり苦しみました。こういう状況で挫折するかもしれませんね。遠回りですが、やはりちゃんと理解しなければなりません。

PhysicsCategaryをビットを使って理解すると分かり易いです。

monsterのPhysicsBodyの設定をしたので、手裏剣projectileのPhysicsBodyの設定もします。touchesEnded(_ touches: Set, with event: UIEvent?)メソッド内のprojectileを作った後に次のコードを加えます。

projectile.physicsBody = 
    SKPhysicsBody(circleOfRadius: projectile.size.width/2)
projectile.physicsBody?.isDynamic = true
projectile.physicsBody?.categoryBitMask = 
                            PhysicsCategory.projectile
projectile.physicsBody?.contactTestBitMask = 
                            PhysicsCategory.monster
projectile.physicsBody?.collisionBitMask = PhysicsCategory.none
projectile.physicsBody?.usesPreciseCollisionDetection = true

モンスターと違うところは、ボディースーツを丸にしたろころと、「usesPreciseCollisionDetection」を設定したところです。これは動きの速いphysicsBodyの衝突を判定する時に設定しないと素通りしてしまう可能性が出てくるそうです。

SKSpriteKitで繰り広げることのできる物理世界は、「ノード」というスプライトノードオブジェクトに「PhysicsBody」という重力世界を感じることのできるボディースーツを着けるところから始まります。このボディースーツにはカテゴリーという状態(ビットマスク)を設定してセンサーの役割を持たせます。互いのスーツがふれ合えば、カテゴリーに変化(ANDオペレーション)が生じ、PhysicsContactオブジェクトが生成され、その情報は物理世界を司るPhysicsWorldに委譲(delegate)されて世界は動くんです。

これを図にしてまとめたの以下です。

f:id:yataiblue:20160713105612j:plain

そして最後に実装すべきdelegationメソッドをコーディングします。

func didBegin(_ contact: SKPhysicsContact) {
        
    // 1
    var firstBody: SKPhysicsBody
    var secondBody: SKPhysicsBody
    if contact.bodyA.categoryBitMask < 
            contact.bodyB.categoryBitMask {
        firstBody = contact.bodyA
        secondBody = contact.bodyB
    } else {
        firstBody = contact.bodyB
        secondBody = contact.bodyA
    }
        
    // 2
    if ((firstBody.categoryBitMask & 
            PhysicsCategory.Monster != 0) &&
        (secondBody.categoryBitMask & 
            PhysicsCategory.projectile != 0)) {
        projectileDidCollideWithMonster(
            projectile: firstBody.node as! SKSpriteNode, 
               monster: secondBody.node as! SKSpriteNode)
    }
        
}
  1. このコードも定型文のような内容です。衝突が生じた時に渡される2つのphysicsBodyの順番が決まっていないので、categoryBitMaskを使って判断します。
  2. 衝突判定をするのが「if」文です。「&」がANDオペレータなんで、ビット計算させればいいんです。簡単に理解できるでしょう。「projectileDidCollideWithMonster」はカスタムメソッドなので、このコードを入力するとエラーが表示されます。衝突していたらノードを消すステップを次のようにコードします。
func projectileDidCollideWithMonster(projectile: SKSpriteNode, 
                                        monster: SKSpriteNode) {
    print("Hit")
    projectile.removeFromParent()
    monster.removeFromParent()
}

これでランしたら遊ぶことができました。コードの理解もできたしプログラムは動くし、少しずつですがiOS開発が分かってきています。

仕上げ

ゲームを盛り上げるサウンドを組み込みます。音もノードとして扱うようです。SKAudioNodeクラスで扱います。サウンドファイルをリソースとして用意してファイル名を指定してオブジェクトを作るところはスプライト・ノードと同じなので、didMove(to view: SKView)メソッドに下記のコードを加えます。

let backgroundMusic = 
        SKAudioNode(fileNamed: "background-music-aac.caf")
backgroundMusic.autoplayLooped = true
self.addChild(backgroundMusic)

ランしてみると... ???? サウンドが流れません? なぜ? シュミレーターでも実機(iPhone5s)でも音はしないんです。ドキュメントにあたっても間違っていないような...

最後にゲームオーバーSceneを追加します。Fileメニューから「New」>「File... 」を選んで、iOS\Source\Swift Fileを作成すると、import Foundationのみの真っ新なファイルが作られます。ここでコードのみでSKSceneクラスを作ります。

import Foundation
import SpriteKit

class GameOverScene: SKScene {
    
    init(size: CGSize, won:Bool) {
        
        super.init(size: size)
        
        // 1
        backgroundColor = SKColor.white
        
        // 2
        let message = won ? "You Won!" : "You Lose :["
        
        // 3
        let label = SKLabelNode(fontNamed: "Chalkduster")
        label.text = message
        label.fontSize = 40
        label.fontColor = SKColor.black
        label.position = 
            CGPoint(x: size.width/2, y: size.height/2)
        addChild(label)
        
        // 4
        run(SKAction.sequence([
            SKAction.wait(forDuration: 3.0),
            SKAction.run() {
                // 5
                let reveal = 
                    SKTransition.flipHorizontal(withDuration: 0.5)
                let scene = GameScene(size: size)
                self.view?.presentScene(scene, transition:reveal)
            }
            ]))
        
    }
    
    // 6
    required init(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

クラスを継承して拡張する場合、イニシャライザーを書かないコードが推奨されています。SKSceneクラスを継承しているのでdidMove(to view: SKView)メソッドで初期設定をする方法が一般的です。しかし、ここで普通のイニシャライザーが出てきました。どうしてイニシャライザーを使うのか?

GameOverSceneインスタンス化にパラメーターを与えなければならないからです。実はこのアプリケーション、プログラム構造上の問題を抱えています。GameSceneクラスの切りかえをGameSceneに依存させているプログラミング構造は好ましくないでしょう。GameSceneクラスの切りかえはGmaeViewControllerのするべき役割*7と言えます。SpriteKitのチュートリアルというテーマから問題を抱えたコーディングになっていると思います。

SKSceneクラスもUIViewクラスを継承しているのでイニシャライザは共通しています。UIViewクラスのイニシャライザの理解は必要です。→Swiftで遊ぼう! - 248 - UIViewの初期化ステップ - Swiftで遊ぼう! on Hatena

次にこのGameOverSceneクラスへ切り替えは、GameScene.swifにあるaddMonter()メソッド内の最後の行「monster.run(SKAction.sequence([actionMove, actionMoveDone]))」を次に書き換えます。

let loseAction = SKAction.run() {
    let reveal = 
        SKTransition.flipHorizontal(withDuration: 0.5)
    let gameOverScene = 
         GameOverScene(size: self.size, won: false)
     self.view?.presentScene(gameOverScene, 
                             transition: reveal)
        }
monster.run(SKAction.sequence([actionMove, 
                               loseAction, 
                               actionMoveDone]))

次はゲームに勝利の表示をさせるために一つプロパティを作ります。モンスターを何匹退治したかカウントするためにGameSceneクラス宣言の最初に次のプロパティを加えます。

var monstersDestroyed = 0

次に倒したモンスターをカウントするためにprojectile(_:didCollideWithMonster:)メソッドに次のコードを加えます。

monstersDestroyed += 1
    if (monstersDestroyed > 30) {
        let reveal = SKTransition.flipHorizontal(withDuration: 0.5)
        let gameOverScene = GameOverScene(size: self.size, won: true)
        self.view?.presentScene(gameOverScene, transition: reveal)
    }

これで30個以上モンスターを倒すと勝利画面に移ります。これでこのゲームのチュートリアルは終了です。でもなかなかモンスターを30匹倒せませんね(/ _ ; )

f:id:yataiblue:20161116221358j:plain

まとめ

どうでしたが?SpriteKitも使えるようになりました。ここまで理解が進むとオリジナルアプリを発表することができるんじゃないかな。色々なチュートリアルをこなして理解が進みました。そろそろアプリを公開できるようになるでしょう。初心者向けにiOS開発の講義もしたいですね。

今日はここまで。

*1:アップルの規定からβ版アプリのキャプチャー画像は使えないので、Xcode 8の画面を使っています

*2:Rayさんから了承は得ています

*3:もう少しライフサイクルの勉強をしないといけませんね

*4:Swiftで遊ぼう! - 997 - Swift 4.2で乱数の扱いが変わりました! - Swiftで遊ぼう! on Hatena

*5:実際は画面から消えても反対側の出現時の-x軸まで動いていってから消えます

*6:プロトコールとデリゲーションの説明は、Swiftで遊ぼう! - 260 - プロトコールとデリゲーション ProtocolsとDelegation - Swiftで遊ぼう! on Hatena

*7:厳密に言えば、スコア管理などMVCのモデルは別のクラスに任せるべきです。