アクションゲームアプリ

横スクロールアクションゲームアプリ

これから横スクロールアクションゲームを開発します。ゲームの作りはシンプルだけど思わずハマってしまう、タップのみで障害物を避けていくゲームです。

アプリ概要

ゲームのルール

  • キャラクターを操作して移動している障害物の隙間を通過させる
  • 障害物の隙間を通過することでスコアが1加算される
  • キャラクターは自然と画面の下へ落下する
  • キャラクターは画面をタップすると上昇する
  • 障害物は画面の右から左へ移動する

機能の要件

  • SKActionを使って障害物を移動させる
  • 画面タップによりプレイキャラを上昇させる
  • 物理エンジンを使ってスプライトを加工させる
  • プレイキャラと障害物との衝突判定させる
  • 障害物と衝突すると画面を停止させる

キャラクターにアニメーションをつける

SpriteKitのSKActionを使うことによりアニメーションを簡単に実現する方法を学習します。

SKActionとは

SKActionとは対象ノードに動きを持たせたり、拡大・縮小などの簡単なエフェクトを実現するためのクラスです。このクラスには様々なメソッドが用意されており、それらを組み合わせることで複雑なアニメーションを実現する事が可能です。

ゲーム構成要素

今回のゲームアプリは下図のような構成要素で開発します。

プロジェクトの作成

それではプロジェクトを作成していきます。Xcodeを開きプロジェクトを作成します。

項目
テンプレート Game
Product Name Swim
Organization Name (任意)
Organization Identifer (任意)
Language Swift
Game Techinology SpriteKit
Devices iPhone

GameScene.swiftを開き、下記のとおりに余分なコードを削除しておいてください。

import SpriteKit

class GameScene: SKScene {
// シーンが表示された時に呼ばれる
override func didMoveToView(view: SKView) {

}
// ユーザーが画面をタッチした際に呼ばれる
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {

}
// フレームごとに呼ばれる
override func update(currentTime: NSTimeInterval) {

}
}

画像ファイルの取り込み

下記よりダウンロードしてください。

素材ダウンロード

ダウンロードしたらAssets.xcassetsの中にドラッグしてインポートしてください。

この画像素材を使ってゲームを開発していきます。

背景を動かす

今回は横スクロールアニメーションのゲームですので背景を横に動かしていきます。
次の3ステップで背景を動かします。

  1. 背景画像の必要枚数をディスプレイサイズから算出して並べます

  1. 各画像を1枚分移動させます

  1. 1枚分移動するとアニメーション開始時と同じ見え方になります

背景アニメーションの実装

背景画像をアニメーションさせるメソッドを追加します。

GameScene.swift

//////////////// ▼▼ 追加 ▼▼ ////////////////
// 背景画像を構築
func setupBackgroundSea() {
// 背景画像を読み込む
let texture = SKTexture(imageNamed: "background")
texture.filteringMode = .Nearest
// 必要な画像枚数を算出
let needNumber = 2.0 + (self.frame.size.width / texture.size().width)
// 左に画像一枚分移動アニメーションを作成
let moveAnim = SKAction.moveByX(
-texture.size().width,
y: 0.0, duration: NSTimeInterval(texture.size().width / 10.0)
)
// 元の位置に戻すアニメーションを作成
let resetAnim = SKAction.moveByX(
texture.size().width,
y: 0.0, duration: 0.0
)
// 移動して元に戻すアニメーションを繰り返すアニメーションを作成
let repeatForeverAnim = SKAction.repeatActionForever(
SKAction.sequence([moveAnim, resetAnim])
)
// 画像の配置とアニメーションを設定
for i in CGFloat(0.0).stride(to: needNumber, by: 1.0) {
// SKTextureからSKSpriteNodeを作成
let sprite = SKSpriteNode(texture: texture)
// 一番奥に配置
sprite.zPosition = -100.0
// 画像の初期位置を設定
sprite.position = CGPoint(
x: i * sprite.size.width,
y: self.frame.size.height / 2.0
)
// アニメーションを設定
sprite.runAction(repeatForeverAnim)
// 親ノードに追加
baseNode.addChild(sprite)
}
}
//////////////// ▲▲ 追加 ▲▲ ////////////////

