前文已经为各个精灵新增了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
- 标识符,记录Player是否掉落至地面。
- 标识符,记录Player是否碰撞了仙人掌。
- 游戏状态,默认是正在玩。
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.根据游戏状态来处理事件
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",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中的时间间隔图,匆忙的你找找原因,试试解决吧。