【Swift】SpriteKitの使い方。ノードとノードの接触を検知する。(Swift 2.2、XCode 7.3)
ノードの接触を検知する
本記事ではノードの接触検知について説明する。
ビームで障害物を破壊したり、キャラクターを動かしてアイテムを取ったりなど、ノードが別のノードに接触したことを検知して処理したいことがある。そんなときはSKPhysicsContactDelegateを利用する。
実際にやってみよう。
以降の手順を行う前のXcodeプロジェクトをGitHubに置いたので、試してみる方はご利用下さい。
⇒「テスト用プロジェクト」
3匹の鳥ノードを配置し、茶鳥をドラッグで移動できるようにしておいた。この段階では接触は検知されない。
まず、TestScene.swiftを以下のコードに変更する(変更箇所は青色網掛け)。SKPhysicsContactDelegateプロトコルを適用、デリゲート先を自分に設定している。
茶鳥ノードのcontactTestBitMaskプロパティ(以下、接触マスク)に赤鳥のカテゴリマスクを設定している。この設定は、茶鳥の物理ボディが赤鳥の物理ボディと触れたときにデリゲートメソッドが呼ばれることを意味している。
カテゴリマスクについては次の記事を参照されたし。⇒「カテゴリマスクとは」
なんで接触マスクの名前がcontactBitMaskではなくcontactTestBitMaskなのだろうか。。
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 77 78 |
// // TestScene.swift // import Foundation import SpriteKit class TestScene:SKScene, SKPhysicsContactDelegate { var birdBrown:SKSpriteNode! var testLabel:SKLabelNode! //現在シーン設定時の呼び出しメソッド override func didMoveToView(view: SKView) { //背景画像のノードを作成する。 let backNode = SKSpriteNode(imageNamed: "night") backNode.size = self.frame.size backNode.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame)); //ラベルノードを作成する。 testLabel = SKLabelNode(text: "テスト開始") testLabel.position = CGPointMake(CGRectGetMidX(self.frame), self.frame.height-50) //鳥のノードを作成する。 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 = 0b0001 birdRed.physicsBody?.categoryBitMask = 0b0010 birdBlue.physicsBody?.categoryBitMask = 0b0100 //赤鳥との接触を検知するように設定する。 birdBrown.physicsBody?.contactTestBitMask = birdRed.physicsBody!.categoryBitMask //重力の影響を受けないようにする。 birdBrown.physicsBody!.affectedByGravity = false birdRed.physicsBody!.affectedByGravity = false birdBlue.physicsBody!.affectedByGravity = false //ノードをシーンに追加する。 self.addChild(backNode) self.addChild(testLabel) self.addChild(birdBrown) self.addChild(birdRed) self.addChild(birdBlue) //デリゲート先を自分に設定する。 self.physicsWorld.contactDelegate = self } //画面タッチ時の呼び出しメソッド override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) { //画面をタッチした座標に移動するアクションを作成する。 let location = touches.first!.locationInNode(self) let action = SKAction.moveTo(CGPoint(x:location.x, y:location.y), duration:0.1) //アクションを実行する。 birdBrown.runAction(action) } } |
SKPhysicsContactDelegateのデリゲートメソッドは接触開始時と終了時に呼び出される2つのメソッドしかない。両方とも試してみよう。
TestScene.swiftに以下のメソッドを追加する。接触開始時と終了時にラベルの文字列を変更している。
1 2 3 4 5 6 7 8 9 10 11 12 |
//接触開始時の呼び出しメソッド func didBeginContact(contact: SKPhysicsContact) { testLabel.text = "接触開始" print("接続開始") } //接触終了時の呼び出しメソッド func didEndContact(contact: SKPhysicsContact) { testLabel.text = "接触終了" print("接続終了") } |
以下は実際のプレイ動画。
茶鳥で赤鳥を運んでいる最中に「接触開始」と「接触終了」が連続で呼び出された。また、開始と終了が必ず交互に呼び出されるのではなく、「接続開始、接続開始」のように同じものが連続で呼び出されることもある。
衝突させずに検知する
衝突しないノード同士でも物理ボディが重なることで接触を検知できる。やってみよう。
didMoveToViewメソッドに以下のコードを追加する。茶鳥と赤鳥の衝突マスクに青鳥のカテゴリマスクを設定し、茶鳥と赤鳥は衝突しないようにした。
1 2 3 4 |
//青鳥のみと衝突するように設定 birdBrown.physicsBody?.collisionBitMask = birdBlue.physicsBody!.categoryBitMask birdRed.physicsBody?.collisionBitMask = birdBlue.physicsBody!.categoryBitMask |
以下は実際のプレイ動画。茶鳥と赤鳥が重なったときも「接触開始」、「接触終了」が呼び出されている。しかし、重なっている状態でも開始、終了が連続で呼び出されるのが気になる。
重なり開始、終了時の1回ずつ通知されるようにする
「上記を読むと、このデリゲートメソッドではノードがどこかの領域の中に入った、抜けたの検知はできないということなのだろうか。。」
と思ってしまうが、物理ボディの形状を四角形や円形にすれば、重なり始めたときに「接続開始」、重なりが終わったときに「接続終了」が呼び出されるようになる。
やってみよう。didMoveToViewメソッドの物理ボディを設定する箇所を以下のコードに変更する。
1 2 3 4 5 6 7 8 9 |
//物理ボディを設定する。 //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 = SKPhysicsBody(rectangleOfSize: CGSize(width: 100.0, height: 50.0)) birdRed.physicsBody = SKPhysicsBody(rectangleOfSize: CGSize(width: 100.0, height: 50.0)) birdBlue.physicsBody = SKPhysicsBody(rectangleOfSize: CGSize(width: 100.0, height: 50.0)) |
以下は実際のプレイ動画。デリゲートメソッドが呼び出されるタイミングが、重なり開始時と終了時の2回だけになった。
ただし、物理ボディに四角形や円形を使った場合はノードの接触位置がリアルではなくなる。
注意点
物理ボディのdynamicプロパティをfalseにするとデリゲートメソッドが呼び出されなくなるので注意すること。
stackoverflow参照⇒「SKPhysicsContactDelegate Not Working」
1 2 3 4 5 |
//他のノードに衝突されても動かなくする。 birdBrown.physicsBody!.dynamic = false birdRed.physicsBody!.dynamic = false birdBlue.physicsBody!.dynamic = false |
接触したノードを調べる
デリゲートメソッドの引数のPhysicsContactインスタンスには、接触した2つの物理ボディがbodyA、bodyBのプロパティで渡ってくる。
そこで本記事の最後は、デリゲートメソッドが呼び出されたら接触したノードを調べ、赤鳥なら10点、青鳥なら20点が入り、鳥が大きくなる単純なゲームを作った。
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 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
// // TestScene.swift // import Foundation import SpriteKit class TestScene:SKScene, SKPhysicsContactDelegate { var birdBrown:SKSpriteNode! var testLabel:SKLabelNode! let brownMask:UInt32 = 0b0001 let redMask:UInt32! = 0b0010 let blueMask:UInt32! = 0b0100 var point = 0 //現在シーン設定時の呼び出しメソッド override func didMoveToView(view: SKView) { //背景画像のノードを作成する。 let backNode = SKSpriteNode(imageNamed: "night") backNode.size = self.frame.size backNode.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame)); //ラベルノードを作成する。 testLabel = SKLabelNode(text: "テスト開始") testLabel.position = CGPointMake(CGRectGetMidX(self.frame), self.frame.height-50) //鳥のノードを作成する。 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)-60, self.frame.height-400) birdBlue.position = CGPointMake(CGRectGetMidX(self.frame)+60, self.frame.height-500) //物理ボディを設定する。 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 = brownMask birdRed.physicsBody?.categoryBitMask = redMask birdBlue.physicsBody?.categoryBitMask = blueMask //赤鳥と青鳥の接触を検知するように設定する。 birdBrown.physicsBody?.contactTestBitMask = redMask | blueMask //他のノードに衝突されても動かなくする。 birdRed.physicsBody!.dynamic = false birdBlue.physicsBody!.dynamic = false //重力の影響を受けないようにする。 birdBrown.physicsBody!.affectedByGravity = false birdRed.physicsBody!.affectedByGravity = false birdBlue.physicsBody!.affectedByGravity = false //ノードをシーンに追加する。 self.addChild(backNode) self.addChild(testLabel) self.addChild(birdBrown) self.addChild(birdRed) self.addChild(birdBlue) //デリゲート先を自分に設定する。 self.physicsWorld.contactDelegate = self } //画面タッチ時の呼び出しメソッド override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) { //画面をタッチした座標に移動するアクションを作成する。 let location = touches.first!.locationInNode(self) let action = SKAction.moveToX(location.x, duration: 0.1) //アクションを実行する。 birdBrown.runAction(action) } //画面タッチ終了時の呼び出しメソッド override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) { //茶鳥を重力の影響を受けるようにする。 birdBrown.physicsBody!.affectedByGravity = true } //接触開始時の呼び出しメソッド func didBeginContact(contact: SKPhysicsContact) { if(contact.bodyA.categoryBitMask == redMask || contact.bodyB.categoryBitMask == redMask) { //得点を加算する。 point += 10 } else { //得点を加算する。 point += 20 } //ノードを拡大する let action = SKAction.scaleBy(1.05, duration: 0.5) contact.bodyA.node!.runAction(action) contact.bodyB.node!.runAction(action) //スコアを表示する。 testLabel.text = String("\(point)点") } } |
以下は実際のプレイ動画