追加したメソッドを処理させるために下記を追加してください。

GameScene.swift

class GameScene: SKScene {
//////////////// ▼▼ 追加 ▼▼ ////////////////
// プレイキャラ以外の移動オブジェクトを追加する空ノード
var baseNode: SKNode!
// サンゴ関連のオブジェクトを追加する空ノード(リスタート時に活用)
var coralNode: SKNode!
//////////////// ▲▲ 追加 ▲▲ ////////////////

// シーンが表示された時に呼ばれる
override func didMoveToView(view: SKView) {
//////////////// ▼▼ 追加 ▼▼ ////////////////
// 全ノードの親となるノードを生成
baseNode = SKNode()
baseNode.speed = 1.0
self.addChild(baseNode)
// 障害物を追加するノードを生成
coralNode = SKNode()
baseNode.addChild(coralNode)
// 背景画像を構築
self.setupBackgroundSea()
//////////////// ▲▲ 追加 ▲▲ ////////////////
}
:
}

奥行きの表現

続いて岩山画像を用いて背景画像の奥行きを出しましょう。
奥行きに関する位置は、プロパティzPositionとx座標、y座標の関係を図示すると下記のようになります。

zPositionの値が小さいほど奥に配置され、値が大きいほど手前に配置されます。
それではGameScene.swiftに実装してみます。

GameScene.swift

//////////////// ▼▼ 追加 ▼▼ ////////////////
// 背景の岩山画像を構築
func setupBackgroundRock() {
// 岩山(下)画像を読み込む
let under = SKTexture(imageNamed: "rock_under")
under.filteringMode = .Nearest
// 必要な画像枚数を算出
var needNumber = 2.0 + (self.frame.size.width / under.size().width)
// 左に画像一枚分移動アニメーションを作成
let moveUnderAnim = SKAction.moveByX(
-under.size().width,
y: 0.0,
duration:NSTimeInterval(under.size().width / 20.0)
)
// 元の位置に戻すアニメーションを作成
let resetUnderAnim = SKAction.moveByX(
under.size().width,
y: 0.0,
duration: 0.0
)
// 移動して元に戻すアニメーションを繰り返すアニメーションを作成
let repeatForeverUnderAnim = SKAction.repeatActionForever(
SKAction.sequence([moveUnderAnim, resetUnderAnim])
)

// 画像の配置とアニメーションを設定
for i in CGFloat(0.0).stride(to: needNumber, by: 1.0) {
// SKTextureからSKSpriteNodeを作成
let sprite = SKSpriteNode(texture: under)
// 背景画像より手前に設定
sprite.zPosition = -50.0
// 画像の初期位置を設定
sprite.position = CGPoint(x: i * sprite.size.width, y: sprite.size.height / 2.0 )
// アニメーションを設定
sprite.runAction(repeatForeverUnderAnim)
// 親ノードに追加
baseNode.addChild(sprite)
}

// 岩山(上)画像を読み込む
let above = SKTexture(imageNamed: "rock_above")
above.filteringMode = .Nearest
// 必要な画像枚数を算出
needNumber = 2.0 + (self.frame.size.width / above.size().width)
// 左に画像一枚分移動アニメーションを作成
let moveAboveAnim = SKAction.moveByX(
-above.size().width,
y: 0.0,
duration:NSTimeInterval(above.size().width / 20.0)
)
// 元の位置に戻すアニメーションを作成
let resetAboveAnim = SKAction.moveByX(
above.size().width,
y: 0.0,
duration: 0.0
)
// 移動して元に戻すアニメーションを繰り返すアニメーションを作成
let repeatForeverAboveAnim = SKAction.repeatActionForever(
SKAction.sequence([moveAboveAnim, resetAboveAnim])
)
// 画像の配置とアニメーションを設定
for i in CGFloat(0.0).stride(to: needNumber, by: 1.0) {
// SKTextureからSKSpriteNodeを作成
let sprite = SKSpriteNode(texture: above)
// 背景画像より手前に設定
sprite.zPosition = -50.0
// 画像の初期位置を設定
sprite.position = CGPoint(x: i * sprite.size.width, y: self.frame.size.height - (sprite.size.height / 2.0))
// アニメーションを設定
sprite.runAction(repeatForeverAboveAnim)
// 親ノードに追加
baseNode.addChild(sprite)
}
}
//////////////// ▲▲ 追加 ▲▲ ////////////////

