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

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

f:id:tukumosanzou:20180702205113p:plain

今回は、Player や enemy が爆発した時のエフェクトを作るところから始めたいと思います。


シューティングゲームであれば衝突 -> 爆発ですから、衝突したときの爆発のエフェクトが必要になります。


画像を使う方法もありますが今回は、SpriteKit の SKEmitterNode を使用したいので次の様にします。


メニューで、File / New / File / iOS / Resource / SpriteKit Particle File を選択して Next。

f:id:tukumosanzou:20180722223806p:plain


Particle Template は Fire で Next。

f:id:tukumosanzou:20180722223940p:plain

ファイル名を ExplosionEffect.sks で Create。

f:id:tukumosanzou:20180723080616p:plain



次に、ExplosionEffect.sks を開いて各パラメーターを図の様にします。

SpriteKit Particle Emitter エディターが表示されます。

爆発のエフェクトができてます。


パラメーターはいろいろ調整してみてください。

f:id:tukumosanzou:20180723080849p:plain



これで衝突判定を行うための準備ができました。


衝突判定を行う func didBegin(_ contact: SKPhysicsContact){ } を作成します。

SKPhysicsContactDelegate と physicsWorld.contactDelegate = self を追加しましたがそれによってfunc didBegin() が使えるようになります。

SKEmitterNode - SpriteKit | Apple Developer Documentation

SPRITEKIT PARTICLE EMITTER EDITOR


全体図をはこうなります。

func didBegin(_ contact: SKPhysicsContact) {

    //衝突した2つの物体のどちらかの名前が"laser"であるか判定する。
    if contact.bodyA.node?.name == "laser" || contact.bodyB.node?.name == "laser" {

        //bodyAの名前が"enemy"のとき。
        if contact.bodyA.node?.name == "enemy" {

            //両方の物体をGameSceneから削除する。
            contact.bodyA.node?.removeFromParent()
            contact.bodyB.node?.removeFromParent()
            

            //爆発の画像を読み込む。    
            if let explosion = SKEmitterNode(fileNamed: "ExplosionEffect") {

                //爆発の画像をenemyの位置に表示する。
                explosion.position = (contact.bodyA.node?.position)!

                //GameSceneに追加する。
                addChild(explosion)
            }
                
        } else {

            //両方の物体をGameSceneから削除する。
            contact.bodyA.node?.removeFromParent()
            contact.bodyB.node?.removeFromParent()
            

            //爆発の画像を読み込む。
            if let explosion = SKEmitterNode(fileNamed: "ExplosionEffect") {

                //爆発の画像をenemyの位置に表示する。
                explosion.position = (contact.bodyB.node?.position)!

                //GameSceneに追加する。
                addChild(explosion)
            }
        }
        

        //爆発した時のサウンドを再生する。    
        run(explosionSound)
        
        return
    }
        
    guard contact.bodyA.node != nil && contact.bodyB.node != nil else { return }
       

    //衝突した2つの物体のどちらかの名前が"player"であるか判定する。
    if contact.bodyA.node?.name == "player" || contact.bodyB.node?.name == "player" {

         //爆発の画像を読み込む。
        if let explosion = SKEmitterNode(fileNamed: "ExplosionEffect") {

            //爆発の画像をenemyの位置に表示する。
            explosion.position = player.position

            //GameSceneに追加する。
            addChild(explosion)
        }
         

        //爆発した時のサウンドを再生する。   
        run(explosionSound)
        

        //両方の物体をGameSceneから削除する。    
        contact.bodyA.node?.removeFromParent()
        contact.bodyB.node?.removeFromParent()
    

        //playerを作成する。
        createPlayer()
            
    }
}




didBegin(_:) - Spritekit | Apple Developer Documentation にあるようにcontactパラメータに記述されている2つの物理的ボディは、保証された順序で渡されません。




どういう事かと言いますと。


衝突判定で使う didBegin() は bodyA、bodyB というふたつのパラメーターがあるんですが、これがぶつかる二つの物体になるわけです。


ただ、この物体はどちらが laser または enemy なのか決まっておらず、その時々で bodyA が laser だったり bodyB がlaser だったりするので2通りの判定が必要になるわけです。


ですので以下のように。


didBegi(_:) のcontactパラメーター(SKPhysicsContactクラスを継承)にはbodyA bodyB の二つの物理ボディがあり、これを使って衝突判定をするのですが前述にあるように必ずしも意図した順番ではないので、コードにあるようにそれぞれの物体に対して判定が必要になります。

SKPhysicsContact - SpriteKit | Apple Developer Documentation


今回は laser と enemy、player と enemy のそれぞれの衝突において bodyA bodyB の判定が必要になります。


前述の様なコードになります。

内容は特に難しいものではないと思います。


これで衝突判定はできましたが、せっかくですからもっとゲームらしくlaserがenemy当たったら得点が入るようにし、playerも3機までと限界を決めます。


まず得点からいきます。


import GameplayKit の直後に追加。

//得点の初期値
var gameScore = 0




var getTimer: Timer! の直後に追加。

//スコアを表示するためのプロパティをつくる。
var scoreLabel: SKLabelNode!

//scoreを監視し変更があればそれを反映させる。
var score = gameScore {

    //内包したものを監視する。
    didSet {
        scoreLabel.text = "SCORE: \(score)"
    }
}

didSet は内包したプロパティ(この場合 score)を監視し変更があればそれをリアルタイムで反映させることが出来ます。


ここでは得点が加算される度に表示が変わっていきます。


加算する処理追加します。


didBegin() の laser と enemy との衝突判定の処理の中の run(explosionSound) の直後に以下のコードを追加。

//gameScoreに1を足す。
gameScore += 1

//変数scoreを更新する。
score = gameScore



以下のようになります。

func didBegin(_ contact: SKPhysicsContact) {
    if contact.bodyA.node?.name == "laser" || contact.bodyB.node?.name == "laser" {
    
        //省略

    run(explosionSound)

    //ここに追加します。
    gameScore += 1
    score = gameScore

    }
}

これで enemy が laser で破壊されるたび変数 gameScore に1が加算されていきます、あとはそれを画面上に表示させれば良いだけですね。



続いて、画面上に表示する関数createScore()をつくり、それを呼び出すようにします。


createPlayer() の呼び出しの直後にcreateScore() を追加します。

createPlayer()

//得点を表示する。
createScore()



その処理内容を @objc func createEnemy() の直前に追加。

func createScore() {

    //フォントを指定する。
    scoreLabel = SKLabelNode(fontNamed: "Optima-ExtraBlack")

    //フォンサイズを指定する。
    scoreLabel.fontSize = 24

    //表示する位置。
    scoreLabel.position = CGPoint(x: frame.minX + 80, y: UIScreen.main.bounds.height * 0.92)

    //表示するテキスト。
    scoreLabel.text = "SCORE:0"

    //テキストの色を指定。
    scoreLabel.fontColor = UIColor.white

    //GameSceneに追加。
    addChild(scoreLabel)
}

@objc func createEnemy(){...}

内容は単純なのでわかりやすいと思います。


frame.minX は画面のx軸の最小値です(つまり0です)、そこから+80で少し右に寄せてます。


UIScreen.main.bounds.height はデバイス毎のスクリーンサイズの高さを取得できます、それによりデバイス毎の誤差をなるべく小さくするようにしています。

SKLabelNode - SpriteKit || Apple Developer Documentation



ここで、シミュレーターで確認して見ましょう。


laser で enemy が破壊される左上の得点が加算されていれば成功です。


次回は player の残機数を設定したいと思います。


今回は以上です。