上一篇我们迅速构建了一个生成金币的原型,现在我们需要进一步完善我们的功能。
首先,我们需要让小飞机碰到金币不会爆炸,稍后我们让“金币”更像一个金币 ;)
现在飞机的物理对象设置如下:
player.physicsBody = SKPhysicsBody(texture: playerTexture, size: playerTexture.size())
player.physicsBody!.contactTestBitMask = player.physicsBody!.collisionBitMask
player.physicsBody?.isDynamic = false
player.physicsBody!.collisionBitMask = 0
这意味着飞机与其他任何物体接触而不碰撞!所以金币自然也无一幸免,我们首先要修改的是didBegin(_ contact: SKPhysicsContact)方法,将其末尾的爆炸代码修改如下:
if contact.bodyA.node?.name == coinName || contact.bodyB.node?.name == coinName{
if contact.bodyA.node == player{
contact.bodyB.node?.removeFromParent()
}else{
contact.bodyA.node?.removeFromParent()
}
let sound = SKAction.playSoundFileNamed("coin.wav", waitForCompletion: false)
run(sound)
score += 10
return
}
if contact.bodyA.node == player || contact.bodyB.node == player{
if contact.bodyA.node == player && contact.bodyB.node?.parent == nil{
print("removed object...")
return
}else if contact.bodyB.node == player && contact.bodyA.node?.parent == nil{
print("removed object...")
return
}
if let explosion = SKEmitterNode(fileNamed: "PlayerExplosion"){
explosion.position = player.position
addChild(explosion)
}
let sound = SKAction.playSoundFileNamed("explosion.wav", waitForCompletion: false)
run(sound)
gameOver.alpha = 1
gameState = .dead
bgMusic.run(SKAction.stop())
player.removeFromParent()
speed = 0
}
代码很好理解,不过值得注意的是我在中间位置判断了node的父对象是否为nil,如果是则直接退出方法,这样做是有意而为之的!因为我们开启了SpriteKit物理对象的精确边界,这会导致短时间内多次进入didBegin(_ contact: SKPhysicsContact)方法,如果第一次在碰到金币将其删除后,第二次(在金币还未删除时就已经入栈缓存)就会直接进入后面的爆炸代码,从而出现少数玩家碰到金币仍然会爆炸的怪异情形。
而如果node从父对象中删除则其parent属性立即会被设置为nil,这就是我们在后面判断的原因。
好了,现在玩家可以“吃”金币了,可以增加分数了,现在我们需要美化一下金币了,玩家对“吃”长方体状物可没什么兴趣哦 ;)
从google找到一个金币动画的gif文件,注意背景一定要是透明的:
看起来不错,不过尺寸有点大,首先用动画编辑工具将其缩小到合理大小。
因为无法直接将gif动画在SpriteKit中播放,所以我们必须将gif中的一系列静态图片抽取出来然后形成一个动画帧。
以下是将gif文件每张静态图片转换为一个纹理数组的helper方法,如果失败则返回nil:
func load(imagePath:String)->[SKTexture]?{
guard let imageSource = CGImageSourceCreateWithURL(URL(fileURLWithPath: imagePath) as CFURL, nil) else {
return nil
}
let count = CGImageSourceGetCount(imageSource)
var images:[CGImage] = []
for i in 0..<count{
guard let img = CGImageSourceCreateImageAtIndex(imageSource, i, nil) else {continue}
images.append(img)
}
return images.map {SKTexture(cgImage:$0)}
}
不过还有个问题,我如何知道原来gif每帧播放的间隔时间是多少?以下代码可以为你解忧:
extension CGImageSource { // this was originally from another SO post for which I've lost the link. Apologies.
func delayFor(imageAt index: Int) -> TimeInterval {
var delay = 0.1
// Get dictionaries
let cfProperties = CGImageSourceCopyPropertiesAtIndex(self, index, nil)
let gifPropertiesPointer = UnsafeMutablePointer<UnsafeRawPointer?>.allocate(capacity: 0)
if CFDictionaryGetValueIfPresent(cfProperties, Unmanaged.passUnretained(kCGImagePropertyGIFDictionary).toOpaque(), gifPropertiesPointer) == false {
return delay
}
let gifProperties: CFDictionary = unsafeBitCast(gifPropertiesPointer.pointee, to: CFDictionary.self)
// Get delay time
var delayObject: AnyObject = unsafeBitCast(
CFDictionaryGetValue(gifProperties,
Unmanaged.passUnretained(kCGImagePropertyGIFUnclampedDelayTime).toOpaque()),
to: AnyObject.self)
if delayObject.doubleValue == 0 {
delayObject = unsafeBitCast(CFDictionaryGetValue(gifProperties,
Unmanaged.passUnretained(kCGImagePropertyGIFDelayTime).toOpaque()), to: AnyObject.self)
}
delay = delayObject as? TimeInterval ?? 0.1
if delay < 0.1 {
delay = 0.1 // Make sure they're not too fast
}
return delay
}
}
它的原理是用底层API接口直接读取gif文件中关于每帧间隔时间的属性值。不过我们这里就请允许本猫随便估计一个大概值:0.05s is perfect!!!
在GameScene初始化代码中加入以下一句:
coinTextures = load(imagePath: Bundle.main.path(forResource: "coin_s", ofType: "gif")!)!
将createCoins方法修改如下:
if let speed = rockSpeed,let yPosTop = rockYPosTop,let yPosBottom = rockYPosBottom{
//用金币纹理生成金币
let coinTop = SKSpriteNode(texture: coinTextures[0])
let xPos = frame.width + coinTop.frame.width
//用纹理精确边界确定物理边界
coinTop.physicsBody = SKPhysicsBody(texture: coinTop.texture!, size: coinTop.size)
coinTop.physicsBody?.isDynamic = false
coinTop.position = CGPoint(x: xPos, y: yPosTop)
coinTop.name = coinName
addChild(coinTop)
let coinBottom = SKSpriteNode(texture: coinTextures[0])
coinBottom.physicsBody = SKPhysicsBody(texture: coinBottom.texture!, size: coinBottom.size)
coinBottom.physicsBody?.isDynamic = false
coinBottom.position = CGPoint(x: xPos, y: yPosBottom)
coinBottom.name = coinName
addChild(coinBottom)
//增加金币动画效果
let action = SKAction.repeatForever(SKAction.animate(with: coinTextures, timePerFrame: 0.05))
coinTop.run(action)
coinBottom.run(action)
let endPos = frame.width + (coinTop.frame.width * 2)
let duration = (endPos - -endPos) / speed
let moveAction = SKAction.moveBy(x: -endPos, y: 0, duration: TimeInterval(duration))
let moveSeq = SKAction.sequence([moveAction,SKAction.removeFromParent()])
coinTop.run(moveSeq)
coinBottom.run(moveSeq)
}
}
为了更加符合实际,我在rock的上下两侧而不是中间生成金币,因为那样难度太低,玩家穿越个山顺便吃个金币的情况从此不复存在了 ;)玩家为了吃到金币必须抱着可能会机毁人亡的风险哦。
运行游戏,效果棒棒哒:
好了,这就是最后的效果,我们实现了从最初的0开始,一步一步重构为一个个旋转的金币,我很享受这个过程,希望你们也玩得愉快! ;)