Xcode
iOS
SpriteKit
Swift
Swift3.0

SpriteKitの衝突処理について(categoryBitMask collisionBitMask contactTestBitMask 使い方)

More than 1 year has passed since last update.

はじめに

私はSpriteKitをアプリの部品の一部として使ったりするのですが、
ノード(ゲーム中の物体)同士の衝突処理であるcategoryBitMaskやcollisionBitMaskについて
理解に悩んだので書いてみました。
また、私の勝手な解釈を含みますので、考え方の1つとして理解して頂けたら幸いです。

physicsBodyのビットマスク

名前 説明
categoryBitMask 自分が属するカテゴリ値
collisionBitMask この値ぶつかってくる相手のcategoryBitMaskの値とをAND算出結果が1で衝突する
contactTestBitMask 物体と衝突した時に、通知として送る値

categoryBitMask

categoryBitMaskという値を使って「属する物理の世界において、値で違いをつけるもの」とイメージしてみました。

world.png

collisionBitMask

collisionBitMaskという値で「自分というノードが当てられる側の時」どの世界にいる値(categoryBitMask)のノードと当たるかを決めるものと理解してみました。

Collision.png

ここで分かりづらいのですが、衝突される/衝突されないという
れる/られるの関係があるのです。

サンプル

ANode
Green.png

LightNode
Light.png

として、サンプルをあげてみたいと思います。

GameScene.swift
aNode.physicsBody?.categoryBitMask = 0b0001
lightNode.physicsBody?.categoryBitMask = 0b0010

aNode.physicsBody?.collisionBitMask = 0b0010
lightNode.physicsBody?.collisionBitMask = 0b0100

Sample.png

このようにcategoryBitMaskとcollisionBitMaskをそれぞれのノードに設定し、動かして衝突させた場合このような感じです。

sample.gif

ANodeから衝突しに行っても、LightNodeのcollisionBitMaskは「0100」の世界のノードからしか衝突しないことになっているためLightNodeは動きません。
また、ANodeのcollisionBitMaskは「0010」の世界のノードと衝突することになっているため、LightNodeから衝突されるとANodeは動きます。

contactTestBitMask

contactTestBitMaskとは衝突相手のcategoryBitMask値を指定しておくことで衝突を検知できる「設定の値」と理解してみました。

Test.png

SKPhysicsContactDelegateでノード同士が衝突したことを検知できる仕組みがあります。
衝突を検知したいノードに、もう片方のcategoryBitMaskを指定することで、
didBegin(_:)メソッド内で衝突処理を書くことができます。
また、自身のcollisionBitMaskと必ずしも合わせる必要はなく、
衝突しないけれど重なったことは検知するというような使い方ができます。

サンプル

ANode
Green.png

BNode
Blue.png

LightNode
Light.png

GameScene.swift
import Foundation
import SpriteKit

class GameScene: SKScene {

    var backgroundNode: SKSpriteNode!
    var aNode: SKSpriteNode!
    var bNode: SKSpriteNode!
    var lightNode: SKShapeNode!
    var touchedNode: SKNode?

    override func didMove(to view: SKView) {
        physicsWorld.contactDelegate = self
        physicsBody = SKPhysicsBody(edgeLoopFrom: self.frame)
        backgroundNode = childNode(withName: "Background") as! SKSpriteNode
        backgroundNode.isPaused = true
        aNode = childNode(withName: "ANode") as! SKSpriteNode
        aNode.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: 100, height: 100))
        aNode.physicsBody?.affectedByGravity = false
        bNode = childNode(withName: "BNode") as! SKSpriteNode
        bNode.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: 100, height: 100))
        bNode.physicsBody?.affectedByGravity = false
        lightNode = childNode(withName: "Light") as! SKShapeNode
        lightNode.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: 50, height: 50))
        lightNode.physicsBody?.affectedByGravity = false

        aNode.physicsBody?.categoryBitMask = 0b0001
        bNode.physicsBody?.categoryBitMask = 0b0010
        lightNode.physicsBody?.categoryBitMask = 0b0001

        aNode.physicsBody?.collisionBitMask = 0b0001
        bNode.physicsBody?.collisionBitMask = 0b0001
        lightNode.physicsBody?.collisionBitMask = 0b0001

        lightNode.physicsBody?.contactTestBitMask = aNode.physicsBody!.categoryBitMask | bNode.physicsBody!.categoryBitMask
    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        if let pos = touches.first?.location(in: self) {
            touchedNode = atPoint(pos)
        }
    }

    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        if let pos = touches.first?.location(in: self) {
            let action = SKAction.move(to: pos, duration: 0.1)
            touchedNode?.run(action)
        }
    }
}

extension GameScene: SKPhysicsContactDelegate {
    func didBegin(_ contact: SKPhysicsContact) {
        print("------------衝突しました------------")
        print("bodyA:\(contact.bodyA.node?.name)")
        print("bodyB:\(contact.bodyB.node?.name)")
        if contact.bodyA.categoryBitMask == aNode.physicsBody!.categoryBitMask {
            lightNode.fillColor = UIColor.yellow
        }else if contact.bodyA.categoryBitMask == bNode.physicsBody!.categoryBitMask {
            lightNode.fillColor = UIColor.cyan
        }
    }

    func didEnd(_ contact: SKPhysicsContact) {
        lightNode.fillColor = UIColor(red: 216/255, green: 216/255, blue: 216/255, alpha: 216/255)
    }
}

light.gif

さいごに

func didBegin(_ contact: SKPhysicsContact)

contactが持つbodyA/bodyBの配置について、この順序について

Apple API Reference SKPhysicsContactDelegateのdidBegin(_:)より

The two physics bodies described in the contact parameter are not passed in a guaranteed order.

contactパラメータに記述されている2つの物理的ボディは、保証された順序で渡されません。

上の例ではbodyAにのみ条件を見ていましたが、何らかの理由で入れ替わることがあるかもしれないので、基本的にbodyBに対しても条件をかけていた方が良さそうです。

また、今回の例では以下のように

lightNode.physicsBody?.contactTestBitMask = aNode.physicsBody!.categoryBitMask | bNode.physicsBody!.categoryBitMask

LightNodeに対してcontactTestBitMaskを設定しましたが、

aNode.physicsBody?.contactTestBitMask = lightNode.physicsBody!.categoryBitMask
bNode.physicsBody?.contactTestBitMask = lightNode.physicsBody!.categoryBitMask

このようにANode/BNodeをそれぞれに設定しても
bodyA bodyBに変わりはありませんでした。

print.png

contactTestBitMaskには衝突される/衝突されないという関係はないのかなという疑問が残りました。

参考にさせていただいた記事

見て頂いてありがとうございます。