岩山(上)画像のaboveと岩山(下)画像のunderSKTextureに対しアニメーションを設定しています。またsprite.zPosition = -50.0によって背景画像より手前に表示されるようにしています。

このメソッドをdidMoveToViewメソッドに追加して動作確認してみましょう。

GameScene.swift

// シーンが表示された時に呼ばれる
override func didMoveToView(view: SKView) {
:
:
//////////////// ▼▼ 追加 ▼▼ ////////////////
// 背景の岩山画像を構築
self.setupBackgroundRock()
//////////////// ▲▲ 追加 ▲▲ ////////////////
}

地面の作成

次に地面を作成していきます。作成にあたりまず衝突判定に必要なビット値を用意します。

GameScene.swift

class GameScene: SKScene {
/// プレイキャラ以外の移動オブジェクトを追加する空ノード
var baseNode: SKNode!
/// サンゴ関連のオブジェクトを追加する空ノード(リスタート時に活用)
var coralNode: SKNode!
//////////////// ▼▼ 追加 ▼▼ ////////////////
// 衝突の判定につかうBitMask
struct ColliderType {
/// プレイキャラに設定するカテゴリ
static let Player: UInt32 = (1 << 0)
/// 天井・地面に設定するカテゴリ
static let World: UInt32 = (1 << 1)
/// サンゴに設定するカテゴリ
static let Coral: UInt32 = (1 << 2)
/// スコア加算用SKNodeに設定するカテゴリ
static let Score: UInt32 = (1 << 3)
/// スコア加算用SKNodeに衝突した際に設定するカテゴリ
static let None: UInt32 = (1 << 4)
}
//////////////// ▲▲ 追加 ▲▲ ////////////////
:
}

次にsetupCeilingAndLandメソッドを作って、地面と天井のアニメーションを実装します。

GameScene.swift

//////////////// ▼▼ 追加 ▼▼ ////////////////
// 天井と地面を構築
func setupCeilingAndLand() {
// 地面画像を読み込み
let land = SKTexture(imageNamed: "land")
land.filteringMode = .Nearest
// 必要な画像枚数を算出
var needNumber = 2.0 + (self.frame.size.width / land.size().width)

// 左に画像一枚分移動アニメーションを作成
let moveLandAnim = SKAction.moveByX(-land.size().width, y: 0.0, duration:NSTimeInterval(land.size().width / 100.0))
// 元の位置に戻すアニメーションを作成
let resetLandAnim = SKAction.moveByX(land.size().width, y: 0.0, duration: 0.0)
// 移動して元に戻すアニメーションを繰り返すアニメーションを作成
let repeatForeverLandAnim = SKAction.repeatActionForever(SKAction.sequence([moveLandAnim, resetLandAnim]))

// 画像の配置とアニメーションを設定
for i in CGFloat(0.0).stride(to: needNumber, by: 1.0) {
// SKTextureからSKSpriteNodeを作成
let sprite = SKSpriteNode(texture: land)
// 画像の初期位置を設定
sprite.position = CGPoint(x: i * sprite.size.width, y: sprite.size.height / 2.0)

// 画像に物理シミュレーションを設定
sprite.physicsBody = SKPhysicsBody(texture: land, size: land.size())
sprite.physicsBody?.dynamic = false
sprite.physicsBody?.categoryBitMask = ColliderType.World
// アニメーションを設定
sprite.runAction(repeatForeverLandAnim)
// 親ノードに追加
baseNode.addChild(sprite)
}
// 天井画像を読み込み
let ceiling = SKTexture(imageNamed: "ceiling")
ceiling.filteringMode = .Nearest
// 必要な画像枚数を算出
needNumber = 2.0 + self.frame.size.width / ceiling.size().width
// 画像の配置とアニメーションを設定
for i in CGFloat(0.0).stride(to: needNumber, by: 1.0) {
// SKTextureからSKSpriteNodeを作成
let sprite = SKSpriteNode(texture: ceiling)
// 画像の初期位置を設定
sprite.position = CGPoint(x: i * sprite.size.width, y: self.frame.size.height - sprite.size.height / 2.0)

// 画像に物理シミュレーションを設定
sprite.physicsBody = SKPhysicsBody(texture: ceiling, size: ceiling.size())
sprite.physicsBody?.dynamic = false
sprite.physicsBody?.categoryBitMask = ColliderType.World
// アニメーションを設定
sprite.runAction(repeatForeverLandAnim)
// 親ノードに追加
baseNode.addChild(sprite)
}
}
//////////////// ▲▲ 追加 ▲▲ ////////////////

