美文网首页SwiftSwift开发技巧iOS开发技巧
用Swift做个游戏Lecture06 —— 碰撞的检测

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

作者: NinthDay | 来源:发表于2015-10-11 19:20 被阅读914次

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

    相关文章

      网友评论

        本文标题:用Swift做个游戏Lecture06 —— 碰撞的检测

        本文链接:https://www.haomeiwen.com/subject/ldfocttx.html