獣は月夜に何を見る...

SpriteKitでゲーム その1- SPACE SHOOTER⑧

f:id:tukumosanzou:20180702205113p:plain

SpriteKit での作業も終盤にさしかかってきました、つたない説明が続きますが、お付き合いくださいませ。


今回は lives = 0 でゲームオーバーになる時の処理も追加していきます。


それにまだ player の移動処理を追加していないのでそれも追加します。


ついでにゲームの状態によってディスプレイの表示を管理したいと思います。

ゲームの状態を管理する変数をつくります。

var livesImages = SKSpriteNode の直前に追加してください、以下のようになります。

//ゲームの状態を管理する変数。
var gameState = GameState.startGame

var livesImages = [SKSpriteNode]()

var lives = 3

var player: SKSpriteNode!



次の列挙型を override func didMove() の直前に追加します。

enum GameState {
    case startGame
    case endGame
}

列挙型(enum)とは関連性のある事柄、データを一つにまとめた定数のようなものです。


switch文の

列挙型の内部データにアクセスするには ” . " で区切って”GameState.startGame " と書きます。

Enumerations — The Swift Programming Language (Swift 4.2)


override func touchesBegan() を以下のように変更します。


playerゲームオーバーの移動処理を追加していきます。

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {

    //画面にタッチしたかどうかの判定。
    guard let touch = touches.first else { return }

    //ゲームの状態によって処理を分ける。
    switch gameState {
    
    //gameStateがstartGameのとき。
    case .startGame:

        //タッチした位置を取得する。
        let location = touch.location(in: self)

        //タッチした位置がplayerのx軸の位置より小さい(左側)場合。
        if location.x < player.position.x {

            //playerの位置を-50移動する(左に50)
            player.position.x -= 50

        //タッチした位置がplayerのx軸の位置より大きい(右側)場合。
        } else if location.x > player.position.x {

            //playerの位置を+50移動する(右に50)
            player.position.x += 50
        }
    
        //playerの右方向の移動範囲の最大値を決める。
        if player.position.x > frame.maxX - player.size.width {
            player.position.x = frame.maxX - player.size.width / 2
        }

        //playerの左方向の移動範囲の最大値を決める。
        if player.position.x < frame.minX + player.size.width {
            player.position.x = frame.minX + player.size.width / 2
        }

        createLaser()

    //gameStateがendGameのとき。
    case .endGame

        //何も処理をしない。
        break
    }
}

player の中心点、つまり SpriteNode の anchorPoint より右側をタッチすれば右方向に+50移動し、左側をタッチすれば左に-50移動します。

しかし移動範囲は画面の幅以内にしたいので、以下の部分で

//playerの右方向の移動範囲の最大値を決める。
if player.position.x > frame.maxX - player.size.width {
    player.position.x = frame.maxX - player.size.width / 2
}

//playerの左方向の移動範囲の最大値を決める。
if player.position.x < frame.minX + player.size.width {
    player.position.x = frame.minX + player.size.width / 2
}

としていますが、playeranchorPoint(0.5, 0.5) つまり中心なので player の大きさの半分の幅を移動範囲の最大・最小から取り除かないと player の半分が画面からはみ出してしまいます。


ゲームオーバー時の処理に移ります。

以下のコードを func subtractLife() に追加します。

//残機数がゼロならば。
if lives == 0 {

    //gameStateをendGameに変更する。
    gameState = .endGame

    //ゲームオーバーの処理を実行する。
    runGameOver()
}



追加する位置は、以下のようになります。

func subtractLife() {
    
    //初期値から-1する。
    lives -= 1

    //関数内部で使う変数を設定。
    var life: SKSpriteNode

    if lives == 2 {
        life = livesImages[0]
    } else if lives == 1 {
        life = livesImages[1]
    } else {
        life = livesImages[2]
    }

    //GameSceneから削除する。
    life.removeFromParent()

    //残機数がゼロならば。
    if lives == 0 {

        //gameStateをendGameに変更する。
        gameState = .endGame

        //ゲームオーバーの処理を実行する。
        runGameOver()
    }

}



ゲームオーバーの処理を行う func runGameOver() func subtractLife() の直前に追加します。

func runGameOver() {

    //Timerがまだ有効になっているかチェックする。
    if getTimer != nil {

        //タイマーを破棄する
        getTimer.invalidate()

        //Timerを無効にする。
        getTimer = nil
    }

    //func changeSceneを実行するアクションを作成。
    let changeSceneAction = SKAction.run(changeScene)

    //2.5秒のアイドル状態のアクションを作成します。
    let waitToChangeScene = SKAction.wait(forDuration: 2.5)

    //アクションのコレクションを順番に実行するアクションを作成。
    let changeSceneSequence = SKAction.sequence([waitToChangeScene, changeSceneAction])

    //アクションを実行する。
    run(changeSceneSequence)
}

getTimerTimer.scheduledTimer(timeInterval:target:selector:userInfo:repeats:) を格納しています、パラメーターを repeats: true にしてあるので無効になるまでタイマーを繰返しますのでゲームオーバー時には無効にする必要があります。


