Swift 游戏开发之「方块弹珠」(四)

1,007 阅读4分钟

前言

在上一篇文章中,我们已经基本实现了「方块弹珠」小游戏的核心逻辑。在这篇文章中,我们主要关注在调整游戏 UI,让游戏具备基本可玩的阶段。

控制小球发射

之前我们只是通过对小球的 physicsBody 通过调用 applyForce 设置一个初始力,让小球在这个「初始力」的作用下进行运动,我们已经写死了对小球发射方向的设置,现在,我们要对其改造成根据用户手指在屏幕上滑动的方向进行运动。我们先对小球进行初始化归位,使其当用户触摸事件结束后再进行发射。


class GameScene: SKScene {
    
    private var balls = [Ball]()
    
    // ...
    
    private func createContent() {
        // ...
        
        for _ in 0..<5 {
            // ...
            balls.append(ball)
            ball.position = CGPoint(x: size.width / 2, y: ground.frame.size.height + ball.frame.size.height / 2)
            // ...
        }
    // ...
}

extension GameScene {
    private func shot() {
        for (index, ball) in balls.enumerated() {
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.1 * Double(index)) {
                ball.physicsBody?.applyForce(CGVector(dx: 400 + CGFloat(index) * 0.1, dy: 800))
            }
        }
    }
}

extension GameScene {
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        shot()
    }
}

此时,运行工程,小球将出现在地面的中心位置上,并且只有当用户的触摸事件结束后才会进行「发射」。需要注意的是,我们对每一个小球都加上了时延,第二个小球发射出去的前提是第一个小球已经发射出去了,第三个小球发射出去的前提是第二个小球已经发射出去了,以此类推,我们只需要在之前统一发射的 for 循环中增加一个时延方法来控制每一个小球发射出去的时机即可。

接着,我们来完成当小球和地面进行接触时,把撞击地面的小球都进行归位。不过这里需要注意的是,SpriteKit 为了提高渲染速度,推荐我们把一段时间内不需要进行绘制的节点进行移除,注意是移除不是删除,如果我们不这么做,那么这些已经被「隐藏」起来的节点同样会被纳入 SpriteKit 的物理计算中,从而拖慢我们的游戏系统的响应速度。

因此,我们重新修改下底部地面的与碰撞相关的三个枚举值。

struct BitMask {
    // ...
    static let Ground = UInt32(0x00004)
}


// ...

private func createContent() {
    // ...
    ground.physicsBody?.collisionBitMask = BitMask.Ball
    ground.physicsBody?.categoryBitMask = BitMask.Ground
    ground.physicsBody?.contactTestBitMask = BitMask.Ball
}

// ...

并新增小球对地面的碰撞检测方法。

extension GameScene: SKPhysicsContactDelegate {
    func didBegin(_ contact: SKPhysicsContact) {
        
        switch contact.bodyA.categoryBitMask {
        // ...
        case BitMask.Ground:
            checkNodeIsGround(contact.bodyB.node)
        default:
            break
        }
        
        switch contact.bodyB.categoryBitMask {
        // ...
        case BitMask.Ground:
            checkNodeIsGround(contact.bodyA.node)
        default:
            break
        }
    }
}

extension GameScene {
    // ...
    private func checkNodeIsGround(_ node: SKNode?) {
        guard let ball = node as? Ball else { return }
        
        if (ball.physicsBody?.categoryBitMask == BitMask.Ball) {
            ball.removeFromParent();
        }
    }
}

此时运行工程,发现当所有小球到了底部后都被自动从当前 Scene 中移除了,为了提高可玩度,我们给第一个下落的小球加上标记,告诉用户这是你第一个触底小球的位置,下一次再发射小球时,将会从这个位置重新出发。

我们需要一个定位小球变量,用于标记第一个小球到底地面的位置。

private func checkNodeIsGround(_ node: SKNode?) {
    guard let ball = node as? Ball else { return }
    
    // NOTE: 小球 & 发射出去
    if (ball.physicsBody?.categoryBitMask == BitMask.Ball && ball.isShot) {
        ball.removeFromParent();
        
        
        if (firstDownBall == nil || !children.contains(firstDownBall!)) {
            firstDownBall = Ball(circleOfRadius: 10)
            firstDownBall!.position = CGPoint(x: ball.position.x, y: ground.frame.size.height + ball.frame.size.height / 2 - 2)
            addChild(firstDownBall!)
            firstDownBall!.physicsBody?.isDynamic = false
        }
            
        ball.position = CGPoint(x: firstDownBall!.position.x, y: ground.frame.size.height + ball.frame.size.height / 2)
    }
}

这里需要注意的是,当定位小球已经创建出来时,我们需要把后续触底的小球二次发射的初始位置均设置为一致值,否则二次发射时,各个小球都会从当前触底位置直接发射,并不理会定位小球所定位的位置。

其中,为了缩减每次创建 Ball 的代码量,可以重写 init 方法。

class Ball: SKShapeNode {
    var isShot = false
    
    
    override init() {
        super.init()
        
        initObject()
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func initObject() {
        self.fillColor = .red
        self.physicsBody = SKPhysicsBody(circleOfRadius: 10)
        self.physicsBody?.categoryBitMask = BitMask.Ball
        self.physicsBody?.contactTestBitMask = BitMask.Box | BitMask.Ground
        self.physicsBody?.collisionBitMask = BitMask.Box
        self.physicsBody?.usesPreciseCollisionDetection = true;
        self.physicsBody?.linearDamping = 0
        self.physicsBody?.restitution = 1.0
    }
}

总结

至此!我们已经完成了「方块弹珠」的全部核心逻辑。当然这是核心逻辑,换句话说,你也可以认为这就是通常所说的 demo,可以拿去融资的 demo,当然,我们的这个小游戏是肯定不行的了,只是按照同等实现的功能映射到其他的产品层面。

所以,从下篇文章开始,我们将结合该系列的第一篇文章里说阐述的游戏背景,去完善并拓展我们的「方块弹珠」。

我们现在完成的内容有:

  • 游戏讲解;
  • 熟悉 2D 编程;
  • 刚体碰撞与检测;
  • 小球的发射与方块的消除;
  • 游戏逻辑完善。

GitHub 地址: github.com/windstormey…