当前位置: > > > Swift - 跑酷游戏开发(SpriteKit游戏开发)

Swift - 跑酷游戏开发(SpriteKit游戏开发)

一,下面演示了如何开发一个跑酷游戏,实现的功能如下:
1,平台工厂会不断地生成平台,并且向左移动。当平台移出游戏场景时就可将其移除。
2,生成的平台宽度随机,高度随机。同时短平台踩踏的时候会下落。
3,奔跑小人设置了三种状态:奔跑,跳跃,打滚。
4,跳跃时可以再进行二段跳。
5,如果在一定高度落下,会先打滚再变成奔跑状态。同时平台会有震动效果。
6,起跳时会有特效(身后播放尘土飞扬特效)
7,跳跃,碰撞等都使用了苹果的物理引擎

二,效果图如下:
     

三,实现代码

1,奔跑小人类 - Runner.swift
注意:由于跳和打滚的动作不像跑的动作需要循环播放,所以就不需要用repeatActionForever。打滚动作结束后就执行跑步操作。而跳跃动作由于不知道什么时候落地,所以会由外部控制它的动作转变。
import SpriteKit

enum Status:Int{
    case run=1,jump,jump2,roll;
}

class Runner : SKSpriteNode {
    //跑的纹理集
    let runAtlas = SKTextureAtlas(named: "run.atlas")
    //跑的纹理数组
    var runFrames = [SKTexture]()
    //跳的纹理集
    let jumpAtlas = SKTextureAtlas(named: "jump.atlas")
    //存储跳的文理的数组
    var jumpFrames = [SKTexture]();
    //打滚的文理集合
    let rollAtlas = SKTextureAtlas(named: "roll.atlas")
    //存储打滚文理的数组
    var rollFrames = [SKTexture]();
    
    var status = Status.run
    
    //起跳 y坐标
    var jumpStart:CGFloat = 0.0
    //落地 y坐标
    var jumpEnd :CGFloat = 0.0
    
    //起跳特效纹理集
    let jumpEffectAtlas = SKTextureAtlas(named: "jump_effect.atlas")
    //存储起跳特效纹理的数组
    var jumpEffectFrames = [SKTexture]()
    //起跳特效
    var jumpEffect = SKSpriteNode()
    
    //构造器
    override init() {
        let texture = runAtlas.textureNamed("panda_run_01")
        let size = texture.size()
        super.init(texture:texture,color:SKColor.whiteColor(),size:size)
        
        var i:Int
        //填充跑的纹理数组
        for i=1 ; i<=runAtlas.textureNames.count ; i++ {
            let tempName = String(format: "panda_run_%.2d", i)
            let runTexture = runAtlas.textureNamed(tempName)
            if runTexture != nil {
                runFrames.append(runTexture)
            }
        }
        //填充跳的纹理数组
        for i=1 ; i<=jumpAtlas.textureNames.count ; i++ {
            let tempName = String(format: "panda_jump_%.2d", i)
            let jumpTexture = jumpAtlas.textureNamed(tempName)
            if jumpTexture != nil {
                jumpFrames.append(jumpTexture)
            }
        }
        //填充打滚的纹理数组
        for i=1 ; i<=rollAtlas.textureNames.count ; i++ {
            let tempName = String(format: "panda_roll_%.2d", i)
            let rollTexture = rollAtlas.textureNamed(tempName)
            if rollTexture != nil{
                rollFrames.append(rollTexture)
            }
        }
        //起跳特效
        for i=1 ; i <= jumpEffectAtlas.textureNames.count ; i++ {
            let tempName = String(format: "jump_effect_%.2d", i)
            let effectexture = jumpEffectAtlas.textureNamed(tempName)
            if effectexture != nil {
                jumpEffectFrames.append(effectexture)
            }
        }
        
        jumpEffect = SKSpriteNode(texture: jumpEffectFrames[0])
        jumpEffect.position = CGPointMake(-80, -30)
        jumpEffect.hidden = true
        self.addChild(jumpEffect)
        
        run()
        
        self.zPosition = 30
        
        self.physicsBody = SKPhysicsBody(rectangleOfSize:texture.size())
        self.physicsBody?.dynamic = true
        self.physicsBody?.allowsRotation = false
        //弹性
        self.physicsBody?.restitution = 0
        self.physicsBody?.categoryBitMask = BitMaskType.runner
        self.physicsBody?.contactTestBitMask = BitMaskType.platform | BitMaskType.scene
        self.physicsBody?.collisionBitMask = BitMaskType.platform
    }
    