先ずその処理を行い、次に2.5秒おいて画面をゲームオーバーの画面に切り替える処理を行います。

Timer.scheduledTimer(timeInterval:target:selector:userInfo:repeats:) wait(forDuration:) - SKAction | Apple Developer Documentation sequence(_:) - SKAction | Apple Developer Documentation


画面をゲームオーバーの画面に切り替える処理を作成します。

func changeScene() func runGameOver() の直前に追加します。

func changeScene() {
    
    //sceneにGameOverSceneを指定しサイズを端末の画面にします。
    let scene = GameOverScene(size: frame.size)

    //シーンを提示するビューにシーンがどのようにマッピングされるかを定義します。
    scene.scaleMode = self.scaleMode

    //画面遷移のアニメーションを作成する。
    let transition = SKTransition.crossFade(withDuration: 1.5)

    //現在のシーンから新しいシーンに移行します
    view?.presentScene(scene, transition: transition)
}

この処理でゲームオーバー時に表示する画面に切り替えます。


移動先の画面を指定し、アニメーションで1.5秒かけて徐々に画面が現れるように調整します。
SKScene - SpriteKit | Apple Developer Documentation scaleMode - SKScene | Apple Developer Documentation SKTransition - SpriteKit | Apple Developer Documentation crossFade(withDuration:) - SKTransition | Apple Developer Documentation presentScene(_: transition:) - SKView | Apple Developer Documentation


以下のような画面に切り替わるようにしたいと思います。
f:id:tukumosanzou:20180727234533p:plain


では、ゲームオーバーの画面を作成していきます。


新しくファイルをつくります。

File / New / File / ios / Swift File で GameOverScene.swift を作成しましょう。

コードは以下のようになります。

import Foundation
import SpriteKit

class GameOverScene: SKScene {
    

    //テキストラベルを表示するノードを作成し、フォントを指定する。
    let restartLabel = SKLabelNode(fontNamed: "AppleSDGothicNeo-Bold")
    
    override func didMove(to view: SKView) {
        
        //背景のノードを作成。
        let background = SKSpriteNode(imageNamed: "background")
        //位置を指定。
        background.position = CGPoint(x: frame.size.width / 2, y: frame.size.height / 2)
        //重なり順を指定。
        background.zPosition = 0
        //GameOverSceneに追加。
        addChild(background)
        

        //GAME OVERのテキストラベルを表示するノードを作成し、フォントを指定する。
        let gameeOverLabel = SKLabelNode(fontNamed: "Helvetica-Bold")
        //表示するテクストを指定。
        gameeOverLabel.text = "GAME OVER"
        //フォントのサイズを指定。
        gameeOverLabel.fontSize = 60
        //フォントのカラーを指定。
        gameeOverLabel.fontColor = SKColor.white
        //位置を指定。
        gameeOverLabel.position = CGPoint(x: frame.size.width * 0.5, y: frame.size.height * 0.7)
        //重なり順を指定。
        gameeOverLabel.zPosition = 1
        //GameOverSceneに追加。
        addChild(gameeOverLabel)
        
        //SCOREのテキストラベルを表示するノードを作成し、フォントを指定する。
        let scoreLabel = SKLabelNode(fontNamed: "AppleSDGothicNeo-Bold")
        //表示するテクストを指定。
        scoreLabel.text = "SCORE: \(gameScore)"
        //フォントのサイズを指定。
        scoreLabel.fontSize = 30
        //フォントのカラーを指定。
        scoreLabel.fontColor = SKColor.white
         //位置を指定。
        scoreLabel.position = CGPoint(x: frame.size.width / 2, y: frame.size.height * 0.55)
        //重なり順を指定。
        scoreLabel.zPosition = 1
        //GameOverSceneに追加。
        addChild(scoreLabel)
        
        //ユーザーのデフォルトデータベースとのインターフェイス。
        let defaults = UserDefaults()
        //保存するデータと関連づけるためのキーを作成。
        var highScoreNumber = defaults.integer(forKey: "highScoreSaved")
        //得点が過去の得点より高いならば。
        if gameScore > highScoreNumber {
            //ハイスコアをゲームスコアで更新する。
            highScoreNumber = gameScore
            //デフォルト・データベースに格納するオブジェクトとデフォルトキーの値を設定します。 
            defaults.set(highScoreNumber, forKey: "highScoreSaved")
        }
        
        //ハイスコア(HIGH SCORE)用。
        let highScoreLabel = SKLabelNode(fontNamed: "AppleSDGothicNeo-Bold")
        highScoreLabel.text = "HIGH SCORE: \(highScoreNumber)"
        highScoreLabel.fontSize = 30
        highScoreLabel.fontColor = SKColor.white
        highScoreLabel.zPosition = 1
        highScoreLabel.position = CGPoint(x: frame.size.width / 2, y: frame.size.height * 0.45)
        addChild(highScoreLabel)
        
        //再スタート(Restart)用。
        restartLabel.text = "Restart"
        restartLabel.fontSize = 30
        restartLabel.fontColor = SKColor.white
        restartLabel.zPosition = 1
        restartLabel.position = CGPoint(x: frame.size.width / 2, y: frame.size.height * 0.3)
        addChild(restartLabel)

    }
    
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        for touch: AnyObject in touches {
            //タッチした位置を検出。
            let pointOfTouch = touch.location(in :self)
            //タッチした位置が"Restart"の位置に含まれるかチェックする。
            if restartLabel.contains(pointOfTouch) {
                let scene = GameScene(size: self.size)
                scene.scaleMode = self.scaleMode
                let transition = SKTransition.crossFade(withDuration: 0.5)
                view?.presentScene(scene, transition: transition)
            }
        }
    }
}

