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) } }
四,源码下载
请问假如在场景内同时显示1000个SKSpriteNode,怎么才能让fps不下降那么厉害,SKSpriteNode同一个纹理。
请问计算两点之间角度和距离的公式是什么啊