用Swift做个游戏Lecture06 —— 碰撞的检测

系列:用Swift作个游戏
作者:pmst(1345614869)
微博:PPPPPPMST

前文已经为各个精灵新增了Physics Body,设置了三个掩码:

  • categoryBitMask表明了分属类别。
  • collisionBitMask告知能与哪些物体碰撞。
  • contactTestBitMask则告知能与哪些物体接触。

现在遗留的问题是如何检测碰撞?难道是在update()方法进行检测:遍历所有的节点,通过判断节点的位置是否有交集吗?天呐!这也太麻烦了。确实,如果通过自己实时检测实在过于劳累,何不让Sprite Kit来帮你代劳,每当物体之间发生碰撞了,立马通知你来处理事件。Bingo!! 显然这里要用协议+代理了,设置场景为代理,每当Sprite Kit检测到碰撞事件发生,就通知GameScene来处理,当前哪里事情都是在协议(Protocol)中声明了。

01.游戏状态

在正式开始今天的碰撞检测课程之前,谈谈如何划分游戏各时的状态,仅以Flappy bird游戏为例,简单划分如下:

  • MaiMenu。开始一次游戏、查看排名以及游戏帮助。
  • Tutorial。考虑到新手对于新游戏的上手,在选择进行一次新游戏时,展示玩法教程显然是一个明确且友好的措施。
  • Play。正处于游戏的状态。
  • FallingPlayer因为不小心碰到障碍物失败下落时刻。注意:接触障碍物,失败掉落才算!
  • 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包含了接触的所有信息,其中bodyAbodyB代表两个碰撞的物体,显然发生碰撞的结果只有两种可能: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()方法,分别设置bottomObstacletopObstacle精灵的名字,方便之后找到它们并进行操作:

...
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中的时间间隔图,匆忙的你找找原因,试试解决吧。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容