Quantcast
Channel: CSDN博客移动开发推荐文章
Viewing all articles
Browse latest Browse all 5930

SpriteKit游戏如何一步一步重构在场景中增加金币动画(二)

$
0
0

上一篇我们迅速构建了一个生成金币的原型,现在我们需要进一步完善我们的功能。

首先,我们需要让小飞机碰到金币不会爆炸,稍后我们让“金币”更像一个金币 ;)

现在飞机的物理对象设置如下:

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开始,一步一步重构为一个个旋转的金币,我很享受这个过程,希望你们也玩得愉快! ;)

作者:mydo 发表于2017/5/18 8:13:18 原文链接
阅读:317 评论:0 查看评论

Viewing all articles
Browse latest Browse all 5930

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>