    func run(){
        //移除所有的动作
        self.removeAllActions()
        //将当前动作状态设为跑
        self.status = .run
        //通过SKAction.animateWithTextures将跑的文理数组设置为0.05秒切换一次的动画
        // SKAction.repeatActionForever将让动画永远执行
        // self.runAction执行动作形成动画
        self.runAction(SKAction.repeatActionForever(
            SKAction.animateWithTextures(runFrames, timePerFrame: 0.05)))
    }
    
    //跳
    func jump (){
        self.removeAllActions()
        if status != Status.jump2 {
            self.runAction(SKAction.animateWithTextures(jumpFrames, timePerFrame: 0.05))
            //施加一个向上的力,让小人跳起来
            self.physicsBody?.velocity = CGVectorMake(0, 450)
            if status == Status.jump {
                status = Status.jump2
                self.jumpStart = self.position.y
            }else{
                showJumpEffect()
                status = Status.jump
            }
        }
    }
    
    //打滚
    func roll(){
        self.removeAllActions()
        status = .roll
        self.runAction(SKAction.animateWithTextures(rollFrames, timePerFrame: 0.05),
            completion:{() in self.run()})
    }
    
    //起跳特效
    func showJumpEffect(){
        //先将特效取消隐藏
        jumpEffect.hidden = false
        //利用action播放特效
        var ectAct = SKAction.animateWithTextures( jumpEffectFrames, timePerFrame: 0.05)
        //执行闭包,再次隐藏特效
        var removeAct = SKAction.runBlock({() in
            self.jumpEffect.hidden = true
        })
        //组成序列Action进行执行
        jumpEffect.runAction(SKAction.sequence([ectAct,removeAct]))
    }
    