いままでと同様に、このメソッドをdidMoveToViewメソッドに追加して動作確認してみましょう。

// シーンが表示された時に呼ばれる
override func didMoveToView(view: SKView) {
:
:
//////////////// ▼▼ 追加 ▼▼ ////////////////
// 天井と地面を構築
self.setupCeilingAndLand()
//////////////// ▲▲ 追加 ▲▲ ////////////////
}

キャラクターを設置

続いて、キャラクターを配置していきます。
キャラクターはパタパタするようなアニメーションをつけながら移動するようにします。

まずはGameSceneクラスにキャタクターのplayerというSKSpriteNodeの変数を定義します。
さらにキャラクターにアニメーションさせるためのスプライト画像を構造体(struct)として定義します。

GameScene.swift

class GameScene: SKScene {
//////////////// ▼▼ 追加 ▼▼ ////////////////
// キャラクター
var player: SKSpriteNode!
struct Constants {
// キャラクタースプライト画像
static let PlayerImages = ["shrimp01", "shrimp02", "shrimp03", "shrimp04"]
}
//////////////// ▲▲ 追加 ▲▲ ////////////////
:
:
}

次にキャラクターの動きを定義するsetupPlayerメソッドを追加します。

GameScene.swift

//////////////// ▼▼ 追加 ▼▼ ////////////////
/// プレイヤーを構築
func setupPlayer() {
// Playerのパラパラアニメーション作成に必要なSKTextureクラスの配列を定義
var playerTexture = [SKTexture]()
// パラパラアニメーションに必要な画像を読み込む
for imageName in Constants.PlayerImages {
let texture = SKTexture(imageNamed: imageName)
texture.filteringMode = .Linear
playerTexture.append(texture)
}
// キャラクターのアニメーションをパラパラ漫画のように切り替える
let playerAnimation = SKAction.animateWithTextures(playerTexture, timePerFrame: 0.2)
// パラパラアニメーションをループさせる
let loopAnimation = SKAction.repeatActionForever(playerAnimation)
// キャラクターを生成
player = SKSpriteNode(texture: playerTexture[0])
// 初期表示位置を設定
player.position = CGPoint(x: self.frame.size.width * 0.35, y: self.frame.size.height * 0.6)
// アニメーションを設定
player.runAction(loopAnimation)

self.addChild(player)
}
//////////////// ▲▲ 追加 ▲▲ ////////////////

パラパラアニメーションを実現するためにSKTextureクラスの配列を用意し、for構文で先ほど定義したConstantsの構造体から画像ファイル名を取得し、SKTextureの画像をセットして配列にしています。

// Playerのパラパラアニメーション作成に必要なSKTextureクラスの配列を定義
var playerTexture = [SKTexture]()
// パラパラアニメーションに必要な画像を読み込む
for imageName in Constants.PlayerImages {
let texture = SKTexture(imageNamed: imageName)
texture.filteringMode = .Linear
playerTexture.append(texture)
}

そのSKTextureの配列を使ってSKActionを生成し、下記のプログラムでパラパラアニメーションのアクションを定義しています。

// キャラクターのアニメーションをパラパラ漫画のように切り替える
let playerAnimation = SKAction.animateWithTextures(playerTexture, timePerFrame: 0.2)
// パラパラアニメーションをループさせる
let loopAnimation = SKAction.repeatActionForever(playerAnimation)

