> 原文:http://www.jianshu.com/p/bcd17547b395
> 作者:[PMST](http://www.jianshu.com/users/596f2ba91ce9/latest_articles)
前文已經為各個精靈新增了*Physics Body*,設置了三個掩碼:
* categoryBitMask表明了分屬類別。
* collisionBitMask告知能與哪些物體碰撞。
* contactTestBitMask則告知能與哪些物體接觸。
現在遺留的問題是如何檢測碰撞?難道是在*update()*方法進行檢測:遍歷所有的節點,通過判斷節點的位置是否有交集嗎?天吶!這也太麻煩了。確實,如果通過自己實時檢測實在過于勞累,何不讓*Sprite Kit*來幫你代勞,每當物體之間發生碰撞了,立馬通知你來處理事件。*Bingo!!*?顯然這里要用**協議+代理**了,設置場景為代理,每當*Sprite Kit*檢測到碰撞事件發生,就通知*GameScene*來處理,當前哪里事情都是在協議(*Protocol*)中聲明了。
## 01.游戲狀態
在正式開始今天的碰撞檢測課程之前,談談如何劃分游戲各時的狀態,僅以*Flappy bird*游戲為例,簡單劃分如下:
* *MaiMenu*。開始一次游戲、查看排名以及游戲幫助。
* *Tutorial*。考慮到新手對于新游戲的上手,在選擇進行一次新游戲時,展示玩法教程顯然是一個明確且友好的措施。
* *Play*。正處于游戲的狀態。
* *Falling*。*Player*因為不小心碰到障礙物失敗下落時刻。**注意:接觸障礙物,失敗掉落才算!**
* *ShowingScore*。顯示得分。
* *GameeOver*。告知游戲結束。
為此請打開*Lecture05*的完成工程,打開*GameScene.swift*文件,新增游戲狀態的枚舉聲明到`enum Layer{}`下方:
~~~
enum GameState{
case MainMenu
case Tutorial
case Play
case Falling
case ShowingScore
case GameOver
}
~~~
當然,我們還需要聲明一個變量用于存儲游戲場景的狀態,請找到*GameScene*類中`let sombrero = SKSpriteNode(imageNamed: "Sombrero")`這條代碼,在下方新增三個新變量:
~~~
//1
var hitGround = false
//2
var hitObstacle = false
//3
var gameState: GameState = .Play
~~~
1. 標識符,記錄*Player*是否掉落至地面。
2. 標識符,記錄*Player*是否碰撞了仙人掌。
3. 游戲狀態,默認是正在玩。
## 02.碰撞檢測
正如前面提及的**協議+代理**方式檢測物體之間的碰撞情況。首先請使得類*GameScene*遵循`SKPhysicsContactDelegate`協議:
~~~
class GameScene: SKScene,SKPhysicsContactDelegate{...}
~~~
接著在*didMoveToView()*方法中設置代理為`self`,找到`physicsWorld.gravity = CGVector(dx: 0, dy: 0)`這行代碼,添加該行代碼`physicsWorld.contactDelegate = self`。
`SKPhysicsContactDelegate`協議中定義了兩個可選方法,分別是:
* `optional public func didBeginContact(contact: SKPhysicsContact)`
* `optional public func didEndContact(contact: SKPhysicsContact)`
分別用于反饋兩個物體開始接觸、結束接觸兩個時刻。本文采用第一個方法用戶處理物體接觸事件。
~~~
func didBeginContact(contact: SKPhysicsContact) {
let other = contact.bodyA.categoryBitMask == PhysicsCategory.Player ? contact.bodyB : contact.bodyA
if other.categoryBitMask == PhysicsCategory.Ground {
hitGround = true
}
if other.categoryBitMask == PhysicsCategory.Obstacle {
hitObstacle = true
}
}
~~~
`contact`包含了接觸的所有信息,其中*bodyA*和*bodyB*代表兩個碰撞的物體,顯然發生碰撞的結果只有兩種可能:1.*Player*和地面;2.*Player*和障礙物。可惜我們無法確實*bodyA*就是*Player*,亦或是*bodyB*就是它。這是有不確定性的,我們需要通過`categoryBitMask`來區分“陣營”。一旦確定哪個是*Player*之后,我們就能取到與之發生接觸的*other*,通過判斷其類別來分別置為標志位。
一旦標志位設置之后,我們需要在*update()*方法中進行處理了!
## 03.根據游戲狀態來處理事件
請定位到`update()`方法,修改其中的內容:
~~~
override func update(currentTime: CFTimeInterval) {
if lastUpdateTime > 0 {
dt = currentTime - lastUpdateTime
} else {
dt = 0
}
lastUpdateTime = currentTime
switch gameState {
case .MainMenu:
break
case .Tutorial:
break
case .Play:
updateForeground()
updatePlayer()
//1
checkHitObstacle() //Play狀態下檢測是否碰撞了障礙物
//2
checkHitGround() //Play狀態下檢測是否碰撞了地面
break
case .Falling:
updatePlayer()
//3
checkHitGround() //Falling狀態下檢測是否掉落至地面 此時已經失敗了
break
case .ShowingScore:
break
case .GameOver:
break
}
}
~~~
其中1,2,3中三個方法均是通過狀態標志位來處理碰撞事件,請添加`checkHitObstacle()`以及`checkHitGround()`方法到`updateForeground()`方法下方:
~~~
// 與障礙物發生碰撞
func checkHitObstacle() {
if hitObstacle {
hitObstacle = false
switchToFalling()
}
}
// 掉落至地面
func checkHitGround() {
if hitGround {
hitGround = false
playerVelocity = CGPoint.zero
player.zRotation = CGFloat(-90).degreesToRadians()
player.position = CGPoint(x: player.position.x, y: playableStart + player.size.width/2)
runAction(hitGroundAction)
switchToShowScore()
}
}
// MARK: - Game States
// 由Play狀態變為Falling狀態
func switchToFalling() {
gameState = .Falling
runAction(SKAction.sequence([
whackAction,
SKAction.waitForDuration(0.1),
fallingAction
]))
player.removeAllActions()
stopSpawning()
}
// 顯示分數狀態
func switchToShowScore() {
gameState = .ShowingScore
player.removeAllActions()
stopSpawning()
}
// 重新開始一次游戲
func switchToNewGame() {
runAction(popAction)
let newScene = GameScene(size: size)
let transition = SKTransition.fadeWithColor(SKColor.blackColor(), duration: 0.5)
view?.presentScene(newScene, transition: transition)
}
~~~
完成后自然你發現`stopSpawning()`方法并未實現,因為我打算好好講講這個。早前在`didMoveToView()`方法中調用`startSpawning()`源源不斷地產生障礙物,但是一旦游戲結束,我們所要做的事情有兩個:1.停止繼續產生障礙物;2.已經在場景中的障礙物停止移動。那么如何制定某個動作*Action*停止呢?答案是先為這個動作命名(簡單來說設置一個**Key**而已),然后用`removeActionForKey()`來移除。
OK,找到`startSpawning()`方法,將`runAction(overallSequence)`替換成`runAction(overallSequence, withKey: "spawn")`;定位到`spawnObstacle()`方法,分別設置*bottomObstacle*和*topObstacle*精靈的名字,方便之后找到它們并進行操作:
~~~
...
bottomObstacle.name = "BottomObstacle"
worldNode.addChild(bottomObstacle)
...
topObstacle.name = "TopObstacle"
worldNode.addChild(topObstacle)
...
~~~
現在來實現`stopSpawning()`方法,在`startSpawning()`下方添加就好:
~~~
func stopSpawning() {
removeActionForKey("spawn")
worldNode.enumerateChildNodesWithName("TopObstacle", usingBlock: { node, stop in
node.removeAllActions()
})
worldNode.enumerateChildNodesWithName("BottomObstacle", usingBlock: { node, stop in
node.removeAllActions()
})
}
~~~
點擊運行,我擦!還沒來得及點就掉地上了......好吧,只能在游戲進入一瞬間先讓*Player*向上蹦跶下。添加`flapPlayer()`到`didMoveToView()`方法的最下方。
點擊運行,Nice!!*Player*順利穿過了障礙,不小心碰到了障礙物,再點擊,等等!怎么還能動...好吧,看來`touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?)`點擊事件中我們并未根據游戲狀態來處理,是時候修改了。
~~~
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
switch gameState {
case .MainMenu:
break
case .Tutorial:
break
case .Play:
flapPlayer()
break
case .Falling:
break
case .ShowingScore:
switchToNewGame()
break
case .GameOver:
break
}
}
~~~
點擊運行,失敗重新開始游戲...等等貌似還有問題,怎么點擊想重新開始游戲會突然掉落到地面上...好吧,請看[lecture02](http://www.jianshu.com/p/82697ebf5cad)中的時間間隔圖,匆忙的你找找原因,試試解決吧。