    required init(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

2,平台类 - Platform.swift
//平台类
import SpriteKit

class Platform:SKNode{
    //宽
    var width :CGFloat = 0.0
    //高
    var height :CGFloat = 10.0
    
    //是否下沉
    var isDown = false

    func onCreate(arrSprite:[SKSpriteNode]){
        //通过接受SKSpriteNode数组来创建平台
        for platform in arrSprite {
            //以当前宽度为平台零件的x坐标
            platform.position.x = self.width
            //加载
            self.addChild(platform)
            //更新宽度
            self.width += platform.size.width
        }
        //当平台的零件只有三样,左中右时,设为会下落的平台
        if arrSprite.count<=3 {
            isDown = true
        }
        
        self.zPosition = 20
        //设置物理体为当前高宽组成的矩形
        self.physicsBody = SKPhysicsBody(rectangleOfSize: CGSizeMake(self.width, self.height),
            center: CGPointMake(self.width/2, 0))
        //设置物理标识
        self.physicsBody?.categoryBitMask = BitMaskType.platform
        //不响应响应物理效果
        self.physicsBody?.dynamic = false
        //不旋转
        self.physicsBody?.allowsRotation = false
        //弹性0
        self.physicsBody?.restitution = 0
    }
}

3,平台工厂类 - PlatformFactory.swift
它负责生产平台零件然后传给平台类进行组装。同时负责不断地移动平台,以及移除场景之外的平台。
import SpriteKit

class PlatformFactory: SKNode {
    //定义平台左边纹理
    let textureLeft = SKTexture(imageNamed: "platform_l")
    //定义平台中间纹理
    let textureMid = SKTexture(imageNamed: "platform_m")
    //定义平台右边纹理
    let textureRight = SKTexture(imageNamed: "platform_r")

    //定义一个数组来储存组装后的平台
    var platforms = [Platform]()
    
    //游戏场景的宽度
    var sceneWidth:CGFloat = 0
    //ProtocolMainScene代理
    var delegate:ProtocolMainScene?
    
    //生成自定义位置的平台
    func createPlatform(midNum:UInt32,x:CGFloat,y:CGFloat){
        let platform = self.createPlatform(false, midNum: midNum, x: x, y: y)
        delegate?.onGetData(platform.width - sceneWidth)
    }
    
    //生成随机位置的平台的方法
    func createPlatformRandom(){
        //随机平台的长度
        let midNum:UInt32 = arc4random()%4 + 1
        //随机间隔
        let gap:CGFloat = CGFloat(arc4random()%8 + 1)
        //随机x坐标
        let x:CGFloat = self.sceneWidth + CGFloat( midNum*50 ) + gap + 100
        //随机y坐标
        let y:CGFloat = CGFloat(arc4random()%200 + 200)
        
        let platform = self.createPlatform(true, midNum: midNum, x: x, y: y)
        //回传距离用于判断什么时候生成新的平台
        delegate?.onGetData(platform.width + x - sceneWidth)
        
    }
    
    func createPlatform(isRandom:Bool,midNum:UInt32,x:CGFloat,y:CGFloat)->Platform{
        //声明一个平台类,用来组装平台。
        var platform = Platform()
        //生成平台的左边零件
        let platform_left = SKSpriteNode(texture: textureLeft)
        //设置中心点
        platform_left.anchorPoint = CGPoint(x: 0, y: 0.9)
        //生成平台的右边零件
        let platform_right = SKSpriteNode(texture: textureRight)
        //设置中心点
        platform_right.anchorPoint = CGPoint(x: 0, y: 0.9)
        
        //声明一个数组来存放平台的零件
        var arrPlatform = [SKSpriteNode]()
        //将左边零件加入零件数组
        arrPlatform.append(platform_left)
        
        //根据传入的参数来决定要组装几个平台的中间零件
        //然后将中间的零件加入零件数组
        for i in 1...midNum {
            let platform_mid = SKSpriteNode(texture: textureMid)
            platform_mid.anchorPoint = CGPoint(x: 0, y: 0.9)
            arrPlatform.append(platform_mid)
        }
        //将右边零件加入零件数组
        arrPlatform.append(platform_right)
        //将零件数组传入
        platform.onCreate(arrPlatform)
        platform.name="platform"
        //设置平台的位置
        platform.position = CGPoint(x: x, y: y)
        //放到当前实例中
        self.addChild(platform)
        //将平台加入平台数组
        platforms.append(platform)
        return platform
    }
    
    func move(speed:CGFloat){
        //遍历所有
        for p in platforms{
            //x坐标的变化长生水平移动的动画
            p.position.x -= speed
        }
        //移除平台
        if platforms[0].position.x < -platforms[0].width {
            platforms[0].removeFromParent()
            platforms.removeAtIndex(0)
        }
    }
    
    //重置方法
    func reSet(){
        //清除所有子对象
        self.removeAllChildren()
        //清空平台数组
        platforms.removeAll(keepCapacity: false)
    }
}

//定义一个协议,用来接收数据
protocol ProtocolMainScene {
    func onGetData(dist:CGFloat)
}

4,碰撞标识类 - BitMaskType.swift
记录着物理世界里的种类标示符。有小人,平台,场景边缘三种标识。
class BitMaskType {
    class var runner:UInt32{
        return 1<<0
    }
    class var platform:UInt32{
        return 1<<1
    }
    class var scene:UInt32{
        return 1<<2
    }
}

5,主场景类 - GameScene.swift
要将主场景CameScene设为物理世界,首先要让GameScene遵循SKPhysicsContactDelegatex协议。
这里面设置了重力,并进行了小人与场景边缘的碰撞检测,用于判断游戏是否结束。
同时在update()方法中,进行平台移动,新平台的创建,随游戏进行不断加速以及小人位置修正等功能。
import SpriteKit

class GameScene: SKScene,SKPhysicsContactDelegate,ProtocolMainScene {
    lazy var runner = Runner()
    lazy var platformFactory = PlatformFactory()
    
    //跑了多远变量
    var distance :CGFloat = 0.0
    //移动速度
    var moveSpeed:CGFloat = 15
    //最大速度
    var maxSpeed :CGFloat = 50.0
    //判断最后一个平台还有多远完全进入游戏场景
    var lastDis:CGFloat = 0.0
    //是否game over
    var isLose = false
    
    override func didMoveToView(view: SKView) {
        //物理世界代理
        self.physicsWorld.contactDelegate = self
        //重力设置
        self.physicsWorld.gravity = CGVectorMake(0, -5)
        //设置物理体
        self.physicsBody = SKPhysicsBody(edgeLoopFromRect: self.frame)
        //设置种类标示
        self.physicsBody?.categoryBitMask = BitMaskType.scene
        //是否响应物理效果
        self.physicsBody?.dynamic = false
        
        //场景的背景颜色
        let skyColor = SKColor(red:113/255,green:197/255,blue:207/255,alpha:1)
        self.backgroundColor = skyColor
        
        //给跑酷小人定一个初始位置
        runner.position = CGPointMake(200, 400)
        //将跑酷小人显示在场景中
        self.addChild(runner)
        //将平台工厂加入视图
        self.addChild(platformFactory)
        //将屏幕的宽度传到平台工厂类中
        platformFactory.sceneWidth = self.frame.width
        //设置代理
        platformFactory.delegate = self
        //初始平台让小人有立足之地
        platformFactory.createPlatform(3, x: 0, y: 200)
    }
    
    //触碰屏幕响应的方法
    override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
        if isLose {
            reSet()
        }else{
            runner.jump()
        }
    }
    
    //离开平台时记录起跳点
    func didEndContact(contact: SKPhysicsContact!){
        runner.jumpStart = runner.position.y
    }
    
    //碰撞检测方法
    func didBeginContact(contact: SKPhysicsContact!) {
        //小人和台子碰撞
        if (contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask)
            == (BitMaskType.platform | BitMaskType.runner){
            //假设平台不会下沉,用于给后面判断平台是否会被熊猫震的颤抖
            var isDown = false
            //用于判断接触平台后能否转变为跑的状态,默认值为false不能转换
            var canRun = false
            //如果碰撞体A是平台
            if contact.bodyA.categoryBitMask == BitMaskType.platform {
                //如果是会下沉的平台
                if (contact.bodyA.node as Platform).isDown {
                    isDown = true
                    //让平台接收重力影响
                    contact.bodyA.node?.physicsBody?.dynamic = true
                    //不将碰撞效果取消,平台下沉的时候会跟着熊猫跑这不是我们希望看到的,
                    //大家可以将这行注释掉看看效果
                    contact.bodyA.node?.physicsBody?.collisionBitMask = 0
                    //如果是会升降的平台
                }
                
                if contact.bodyB.node?.position.y > contact.bodyA.node!.position.y {
                    canRun=true
                }
                //如果碰撞体B是平台
            }else if contact.bodyB.categoryBitMask == BitMaskType.platform  {
                if (contact.bodyB.node as Platform).isDown {
                    contact.bodyB.node?.physicsBody?.dynamic = true
                    contact.bodyB.node?.physicsBody?.collisionBitMask = 0
                    isDown = true
                }
                if contact.bodyA.node?.position.y > contact.bodyB.node?.position.y {
                    canRun=true
                }
            }
            
            //判断是否打滚
            runner.jumpEnd = runner.position.y
            if runner.jumpEnd-runner.jumpStart <= -70 {
                runner.roll()
                //如果平台下沉就不让它被震得颤抖一下
                if !isDown {
                    downAndUp(contact.bodyA.node!)
                    downAndUp(contact.bodyB.node!)
                }
            }else{
                if canRun {
                    runner.run()
                }
            }
        }
        
        //如果熊猫和场景边缘碰撞
        if (contact.bodyA.categoryBitMask|contact.bodyB.categoryBitMask)
            == (BitMaskType.scene | BitMaskType.runner) {
            println("游戏结束")
            isLose = true
        }
    }
    
    override func update(currentTime: CFTimeInterval) {
        //如果小人出现了位置偏差,就逐渐恢复
        if runner.position.x < 200 {
            var x = runner.position.x + 1
            runner.position = CGPointMake(x, runner.position.y)
        }
        if !isLose {
            lastDis -= moveSpeed
            //速度以5为基础,以跑的距离除以2000为增量
            var tempSpeed = CGFloat(5 + Int(distance/2000))
            //将速度控制在maxSpeed
            if tempSpeed > maxSpeed {
                tempSpeed = maxSpeed
            }
            //如果移动速度小于新的速度就改变
            if moveSpeed < tempSpeed {
                moveSpeed = tempSpeed
            }
            
            if lastDis <= 0 {
                platformFactory.createPlatformRandom()
            }
            platformFactory.move(self.moveSpeed)
            distance += moveSpeed
        }
        
    }
    
    func onGetData(dist:CGFloat){
        self.lastDis = dist
    }
    
    //up and down 方法(平台震动一下)
    func downAndUp(node :SKNode,down:CGFloat = -50,downTime:Double=0.05,
        up:CGFloat=50,upTime:Double=0.1){
        //下沉动作
        let downAct = SKAction.moveByX(0, y: down, duration: downTime)
        //上升动过
        let upAct = SKAction.moveByX(0, y: up, duration: upTime)
        //下沉上升动作序列
        let downUpAct = SKAction.sequence([downAct,upAct])      
        node.runAction(downUpAct)
    }
    
    //重新开始游戏
    func reSet(){
        //重置isLose变量
        isLose = false
        //重置小人位置
        runner.position = CGPointMake(200, 400)
        //重置移动速度
        moveSpeed  = 15.0
        //重置跑的距离
        distance = 0.0
        //重置首个平台完全进入游戏场景的距离
        lastDis = 0.0
        //平台工厂的重置方法
        platformFactory.reSet()
        //创建一个初始的平台给熊猫一个立足之地
        platformFactory.createPlatform(3, x: 0, y: 200)
    }
}

四,源码下载
   Runner.zip
评论2
  • 2楼
    2016-02-24 15:29
    ppp

    请问假如在场景内同时显示1000个SKSpriteNode,怎么才能让fps不下降那么厉害,SKSpriteNode同一个纹理。

    站长回复

    SKSpriteNode数量太多造成性能下降,我目前也不知道有什么好办法解决。

  • 1楼
    2015-12-17 15:33
    pzy

    请问计算两点之间角度和距离的公式是什么啊

    站长回复

    可以看我这篇文章 http://www.hangge.com/blog/cache/detail_674.html