いままでと同様に、このメソッドをdidMoveToViewメソッドに追加して動作確認してみましょう。

GameScene.swift

// シーンが表示された時に呼ばれる
override func didMoveToView(view: SKView) {
:
:
//////////////// ▼▼ 追加 ▼▼ ////////////////
// キャラクターを構築
self.setupPlayer()
//////////////// ▲▲ 追加 ▲▲ ////////////////
}

これで実行してみましょう。キャラクターがパタパタ動いているのが確認できると思います。

キャラクターのタップ時アニメーション設定

画面をタップしたときにキャラクターがふわっと上昇して一定のとこでゆっくり下降する動きを物理シュミレーションを使って実装します。

GameScene.swift

/// プレイヤーを構築
func setupPlayer() {
:
:
// アニメーションを設定
player.runAction(loopAnimation)
//////////////// ▼▼ 追加 ▼▼ ////////////////
// 物理シミュレーションを設定
player.physicsBody = SKPhysicsBody(texture: playerTexture[0], size: playerTexture[0].size())
// 重力の影響を受けるようにする
player.physicsBody?.dynamic = true
// 画像の角度などが物理シュミレーションに影響をなし
player.physicsBody?.allowsRotation = false
// 自分自身にPlayerカテゴリを設定
player.physicsBody?.categoryBitMask = ColliderType.Player
// 衝突判定相手にWorldとCoralを設定
player.physicsBody?.collisionBitMask = ColliderType.World | ColliderType.Coral
player.physicsBody?.contactTestBitMask = ColliderType.World | ColliderType.Coral
//////////////// ▲▲ 追加 ▲▲ ////////////////
self.addChild(player)
}

物理シュミレーションを設定するには、SKNodephysicsBodyプロパティにSKPhysicsBodyクラスを追加します。SKPhysicsBodyを生成するときにSKTextureを渡す事により、SKTextureの形にそって衝突判定が実行されるようになります。

dynamicプロパティは重力の影響を受けるかどうかを制御します。trueにすることにより影響をうけるようになります。

player.physicsBody?.collisionBitMaskには衝突判定する対象を設定します。

didMoveToViewに下記を追加して実行してみましょう。
画面をタップするとキャラクターが上に上昇するのが確認できます。