GAME OVER, SCORE, HIGH SCORE, Restart のそれぞれのテキストを表示するだけです。


ポイントは得点(SCORE)を表示する部分とハイスコア(HIGH SCORE)を毎回リセットする部分とタッチするとゲームを再開(Restart)するリスタートの部分です。


SCOREgameScoreGameScene.swiftグローバル変数として定義しているので GameOverScene.swift からでも取得できるので簡単です。


HIGH SCORE は取得した SCORE より大きければ デフォルトキーを関連付けてデフォルトデータベースHIGH SCORE として格納し使用します。


Restartゲームオーバー画面に遷移する GameScene.swiftfunc changeScene() の内部処理と同じです、違う点は override touchesBegan() の内部で呼び出しを行い Restart のテキストをタッチしたら GameScene.swift を呼び出しゲームが再開することです。

SKLabelNode - SpriteKit | Apple DeveloperDocumentation SKSpriteNode - SpriteKit | Apple DeveloperDocumentation UseDefault - Foundation | Apple DeveloperDocumentation integer(forKye:) - UserDefaults | Apple Developer Documentation set(_: forKey:) - UserDefaults | Apple Developer Documentation

UITouch - UIKit | Apple Developer Documentation location(in:) - UITouch | Apple Developer Documentation


最後にデバイスごとの表示の調整をします。

GameViewController.swift を開きます。

以下の部分を見つけてください。

if let view = self.view as! SKView {
    //Load the SKScene from 'GameScene.sks'
    if let Scene = SKScene(fileNamed: "GameScene") {
        //Set the scale mode to scale to fit the window
        scene.scaleMode = .aspectFill

        //Present the scene
        view.presentScene(scene)
    }

    view.ignoresSiblingOrder = true

    view.showsFPS = true
    view.showsNodeCount = true
    view.showsPhysics = true
}

そしてこのように変更します。

if let view = self.view as! SKView? {
    //シーンにGameSceneがセットされているならば
    if let scene = SKScene(fileNamed: "GameScene") {
        //デバイスがiPadならば。
        if (UIDevice.current.model.range(of: "iPad") != nil) {
            //シーンのサイズが常にビューのサイズと一致するように、自動的に変更されます。
            scene.scaleMode = .resizeFill
        //デバイスの画面サイズの高さのピクセルが2436.0であるならば。
        } else if UIScreen.main.nativeBounds.height == 2436.0 {
            //シーンのサイズが常にビューのサイズと一致するように、自動的に変更されます。
            scene.scaleMode = .resizeFill
        //上記いがいならば
        } else {
            //シーンのサイズが画面に収まるように切り取ります。
            scene.scaleMode = .aspectFill
        }
        //現在のシーンにセットする。
        view.presentScene(scene)
    }

    view.ignoresSiblingOrder = true
    
    view.showsFPS = true
    view.showsNodeCount = true
    view.showsPhysics = true
}

シーンの遷移のときにでてきた部分にデバイスの判別を追加しています。

1番目にデバイスiPadなのか識別し、2番目は iPhone X 用ですデバイスの画面の高さをピクセルで取得し iPhone X の画面の高さと同じならばシーンのサイズを自動的に調整します。

3番目はiPhone 5s 以降で iPhone X 以外の iPhone 用になります、シーンの大きさはアスペクト比の大きい方をもとにスケーリングされるので切り取られる場合があります。

showsFPSshowsNOdeCount はゲームをプレイしているときの画面の右下に出ている nodefps の表示の切り替えです、false にすると消えます。

showsPhysics は player やその他の物理ボディのまわりの青い円の表示を切り替えます、false にすると消えます。

UIDevice - UIKit |Apple Developer Ducumentation
SKSceneScaleMode - SpriteKit | Apple Developer Documentation
presentScene(_:) - SKView | Apple Dveloper Documentation
showsFPS - SKView | Apple Developer Documntation
showsNodeCount - SKView | Apple Developer Documentation
showsPhysics - SKView | Apple Developer Documentation



以上で、シューティングゲームとしての最低限のものは完成したと思います。

今回は以上です。


swiftを学ぶなら!
TechAcademy オンラインブートキャンプ iPhoneアプリコース