前言
在上一篇文章中,我们已经基本实现了「方块弹珠」小游戏的核心逻辑。在这篇文章中,我们主要关注在调整游戏 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…