2017年11月16日木曜日

SpriteKit の衝突判定について挙動をまとめてみた

概要

SpriteKit の物理エンジンで使える衝突判定の設定について実際に手を動かしながらどうさ確認してみました
今更感もありますが個人的な備忘録として残しておきます

環境

  • macOS X 10.13.1
  • Xcode Version 9.1 (9B55)

categoryBitMask を設定してみる

とりあえず categoryBitMask だけをそれぞれ設定して実行できるところまで準備しましょう
GameScene.sks ファイルには SKSpriteNode を縦に適当に 3 つ配置しておきます
ちなみに以下では青ノード (node1)、緑ノード (node2)、ピンクノード (node3) として扱います
またピンクノードを落下させて挙動を確認しています

import SpriteKit

class GameScene: SKScene, SKPhysicsContactDelegate {

    var node1 = SKSpriteNode()
    var node2 = SKSpriteNode()
    var node3 = SKSpriteNode()

    let Node1: UInt32 = 0x1 << 1
    let Node2: UInt32 = 0x1 << 2
    let Node3: UInt32 = 0x1 << 3

    override func didMove(to view: SKView) {
        self.physicsWorld.contactDelegate = self
        node1 = self.childNode(withName: "node1") as! SKSpriteNode
        node1.physicsBody = SKPhysicsBody(rectangleOf: node1.size)
        node1.physicsBody?.affectedByGravity = false
        node1.physicsBody?.isDynamic = true
        node1.physicsBody?.categoryBitMask = Node1
        node2 = self.childNode(withName: "node2") as! SKSpriteNode
        node2.physicsBody = SKPhysicsBody(rectangleOf: node2.size)
        node2.physicsBody?.affectedByGravity = false
        node2.physicsBody?.isDynamic = true
        node1.physicsBody?.categoryBitMask = Node2
        node3 = self.childNode(withName: "node3") as! SKSpriteNode
    }

    func touchDown(atPoint pos : CGPoint) {
        if let node = atPoint(pos) as? SKSpriteNode {
            if node == node3 {
                node3.physicsBody = SKPhysicsBody(rectangleOf: node3.size)
                node3.physicsBody?.affectedByGravity = true
                node3.physicsBody?.isDynamic = true
                node3.physicsBody?.categoryBitMask = Node3
            }
        }
    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        for t in touches { self.touchDown(atPoint: t.location(in: self)) }
    }
}

friction や linearDumping, mass などは設定しないで OK です
この状態で実行すると以下のように衝突後 3 つのノードが下に落下します
contact_test1.gif

ちなみに categoryBitMask を適当に変更しても状況は変わりません
ノード間の衝突ロジックはデフォルトだと通過ではなく衝突になるようです
これを通過できるようにするのに collisionBitMask の設定をいじります

collisionBitMask を設定してみる

次に collisionBitMask を設定して挙動を確認してみます
まずは各ノードにそれぞれ設定します

node1.physicsBody?.collisionBitMask = Node1
node2.physicsBody?.collisionBitMask = Node2
node3.physicsBody?.collisionBitMask = Node3

でこれで挙動を確認すると以下のように変わります
contact_test2.gif

青 (node1) 、緑 (node2) ともに衝突せずピンク (node3) がすーっと通過していきました
なぜこうなるかというと

例えば、緑のカテゴリ (categoryBitMask) は 2進数表現で「100」です
ピンクの衝突フラグ (collisionBitMask) は 2進数表現で「1000」です
これの 2 つ値の論理積を取ると「0」になります
そう、0 になる 2 つノード間の categoryBitMask と collisionBitMask の論理積が 0 になると衝突せず通過となります
逆に論理積が 1 となると衝突します
一番始めのデモではそれぞれ衝突しましたが、デフォルトだと collisionBitMask はすべてのノードに衝突する設定になっているようです (print すると nil でした)

ではこれを踏まえて青 (node1) とピンク (node3) だけ衝突するようにしています

node3.physicsBody?.collisionBitMask = Node1

に変更しましょう
node1 側の collisionBitMask は変更しなくて OK です
これで実行すると以下のようになります
contact_test3.gif

