前言
在上一篇文章中,我们已经完成了使用 SpriteKit 实践一些小的 demo 实例,对 SpriteKit 有了一个大体上的实践体验,在这篇文章中,我们主要关注在刚体和刚体之间,也就是小球和方块之间的碰撞交互,整个游戏的核心也就在这。
碰撞检测
在 SpriteKit 中进行刚体和刚体之间的碰撞检测,需要对这两个刚体所处的 SKScene 设置对 SKPhysicsContactDelegate 协议的遵守,这样我们的 GameScene 就可以接收到在其之中的各个刚体之间发生碰撞的「通知」。
class GameScene: SKScene {
override init(size: CGSize) {
super.init(size: size)
physicsWorld.contactDelegate = self
}
// ...
}
extension GameScene: SKPhysicsContactDelegate {
func didBegin(_ contact: SKPhysicsContact) {
}
func didEnd(_ contact: SKPhysicsContact) {
}
}
想要让两个刚体之间发生碰撞,这部分内容我们已经在上一篇文章中学习过了,现在我们需要进行的是如何让两个刚体之间发生碰撞后,我们的 GameScene 能够接收到的碰撞通知,定一个结构体。
struct BitMask {
static let Ball = UInt32(0x00001)
static let Box = UInt32(0x00002)
static let Ground = UInt32(0x00003)
}
在这个结构体 BitMask 中定义了三个两种常量值,这些常量值被称为「碰撞检测掩码」,通过使用 physicsBody 的 contactTestBitMask 定义物体的类型。默认情况下,如果我们不给刚体设置 contactTestBitMask,该值为 0,也就是说这种刚体不属于任何碰撞检测的类型。
在 SKPhysicsContactDelegate 协议方法中的参数 SKPhysicsContact 对象参数包含了此次碰撞的相关信息,如碰撞点和力的大小。
open class SKPhysicsContact : NSObject {
open var bodyA: SKPhysicsBody { get }
open var bodyB: SKPhysicsBody { get }
open var contactPoint: CGPoint { get }
open var contactNormal: CGVector { get }
open var collisionImpulse: CGFloat { get }
}
有了「碰撞检测掩码」,我们就可以在碰撞检测回调中判断当前到底是什么刚体和什么刚体发生了碰撞。
extension GameScene: SKPhysicsContactDelegate {
func didBegin(_ contact: SKPhysicsContact) {
}
func didEnd(_ contact: SKPhysicsContact) {
print(contact.bodyA.contactTestBitMask)
print(contact.bodyB.contactTestBitMask)
}
}
bitMaks
physicsBody 中含有三个属性值,
categoryBitMask,定义刚体所属的类别,供碰撞检测进行区分,默认属于所有类型。collisionBitMask,定义刚体可与哪种类别的其它刚体发生碰撞,默认可与所有类型发生碰撞。contactTestBitMask,定义刚体可与哪种类别的其它刚体发生接触,默认不与所有类型发生接触,供代理实现中调用。
这三个属性值分别控制了 SpriteKit 中物体和物体之间的碰撞关系。在我们的这个游戏中,小球和小球之间是不能发生碰撞的,而小球和方块之间是可以发生碰撞的,而我们需要在 SpriteKit 的接触代理方法中判断出当前发生碰撞的两个对象物体分别是什么。
class GameScene: SKScene {
// ...
private func createContent() {
for row in 0..<10 {
let ball = Ball(circleOfRadius: 10)
ball.fillColor = .red
addChild(ball)
ball.physicsBody = SKPhysicsBody(circleOfRadius: 10)
// 设置初速度
ball.physicsBody?.velocity = CGVector(dx: 300 + CGFloat(row) * 0.1, dy: 300)
ball.position = CGPoint(x: size.width / 2, y: 400)
ball.physicsBody?.categoryBitMask = BitMask.Ball
ball.physicsBody?.contactTestBitMask = BitMask.Box
ball.physicsBody?.collisionBitMask = BitMask.Box
ball.physicsBody?.linearDamping = 0
ball.physicsBody?.restitution = 1
}
let box = Box(rectOf: CGSize(width: 50, height: 50))
box.position = CGPoint(x: 300, y: 800)
box.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: 50, height: 50))
box.physicsBody?.categoryBitMask = BitMask.Box
box.physicsBody?.collisionBitMask = BitMask.Box
box.physicsBody?.contactTestBitMask = BitMask.Ball
box.fillColor = .blue
// 静态物体
box.physicsBody?.isDynamic = false
box.physicsBody?.restitution = 1
addChild(box)
}
// ...
}
此时,运行工程,我们的小球已经可以和方块发生碰撞了。更进一步,我们需要当小球和方块进行接触时,把方块从视图中移除。让 GameScene 遵守 SKPhysicsContactDelegate。
extension GameScene: SKPhysicsContactDelegate {
func didBegin(_ contact: SKPhysicsContact) {
switch contact.bodyA.categoryBitMask {
case BitMask.Box:
checkNodeIsBox(contact.bodyA.node)
default:
break
}
switch contact.bodyB.categoryBitMask {
case BitMask.Box:
checkNodeIsBox(contact.bodyB.node)
default:
break
}
}
}
extension GameScene {
private func checkNodeIsBox(_ node: SKNode?) {
guard let box = node else { return }
if box.physicsBody?.categoryBitMask == BitMask.Box {
box.removeFromParent()
}
}
}
方块递减
当小球撞上方块时,我们要给方块设置一个关卡数。比如当方块上关卡数为 8 时,需要小球撞击方块 8 次才能将该方块进行消除。给方块多增加一个子节点 lableNode,用于记录当前方块剩余被撞击数。
class GameScene: SKScene {
// ...
private func createContent() {
// ...
for row in 1...5 {
let box = Box(rectOf: CGSize(width: 50, height: 50))
box.position = CGPoint(x: 50 + (row * 50 + 20), y: (800 - row * 50 + 20))
box.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: 50, height: 50))
box.physicsBody?.categoryBitMask = BitMask.Box
box.physicsBody?.contactTestBitMask = BitMask.Ball
box.physicsBody?.collisionBitMask = BitMask.Box
box.physicsBody?.linearDamping = 0
box.physicsBody?.restitution = 1.0
box.physicsBody?.isDynamic = false
box.fillColor = .red
let label = Label(text: "\(row)")
label.fontSize = 22
label.typoTag = 666
label.fontName = "Arial-BoldMT"
label.color = .white
label.position = CGPoint(x: 0, y: -label.frame.size.height / 2)
box.addChild(label)
addChild(box)
}
}
}
在 GameScene 的代理回调方法中,完善 checkNodeIsBox,使其支持对方块剩余撞击数的检测。
extension GameScene {
private func checkNodeIsBox(_ node: SKNode?) {
guard let box = node else { return }
if box.physicsBody?.categoryBitMask == BitMask.Box {
let label = box.children.first! as! Label
var tag = Int(label.text!)!
if (tag > 1) {
tag -= 1
label.text = "\(tag)"
} else {
box.removeFromParent()
}
}
}
}
此时,运行工程,我们发现小球已经可以和方块进行递减的碰撞检测了!!!
总结
在这篇文章中,我们继续上篇文章中未完成的小球与方块的碰撞检测,通过对 SKNode 中三个 bitMask 属性的理解和运用,保证了 GameScene 中小球和小球之间不发生碰撞,小球和方块、小球和墙体以及小球和地面发生碰撞,并且方块已经具备了递减消失的能力,游戏的核心检测逻辑都已经完成。在下一篇文章中我们将完成小球的发射和回收逻辑。
我们现在完成的内容有:
- 游戏讲解;
- 熟悉 2D 编程;
- 刚体碰撞与检测;
- 小球的发射与方块的消除;
- 游戏逻辑完善。
GitHub 地址: github.com/windstormey…