Lecture06. 碰撞的检测
最后更新于:2022-04-01 23:36:47
> 原文: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, withEvent event: UIEvent?)`点击事件中我们并未根据游戏状态来处理,是时候修改了。
~~~
override func touchesBegan(touches: Set, 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)中的时间间隔图,匆忙的你找找原因,试试解决吧。
';