override func didMoveToView(view: SKView) {
//////////////// ▼▼ 追加 ▼▼ ////////////////
// 物理シミュレーションを設定
self.physicsWorld.gravity = CGVector(dx: 0.0, dy: -2.0)
//////////////// ▲▲ 追加 ▲▲ ////////////////

障害物の設置

次に障害物を設置していきます。画面外に生成し、画面内を移動させ、画面外にでたときに削除するようにします。

//////////////// ▼▼ 追加 ▼▼ ////////////////
// 障害物のサンゴを構築
func setupCoral() {
// サンゴ画像を読み込み
let coralUnder = SKTexture(imageNamed: "coral_under")
coralUnder.filteringMode = .Linear
let coralAbove = SKTexture(imageNamed: "coral_above")
coralAbove.filteringMode = .Linear
// 移動する距離を算出
let distanceToMove = CGFloat(self.frame.size.width + 2.0 * coralUnder.size().width)
// 画面外まで移動するアニメーションを作成
let moveAnim = SKAction.moveByX(-distanceToMove, y: 0.0, duration:NSTimeInterval(distanceToMove / 100.0))
// 自身を取り除くアニメーションを作成
let removeAnim = SKAction.removeFromParent()
// 2つのアニメーションを順に実行するアニメーションを作成
let coralAnim = SKAction.sequence([moveAnim, removeAnim])
// サンゴを生成するメソッドを呼び出すアニメーションを作成
let newCoralAnim = SKAction.runBlock({
// サンゴに関するノードを乗せるノードを作成
let coral = SKNode()
coral.position = CGPoint(x: self.frame.size.width + coralUnder.size().width * 2, y: 0.0)
coral.zPosition = -40.0

// 地面から伸びるサンゴのy座標を算出
let height = UInt32(self.frame.size.height / 12)
let y = CGFloat(arc4random_uniform(height * 2) + height)

// 地面から伸びるサンゴを作成
let under = SKSpriteNode(texture: coralUnder)
under.position = CGPoint(x: 0.0, y: y)
// サンゴに物理シミュレーションを設定
under.physicsBody = SKPhysicsBody(texture: coralUnder, size: under.size)
under.physicsBody?.dynamic = false
under.physicsBody?.categoryBitMask = ColliderType.Coral
under.physicsBody?.contactTestBitMask = ColliderType.Player
coral.addChild(under)

// 天井から伸びるサンゴを作成
let above = SKSpriteNode(texture: coralAbove)
above.position = CGPoint(x: 0.0, y: y + (under.size.height / 2.0) + 160.0 + (above.size.height / 2.0))

// サンゴに物理シミュレーションを設定
above.physicsBody = SKPhysicsBody(texture: coralAbove, size: above.size)
above.physicsBody?.dynamic = false
above.physicsBody?.categoryBitMask = ColliderType.Coral
above.physicsBody?.contactTestBitMask = ColliderType.Player
coral.addChild(above)

// スコアをカウントアップするノードを作成
let scoreNode = SKNode()
scoreNode.position = CGPoint(x: (above.size.width / 2.0) + 5.0, y: self.frame.height / 2.0)

// スコアノードに物理シミュレーションを設定
scoreNode.physicsBody = SKPhysicsBody(rectangleOfSize: CGSize(width: 10.0, height: self.frame.size.height))
scoreNode.physicsBody?.dynamic = false
scoreNode.physicsBody?.categoryBitMask = ColliderType.Score
scoreNode.physicsBody?.contactTestBitMask = ColliderType.Player
coral.addChild(scoreNode)
coral.runAction(coralAnim)
self.coralNode.addChild(coral)
})
// 一定間隔待つアニメーションを作成
let delayAnim = SKAction.waitForDuration(2.5)
// 上記2つを永遠と繰り返すアニメーションを作成
let repeatForeverAnim = SKAction.repeatActionForever(SKAction.sequence([newCoralAnim, delayAnim]))
// この画面で実行
self.runAction(repeatForeverAnim)
}
//////////////// ▲▲ 追加 ▲▲ ////////////////

障害物の表示は下図の仕組みで作成します。

画面の高さを12分割したうちの1つを基準として、その基準点に上下のズレの分を足しこみます。
arc4random_uniformメソッドで、引数を超えないランダムな値を取得します。
引数には画面高さの12分の2をブレ幅として渡しています。

またスコアをカウントアップするためのSKNodeを設定しています。目には見えませんが、存在する位置は先ほどのサンゴの位置関係で示したとおりです。

// スコアをカウントアップするノードを作成
let scoreNode = SKNode()
scoreNode.position = CGPoint(x: (above.size.width / 2.0) + 5.0, y: self.frame.height / 2.0)

// スコアノードに物理シミュレーションを設定
scoreNode.physicsBody = SKPhysicsBody(rectangleOfSize: CGSize(width: 10.0, height: self.frame.size.height))
scoreNode.physicsBody?.dynamic = false
scoreNode.physicsBody?.categoryBitMask = ColliderType.Score
scoreNode.physicsBody?.contactTestBitMask = ColliderType.Player
coral.addChild(scoreNode)

いままでと同様に、このメソッドをdidMoveToViewメソッドに追加して動作確認してみましょう。

GameScene.swift

// シーンが表示された時に呼ばれる
override func didMoveToView(view: SKView) {
:
:
//////////////// ▼▼ 追加 ▼▼ ////////////////
// 障害物のサンゴを構築
self.setupCoral()
//////////////// ▲▲ 追加 ▲▲ ////////////////
}

スコア情報の設置

スコアに関する部分を実装していきます。
まずメンバ変数としてスコアを表示させるためのSKLabelNodeUInt32型のscoreを定義します。

GameScene.swift

class GameScene: SKScene {
//////////////// ▼▼ 追加 ▼▼ ////////////////
// スコアを表示するラベル
var scoreLabelNode: SKLabelNode!
// スコアの内部変数
var score: UInt32!
//////////////// ▲▲ 追加 ▲▲ ////////////////

次にスコアラベルを構築するためのsetupScoreLabelメソッドを定義します。

GameScene.swift

//////////////// ▼▼ 追加 ▼▼ ////////////////
// スコアラベルを構築
func setupScoreLabel() {
// フォント名"Arial Bold"でラベルを作成
scoreLabelNode = SKLabelNode(fontNamed: "Arial Bold")
// フォント色を黄色に設定
scoreLabelNode.fontColor = UIColor.blackColor()
// 表示位置を設定
scoreLabelNode.position = CGPoint(x: self.frame.width / 2.0, y: self.frame.size.height * 0.9)
// 最前面に表示
scoreLabelNode.zPosition = 100.0
// スコアを表示
scoreLabelNode.text = String(score)

self.addChild(scoreLabelNode)
}
//////////////// ▲▲ 追加 ▲▲ ////////////////

いままでと同様に、このメソッドをdidMoveToViewメソッドに追加して動作確認してみましょう。

GameScene.swift

// シーンが表示された時に呼ばれる
override func didMoveToView(view: SKView) {
:
:
//////////////// ▼▼ 追加 ▼▼ ////////////////
// スコアラベルの構築
score = 0
self.setupScoreLabel()
//////////////// ▲▲ 追加 ▲▲ ////////////////
}

衝突処理の実装

今の状態では、物理シュミレーションを設定したSKNodeが衝突しても、このままでは衝突したイベントをハンドルできません。SpriteKitには、衝突しはじめたこと、衝突し終わったことを検知する仕組みがあります。

衝突イベントをハンドルできるようにGameSceneクラスにSKPhysicsContactDelegateプロトコルを追加します。

GameScene.swift

class GameScene: SKScene,SKPhysicsContactDelegate {

また、didMoveToViewメソッド内にself.physicsWorld.contactDelegate = selfを追加します。

GameScene.swift

// シーンが表示された時に呼ばれる
override func didMoveToView(view: SKView) {
// 物理シミュレーションを設定
self.physicsWorld.gravity = CGVector(dx: 0.0, dy: -2.0)
//////////////// ▼▼ 追加 ▼▼ ////////////////
self.physicsWorld.contactDelegate = self
//////////////// ▲▲ 追加 ▲▲ ////////////////
:
:
}

プロトコルを追加したら、didBeginContactメソッドを追加してください。

// 衝突時の検知
func didBeginContact(contact: SKPhysicsContact) {
print("Contact!!!")
}

このようにキャラクターが衝突したらContact!!!のログが表示されます。

衝突時の処理

GameScene.swift

// 衝突時の検知
func didBeginContact(contact: SKPhysicsContact) {
print("Contact!!!")
//////////////// ▼▼ 追加 ▼▼ ////////////////
// 既にゲームオーバー状態の場合
if baseNode.speed <= 0.0 {
return
}

// スコア、None定義
let rawScoreType = ColliderType.Score
let rawNoneType = ColliderType.None

// (画面に表示されていないが)スコア衝突タイプと衝突(つまり障害物を通過した)時
if (contact.bodyA.categoryBitMask & rawScoreType) == rawScoreType ||
(contact.bodyB.categoryBitMask & rawScoreType) == rawScoreType {
// スコアを加算しラベルに反映
score = score + 1
scoreLabelNode.text = String(score)

// スコアラベルをアニメーション
let scaleUpAnim = SKAction.scaleTo(1.5, duration: 0.1)
let scaleDownAnim = SKAction.scaleTo(1.0, duration: 0.1)
scoreLabelNode.runAction(SKAction.sequence([scaleUpAnim, scaleDownAnim]))

// スコアカウントアップに設定されているcontactTestBitMaskを変更
if (contact.bodyA.categoryBitMask & rawScoreType) == rawScoreType {
contact.bodyA.categoryBitMask = ColliderType.None
contact.bodyA.contactTestBitMask = ColliderType.None
} else {
contact.bodyB.categoryBitMask = ColliderType.None
contact.bodyB.contactTestBitMask = ColliderType.None
}
// None衝突タイプに衝突(つまりなにも衝突していない)時
} else if (contact.bodyA.categoryBitMask & rawNoneType) == rawNoneType ||
(contact.bodyB.categoryBitMask & rawNoneType) == rawNoneType {
// なにもしない
// それ以外(つまり障害物、地面、天井に衝突)時
} else {
// baseNodeに追加されたものすべてのアニメーションを停止
baseNode.speed = 0.0

// プレイキャラのBitMaskを変更
player.physicsBody?.collisionBitMask = ColliderType.World
// プレイキャラに回転アニメーションを実行
let rolling = SKAction.rotateByAngle(CGFloat(M_PI) * player.position.y * 0.01, duration: 1.0)
player.runAction(rolling, completion:{
// アニメーション終了時にプレイキャラのアニメーションを停止
self.player.speed = 0.0
})
}
//////////////// ▲▲ 追加 ▲▲ ////////////////
}

まず、スコア(上下の障害物のところにある画面に表示されていないブロック)とNoneの衝突タイプを定義して下記の条件で処理を分けています。

  1. (画面に表示されていないが)スコア衝突タイプと衝突(つまり障害物を通過した)時
  2. None衝突タイプに衝突(つまりなにも衝突していない)時
  3. それ以外(つまり障害物、地面、天井に衝突)時

1の場合は、スコアを加算してアニメーションする処理を実装しています。

didBeginContactメソッドの引数contactの中には、bodyAプロパティとbodyBプロパティが存在し、衝突したノードが設定されています。

categoryBitMaskプロパティで、設定されているカテゴリがColliderType.Scoreなのかどうかを論理積をとって判断しています。

キャラクターの表面すべてが衝突面となrので、キャラクターがスコアカウントアップのノードを通過している間は、didBeginContactメソッドがなんども呼ばれます。それだと正しくスコアがカウントされないので、呼び出しを1回で済むようにスコアカウントアップのノードのcategoryBitMaskcontactTestBitMaskColliderType.Noneに変更しています。

3の条件のときには、障害物、地面、天井に衝突した場合なので、背景などのアニメーションを全て停止しています。

さらに、キャラクターを回転しながら落下させるアニメーションも実行しています。

リスタートの実装

最後にゲームオーバーになった状態で、画面をタップするとゲームをリスタートできるようにします。

GameScene.swift

// ユーザーが画面をタッチした際に呼ばれる
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
//////////////// ▼▼ 追加 ▼▼ ////////////////
// ゲーム進行中のとき
if 0.0 < baseNode.speed {
//////////////// ▲▲ 追加 ▲▲ ////////////////
for touch in touches {
_ = touch.locationInNode(self)
// プレイヤーに加えられている力をゼロにする
player.physicsBody?.velocity = CGVector.zero
// プレイヤーにy軸方向へ力を加える
player.physicsBody?.applyImpulse(CGVector(dx: 0.0, dy: 23.0))
}
//////////////// ▼▼ 追加 ▼▼ ////////////////
} else if baseNode.speed == 0.0 && player.speed == 0.0 {
// ゲームオーバー時はリスタート
// 障害物を全て取り除く
coralNode.removeAllChildren()

// スコアをリセット
score = 0
scoreLabelNode.text = String(score)

// プレイキャラを再配置
player.position = CGPoint(x: self.frame.size.width * 0.35, y: self.frame.size.height * 0.6)
player.physicsBody?.velocity = CGVector.zero
player.physicsBody?.collisionBitMask = ColliderType.World | ColliderType.Coral
player.zRotation = 0.0

// アニメーションを開始
player.speed = 1.0
baseNode.speed = 1.0
}
//////////////// ▲▲ 追加 ▲▲ ////////////////
}

ゲームオーバになっているかどうかは、baseNodespeedが0かどうかで判断できるので、その条件で分岐しています。

ゲームオーバの場合は障害物を全て取り除き、スコアを0にリセットし、プレイキャラを再配置してから、キャラクターとbaseNodespeedを0から1.0に戻しています。

これでアプリは完成です。