【Swift】SpriteKitの使い方。カテゴリマスクと衝突マスクで衝突するノードを分ける(Swift 2.2、XCode 7.3)
ノードの衝突の設定
本記事では物理ボディが設定されたノードの衝突について説明する。
前回までの記事で、物理エンジンを使ってノードに重力をかける方法を説明してきた。⇒「物理エンジンとは」
落下するノードの進行方向に、物理ボディが設定された別のノードがある場合、必ず衝突が発生した。
全部のノードを衝突対象とするのでは無く、例えばノード1はノード2とだけ衝突させ、ノード2はノード1とノード3の2つと衝突させるなど、衝突させたいノードを分けたいことがある。そこで利用されるのがカテゴリマスクと衝突マスクである。
カテゴリマスクを設定する
では、ノードにカテゴリマスクを設定して、衝突するノードを分けてみよう。
以降の手順を行う前のXcodeプロジェクトをGitHubに置いたので、試してみる方はご利用下さい。
⇒「テスト用プロジェクト」
画面に3匹の鳥ノードを配置し、タップしたら重力を受けて落下するものを実装しておいた。鳥ノードは別の鳥ノードに衝突しながら落下する。
以下は変更前のTestScene.swift。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
// // TestScene.swift // import Foundation import SpriteKit class TestScene:SKScene { //現在シーン設定時の呼び出しメソッド override func didMoveToView(view: SKView) { //観測しやすくするため重力を少し弱くする。 self.physicsWorld.gravity = CGVector(dx:0, dy:-5.0) //画面端に物理ボディを設定する。 self.physicsBody = SKPhysicsBody(edgeLoopFromRect: self.frame) //背景画像のノードを作成する。 let backNode = SKSpriteNode(imageNamed: "night") backNode.size = self.frame.size backNode.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame)); //鳥のノードを作成する。 let birdBrown = SKSpriteNode(imageNamed: "bird_brown") let birdRed = SKSpriteNode(imageNamed: "bird_red") let birdBlue = SKSpriteNode(imageNamed: "bird_blue") //鳥の位置を設定する。 birdBrown.position = CGPointMake(CGRectGetMidX(self.frame), self.frame.height-100) birdRed.position = CGPointMake(CGRectGetMidX(self.frame)-35, self.frame.height-250) birdBlue.position = CGPointMake(CGRectGetMidX(self.frame)+35, self.frame.height-350) //物理ボディを設定する。 birdBrown.physicsBody = SKPhysicsBody(texture: birdBrown.texture!, size: birdBrown.size) birdRed.physicsBody = SKPhysicsBody(texture: birdRed.texture!, size: birdRed.size) birdBlue.physicsBody = SKPhysicsBody(texture: birdBlue.texture!, size: birdBlue.size) //カテゴリのビットマスクを設定する。 birdBrown.physicsBody?.categoryBitMask = 1 birdRed.physicsBody?.categoryBitMask = 1 birdBlue.physicsBody?.categoryBitMask = 1 //衝突するカテゴリのビットマスクを設定する。 birdBrown.physicsBody?.collisionBitMask = 1 birdRed.physicsBody?.collisionBitMask = 1 birdBlue.physicsBody?.collisionBitMask = 1 //他ノードに衝突されても動かなくする。 birdBrown.physicsBody!.dynamic = false birdRed.physicsBody!.dynamic = false birdBlue.physicsBody!.dynamic = false //ノードをシーンに追加する。 self.addChild(backNode) self.addChild(birdBrown) self.addChild(birdRed) self.addChild(birdBlue) } //画面タッチ時の呼び出しメソッド override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) { //タッチした座標のノードを取得する。 let location = touches.first!.locationInNode(self) let node = nodeAtPoint(location) as! SKSpriteNode if(node.physicsBody != nil) { //タッチしたノードを落とす。 node.physicsBody!.dynamic = true } } } |
注目するのは、上記コード中のカテゴリを設定する箇所。
衝突有無を分けるにはcategoryBitMask(以下、カテゴリマスク)とcollisionBitMask(以下、衝突マスク)を利用する。衝突させたいノードのカテゴリマスクの値を衝突マスクに設定すれば、ノードが衝突するようになる。
以下コードはカテゴリマスク、衝突マスクがすべて1に設定されているので、すべてのノード同士がぶつかる。
1 2 3 4 5 6 7 8 9 10 |
//カテゴリのビットマスクを設定する。 birdBrown.physicsBody?.categoryBitMask = 1 birdRed.physicsBody?.categoryBitMask = 1 birdBlue.physicsBody?.categoryBitMask = 1 //衝突するカテゴリのビットマスクを設定する。 birdBrown.physicsBody?.collisionBitMask = 1 birdRed.physicsBody?.collisionBitMask = 1 birdBlue.physicsBody?.collisionBitMask = 1 |
ややこしくなってきたので1つ問題を解いてみよう。
以下のコードのように設定して茶鳥を落下させた場合、どうなるか。
1 2 3 4 5 6 7 8 9 10 |
//カテゴリのビットマスクを設定する。 birdBrown.physicsBody?.categoryBitMask = 1 birdRed.physicsBody?.categoryBitMask = 2 birdBlue.physicsBody?.categoryBitMask = 4 //衝突するカテゴリのビットマスクを設定する。 birdBrown.physicsBody?.collisionBitMask = 1 birdRed.physicsBody?.collisionBitMask = 2 birdBlue.physicsBody?.collisionBitMask = 4 |
正解はどの鳥にも衝突しない。なぜなら、茶鳥の衝突マスクは1に設定されているので、カテゴリマスクが1のノードと衝突することになるが、赤鳥のカテゴリマスクは2、青鳥は4のため衝突しない。
では以下に設定した場合はどうなるか。
1 2 3 4 5 6 7 8 9 10 |
//カテゴリのビットマスクを設定する。 birdBrown.physicsBody?.categoryBitMask = 1 birdRed.physicsBody?.categoryBitMask = 2 birdBlue.physicsBody?.categoryBitMask = 3 //衝突するカテゴリのビットマスクを設定する。 birdBrown.physicsBody?.collisionBitMask = 1 birdRed.physicsBody?.collisionBitMask = 2 birdBlue.physicsBody?.collisionBitMask = 3 |
茶鳥の衝突マスクは1なのに対し、赤鳥のカテゴリマスクは2、青鳥は3なので衝突しないと思ったかもしれないが、茶鳥は青鳥と衝突する。
衝突した理由は、衝突の有無はビットマスクによって決定されるためである。
ビットマスクとは、ビットごとにAND演算を行ってデータを抽出することを言う。今回の場合で言うと、カテゴリマスクと衝突マスクの2進数表現をAND演算して0にならなければ衝突が発生することになる。
実際に計算してみよう。
茶鳥の衝突マスクの「1」を2進数にすると「0001」
赤鳥のカテゴリマスクの「2」を2進数にすると「0010」
この2進数に対してAND演算を行う。具体的には「0001」と「0010」を上下に並べて上と下の数字が両方とも1だったら1を書き、それ以外は0を書く。
0001
0010
======
0000
演算結果は「0000」。0の場合は衝突しない仕様なので茶鳥と赤鳥は衝突しない。
次に、茶鳥と青鳥を計算する。
茶鳥の衝突マスクの「1」を2進数にすると「0001」
赤鳥のカテゴリマスクの「3」を2進数にすると「0011」
0001
0011
======
0001
演算結果は「0001」。1以上の場合は衝突する仕様なので茶鳥と青鳥は衝突する。
理解を確認するために、もう1つ例題を解いておこう。以下に設定した場合、茶鳥はどの鳥に衝突するか。
1 2 3 4 5 6 7 8 9 10 |
//カテゴリのビットマスクを設定する。 birdBrown.physicsBody?.categoryBitMask = 4 birdRed.physicsBody?.categoryBitMask = 2 birdBlue.physicsBody?.categoryBitMask = 1 //衝突するカテゴリのビットマスクを設定する。 birdBrown.physicsBody?.collisionBitMask = 3 birdRed.physicsBody?.collisionBitMask = 2 birdBlue.physicsBody?.collisionBitMask = 3 |
まずは茶鳥と赤鳥から計算する。
茶鳥の衝突マスクの「3」を2進数にすると「0011」
赤鳥のカテゴリマスクの「2」を2進数にすると「0010」
0011
0010
======
0010
よって、茶鳥と赤鳥は衝突する。次に茶鳥と青鳥を計算する。
茶鳥の衝突マスクの「3」を2進数にすると「0011」
青鳥のカテゴリマスクの「1」を2進数にすると「0001」
0011
0001
======
0001
よって、茶鳥と青鳥も衝突する。
イメージとしては、カテゴリマスクと衝突マスクの2進数表現を0がOFF、1がONを表すスイッチとして下図のように横にズラーっと並べる。上下のスイッチが両方ともONのものが1つでもあればノードが衝突すると考えれば良い。
ちなみにカテゴリマスクや衝突マスクは32ビットなので32個のスイッチを並べることができる。組み合わせパターンは4294967296通りだ。わぉ
「カテゴリマスクや衝突マスクを確認したときに、どれとどれが衝突するのかパッとみ分からないのが辛いのでは。」と思ったかも知れない。
確かに、衝突マスクが3に設定されているのを見てカテゴリマスクが1のノードと衝突するとはすぐには判断できない。そんなときは以下のコードのように2進数で値を設定するのをお勧めする。
では、以下の設定で茶鳥はどの鳥と衝突するか。
1 2 3 4 5 6 7 8 9 10 |
//カテゴリのビットマスクを設定する。 birdBrown.physicsBody?.categoryBitMask = 0b0001 birdRed.physicsBody?.categoryBitMask = 0b0010 birdBlue.physicsBody?.categoryBitMask = 0b0100 //衝突するカテゴリのビットマスクを設定する。 birdBrown.physicsBody?.collisionBitMask = 0b0010 birdRed.physicsBody?.collisionBitMask = 0b0011 birdBlue.physicsBody?.collisionBitMask = 0b0001 |
茶鳥の衝突マスクは「0b0010」なので、カテゴリマスクの2桁目が1の鳥を探せばいい。赤鳥は「0b0010」、青鳥は「0b0100」なので、茶鳥は赤鳥と衝突する。
では、以下設定のカテゴリマスクは変更せずに、衝突マスクだけ変更して茶鳥を赤鳥と青鳥の両方と衝突させるにはどのようにすればいいか。
1 2 3 4 5 6 7 8 9 10 |
//カテゴリのビットマスクを設定する。 birdBrown.physicsBody?.categoryBitMask = 0b0001 birdRed.physicsBody?.categoryBitMask = 0b0010 birdBlue.physicsBody?.categoryBitMask = 0b0100 //衝突するカテゴリのビットマスクを設定する。 birdBrown.physicsBody?.collisionBitMask = 0b0010 birdRed.physicsBody?.collisionBitMask = 0b0011 birdBlue.physicsBody?.collisionBitMask = 0b0001 |
赤鳥のカテゴリマスクは「0010」(2桁目が1)、青鳥のカテゴリマスクは「0100」(3桁目が1)なので、茶鳥の衝突マスクの2桁目と3桁目を1に設定すれば赤鳥と青鳥の両方と衝突することになる。
1 2 3 |
//衝突するカテゴリのビットマスクを設定する。 birdBrown.physicsBody?.collisionBitMask = 0b0110 |
しかし、上記書き方だと衝突させるカテゴリが増えてきたときにややこしくなるので、以下のようにビット和を使ったほうが分かりやすい。
1 2 3 |
//衝突するカテゴリのビットマスクを設定する。 birdBrown.physicsBody?.collisionBitMask = 0b0010 | 0b0100 |
さらに突き詰めると、以下のコードのように衝突させたいノードのカテゴリマスクを参照して設定すると一目でどのノードと衝突するのかが分かり、後から修正がしやすくなる。
1 2 3 |
//衝突するカテゴリのビットマスクを設定する。 birdBrown.physicsBody?.collisionBitMask = birdRed.physicsBody!.categoryBitMask | birdBlue.physicsBody!.categoryBitMask |
以下は実際のプレイ動画