青には衝突するようになりました
が青が動かなくなりました
isDynamic = true にしているのに止まります
もし衝突したときに青も動かしたい場合には node1 の collisionBitMask を 0 にします (もしくは設定しません)

node1.physicsBody?.collisionBitMask = 0

すると以下のような挙動になります
contact_test4.gif

緑で止まるのは青の collisionBitMask が全衝突判定 (0 or nil) になっているからです
ここで少し補足なのですが collisionBitMask が 0 or nil どちらでも同じ挙動になったということです
そして 0 or nil の場合は他と衝突した場合に isDynamic の設定の影響を受けるのですが明示的に設定している場合 (例えば今回のように node3.physicsBody?.collisionBitMask = Node1 などとした場合) は isDynamic が必ず false の挙動になるようです
これはおそらく仕様かなと思います (バグなような気もしますが、、、)

なので今回の場合、青に衝突してピンクと一緒に落下するけど緑はスルーしたいというケースはおそらく設定できないと思います
もし設定できる方法があれば教えていただきたいです

ちなみに青をスルーして緑に衝突させて停止させたい場合は以下のように設定します

node2.physicsBody?.collisionBitMask = Node2
node3.physicsBody?.collisionBitMask = Node2

さらにちなみに node2 の collisionBitMask の設定を削除すると緑も一緒に落下していきます

contactTestBitMask を設定してみる

これを設定することで衝突時にコールバックとして didBegin メソッドがコールされるようになります

didBegin メソッドは以下のような感じで実装しました

func didBegin(_ contact: SKPhysicsContact) {
    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
    }
    print(firstBody)
    print(secondBody)
}

上記が書けたら contactTestBitMask を各ノードに設定してみます
何も考えずそれぞれに設定してみます

node1.physicsBody?.contactTestBitMask = Node1
node2.physicsBody?.contactTestBitMask = Node2
node3.physicsBody?.contactTestBitMask = Node3

で実行しても didBegin メソッドはコールされないと思います
なぜか、node3.physicsBody?.contactTestBitMask = Node3 の設定の意味は

node3 が Node3 の categoryBitMask を設定しているノードに衝突したときに didBegin をコールする

という意味になります
今回の場合、node3 は node1 or node2 に衝突します
なので、以下のように書き換えてあげることがで didBegin メソッドがコールされるようになります

node3.physicsBody?.contactTestBitMask = Node1

これで node1 に衝突もしくは通過したときに didBegin メソッドがコールされるようになります
contact_test5.gif

見ての通り青は通過、緑で衝突するように collisionBitMask を設定しています
更に上記の通り contactTestBitMask は青を通過したときに didBegin がコールされるようにしています

ここでやりたくなるのが「緑と衝突したときも didBegin がコールされたい」だと思います
その場合は以下のように設定し直しましょう

node3.physicsBody?.contactTestBitMask = Node1 + Node2

これで実行すると緑と衝突したときもコールされるようになります
contact_test6.gif

こんな感じで衝突したいノードのカテゴリ情報を加算していけば OK です

firstBody と secondBody について

didBegin 内で衝突した 2 つノード情報を取得することができます
firstBody が衝突された側で secondBody が衝突した側になります
今回のサンプルで言うとピンクが second で 青や緑が first になります

青は緑に衝突する可能性があるので second になり得る可能性もあります

最後に

SpriteKit の衝突判定で使用する categoryBitMask, collisionBitMask, contactTestBitMask の挙動についてまとめてみました

categoryBitMask は名前の通りノードに設定するカテゴリみたいなものです
カテゴリに設定した情報を元に collisionBitMask や contactTestBitMask に設定した値と比較して衝突するのかスルーするのかコールバックメソッドをコールするのかしないのかを決定する感じです
基本は論理積を取って 0 は何もしない 1 ならアクションという感じです

正直ややこしいです
たぶん自分も今回の記事の説明を聞いただけではさっぱり理解できないと思います
なので、やはり手を動かしながら実際に挙動を確認して理解するのが良いかなと思います

SpriteKit の物理エンジンや衝突判定はややこしいですがこれを理解しないとおもしろいゲームは作れないと思うので頑張って理解してみてください

0 件のコメント:

コメントを投稿