iOS点点滴滴傲视苍穹iOS《Swift》VIP专题iOS学习开发

SpriteKit实战—FlappyBirdSwift

2017-11-15  本文已影响159人  minjing_lin

Spritekit是iOS 7之后苹果官方推出的2D游戏开发框架,最近利用业余时间认真学习了这方面的知识,并利用网上资源及教程用Swift语言仿写了一个以前比较火的小游戏FlappyBird

bird.gif

1.准备

新建一个Project项目,模板选择Game,语言选择Swift,开发库选择SpriteKit

3.41.51.png
3.43.14.png

删除xcode自动创建的实例文件GameScene.sksActions.sks,把GameViewController.swift中viewDidLoad方法中的代码替换为下面的代码

 if let view = self.view as! SKView? {
            
            let scene = GameScene(size:view.bounds.size)
                scene.scaleMode = .aspectFill
                view.presentScene(scene)
            view.ignoresSiblingOrder = true
            
            view.showsFPS = true
            view.showsNodeCount = true
        }

删除GameScene.swift中的代码,留下didMoveupdate方法,在didMove方法中添加背景颜色,如下

override func didMove(to view: SKView) {
        self.backgroundColor =  SKColor(red: 81.0/255.0, green: 192.0/255.0, blue: 201.0/255.0, alpha: 1.0)
    }

这时候就可以运行代码了


16.23.45.png

导入资源文件
新建一个.atlas为后缀的文件,放入小鸟的图片,然后将这个文件夹拖到工程中。然后把其他图片放在Asserts.xcasserts里就可以了。

4.27.57.png
注:因为当你把一类相关的贴图图片素材放在一个.atlas文件夹里,编译程序的时候Xcode会把这个文件夹里的图片都导入“纹理图集”里,相对于只用独立的图片文件而言,使用纹理图集会非常显著地提升游戏的渲染性能。
部分xcode版本,加载.atlas中的图片时,提示 SKTexture: Error loading image resource: "bird-01.png"
是因为: Xcode在直接Add Files to Project 图片文件的时候,没有自动将其添加到编译资源文件中,需要去项目中(Build Phases)手动添加资源文件(copy Bundle Resource)。

到此,准备工作结束~

2.布置场景

切换到GameScene.swift类中,添加变量

/// 小鸟精灵
var bird :SKSpriteNode!

然后再在didMove()方法里添加下面的代码

 // 地面
 let groundTexture = SKTexture(imageNamed: "land")
 groundTexture.filteringMode = .nearest
 for i in 0..<2 + Int(self.frame.size.width / (groundTexture.size().width * 2)) {
       let i = CGFloat(i)
       let sprite = SKSpriteNode(texture: groundTexture)
       sprite.setScale(2.0)
       // SKSpriteNode的默认锚点为(0.5,0.5)即它的中心点。
       sprite.anchorPoint = CGPoint(x: 0, y: 0)
       sprite.position = CGPoint(x: i * sprite.size.width, y: 0)
       self.addChild(sprite)
}
// 天空
let skyTexture = SKTexture(imageNamed: "sky")
    skyTexture.filteringMode = .nearest
    for i in 0..<2 + Int(self.frame.size.width / (skyTexture.size().width * 2)) {
        let i = CGFloat(i)
        let sprite = SKSpriteNode(texture: skyTexture)
        sprite.setScale(2.0)
        sprite.zPosition = -20
        sprite.anchorPoint = CGPoint(x: 0, y:0)
        sprite.position = CGPoint(x: i * sprite.size.width, y:groundTexture.size().height * 2.0)
        moving.addChild(sprite)
    }
//小鸟
bird = SKSpriteNode(imageNamed: "bird-01")
    bird.setScale(1.5)
    bird.position = CGPoint(x: self.frame.size.width * 0.35, y: self.frame.size.height * 0.6)
    addChild(bird)

运行代码,如下图所示:

18.06.29.png

3.精灵动起来

由于地面移动和天空移动类似,都是两个图片交替出现,只是速度不同,所以我们封装一个方法,分别传入对应的精灵及时间。如下:

//陆地及天空移动动画
func moveGround(sprite:SKSpriteNode,timer:CGFloat) {
    let moveGroupSprite = SKAction.moveBy(x: -sprite.size.width, y: 0, duration: TimeInterval(timer * sprite.size.width))
    let resetGroupSprite = SKAction.moveBy(x: sprite.size.width, y: 0, duration: 0.0)
    //永远移动 组动作
    let moveGroundSpritesForever = SKAction.repeatForever(SKAction.sequence([moveGroupSprite,resetGroupSprite]))
    sprite.run(moveGroundSpritesForever)
}

然后在上面代码中,添加精灵之前,就是在 self.addChild(sprite)之前,分别加入精灵动作

// 地面
self.moveGround(sprite: sprite, timer: 0.02)
//天空
self.moveGround(sprite: sprite, timer: 0.1)

之后是我们的主要对象小鸟精灵的活灵活现~

///  小鸟飞的动画
func birdStartFly()  {
    let birdTexture1 = SKTexture(imageNamed: "bird-01")
    birdTexture1.filteringMode = .nearest
    let birdTexture2 = SKTexture(imageNamed: "bird-02")
    birdTexture2.filteringMode = .nearest
    let birdTexture3 = SKTexture(imageNamed: "bird-03")
    birdTexture3.filteringMode = .nearest
    let anim = SKAction.animate(with: [birdTexture1,birdTexture2,birdTexture3], timePerFrame: 0.2)
    bird.run(SKAction.repeatForever(anim), withKey: "fly")
}
 ///  小鸟停止飞动画
 func birdStopFly()  {
     bird.removeAction(forKey: "fly")
 }

运行效果图如下,看小鸟是不是真的飞起来了,哈哈:

birdFly.gif

4.随机创建管道

首先添加变量

/// 竖直管缺口
let verticalPipeGap = 150.0;
/// 向上管纹理
var pipeTextureUp:SKTexture!
/// 向下管纹理
var pipeTextureDown:SKTexture!
/// 储存所有上下管道
var pipes:SKNode!

实现上下管道的随机创建及消失,需要下面四个方法:

///创建一对水管
func creatSpawnPipes() {
    // 管道纹理
    pipeTextureUp = SKTexture(imageNamed: "PipeUp")
    pipeTextureUp.filteringMode = .nearest
    pipeTextureDown = SKTexture(imageNamed: "PipeDown")
    pipeTextureDown.filteringMode = .nearest
    
    let pipePair = SKNode()
    pipePair.position = CGPoint(x: self.frame.size.width + pipeTextureUp.size().width * 2, y: 0)
    // z值的节点(用于排序)。负z是”进入“屏幕,正面z是“出去”屏幕。
    pipePair.zPosition = -10;
    
    // 随机的Y值
    let height = UInt32(self.frame.size.height / 5)
    let y = Double(arc4random_uniform(height) + height)
    
    let pipeDown = SKSpriteNode(texture: pipeTextureDown)
    pipeDown.setScale(2.0)
    pipeDown.position = CGPoint(x: 0.0, y: y + Double(pipeDown.size.height)+verticalPipeGap)
    pipePair.addChild(pipeDown)
    
    let pipeUp = SKSpriteNode(texture: pipeTextureUp)
    pipeUp.setScale(2.0)
    pipeUp.position = CGPoint(x: 0.0, y: y)
    pipePair.addChild(pipeUp)
    
    // 管道移动动作
    let distanceToMove = CGFloat(self.frame.size.width + 2.0*pipeTextureUp.size().width)
    let movePipes = SKAction.moveBy(x: -distanceToMove, y: 0.0, duration: TimeInterval(0.01 * distanceToMove))
    let removePipes = SKAction.removeFromParent()
    let movePipesAndRemove = SKAction.sequence([movePipes,removePipes])
    pipePair.run(movePipesAndRemove)
    
    pipes.addChild(pipePair)
}
/// 随机 创建
func startCreateRandomPipes() {
    let spawn = SKAction.run {
        self.creatSpawnPipes()
    }
    let delay = SKAction.wait(forDuration: TimeInterval(2.0))
    let spawnThenDelay = SKAction.sequence([spawn,delay])
    let spawnThenDelayForever = SKAction.repeatForever(spawnThenDelay)
    self.run(spawnThenDelayForever, withKey: "createPipe")
}
///停止创建管道
func stopCreateRandomPipes() {
    self.removeAction(forKey: "createPipe")
}
/// 移除所有已经存在的上下管
func removeAllPipesNode() {
    pipes.removeAllChildren()
}
birdFly2.gif

写到这里,我们已经完成了这个游戏效果的一半了,但是细心的你会发现,点击屏幕无效果,小鸟不受重力影响下落,小鸟与水管相撞没反应等。。。
那么牵扯出了下一部分,物理世界~

5.物理世界

然后再在didMove()方法里,背景色代码下面,添加下面的代码

//给场景添加一个物理体,限制了游戏范围,确保精灵不会跑出屏幕。
 self.physicsBody = SKPhysicsBody(edgeLoopFrom: self.frame)
 //设置重力
 self.physicsWorld.gravity = CGVector(dx: 0.0, dy: -3.0)
 //物理世界的碰撞检测代理为场景自己
 self.physicsWorld.contactDelegate = self;

然后,让GameScene这个类遵守下面的代理协议
SKPhysicsContactDelegate
协议方法待会再讲,先声明我们要用到的几个物理体

/// 设置物理体的标示符  
let birdCategory: UInt32 = 1 << 0  //1
let worldCategory: UInt32 = 1 << 1  //2
let pipeCategory: UInt32 = 1 << 2  //4

在添加地面的代码后面加上:

// 配置陆地物理体
    let ground = SKNode()
    ground.position = CGPoint(x: 0, y: groundTexture.size().height)
    ground.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: self.frame.size.width, height: groundTexture.size().height * 2.0))
    ground.physicsBody?.isDynamic = false
//当前物理体
    ground.physicsBody?.categoryBitMask = worldCategory
    self.addChild(ground)

在添加小鸟的代码后面加上:

    // 配置小鸟物理体
    bird.physicsBody = SKPhysicsBody(circleOfRadius: bird.size.height / 2.0)
    bird.physicsBody?.allowsRotation = false
    bird.physicsBody?.categoryBitMask = birdCategory
    bird.physicsBody?.contactTestBitMask = worldCategory 

找到creatSpawnPipes方法中,添加上下水管代码之前加入下面的代码内容:

pipeDown.physicsBody = SKPhysicsBody(rectangleOf: pipeDown.size)
pipeDown.physicsBody?.isDynamic = false
pipeDown.physicsBody?.categoryBitMask = pipeCategory
pipeDown.physicsBody?.contactTestBitMask = birdCategory
 pipeUp.physicsBody = SKPhysicsBody(rectangleOf: pipeUp.size)
 pipeUp.physicsBody?.isDynamic = false
 pipeUp.physicsBody?.categoryBitMask = pipeCategory
 pipeUp.physicsBody?.contactTestBitMask = birdCategory

有了物理体之后,我们会发现,运行游戏后,小鸟会直接受重力影响,掉下来~
接下来,我们就不得不考虑,游戏的运行状态了

6.游戏状态

1.初始化状态:小鸟在移动的背景中飞翔而未掉落,无水管出现。
2.运行中状态:小鸟会受重力作用往下坠落,水管开始出现,点击一次屏幕,小鸟就有会受一次上升的力。
3.已结束状态:小鸟碰到水管或地面,游戏结束,小鸟停止飞的动作,场景里的水管和地面都停住不动。

在GameScene类中,定义一个枚举来表示不同的状态,同时增加一个游戏状态的变量

/// 游戏状态
enum GameStatus {
    case idle /// 初始化
    case running /// 游戏运行中
    case over /// 游戏结束
}
/// 游戏状态为初始状态
var gameStatus:GameStatus = .idle

实现不同状态对应的不同方法

func idleStatus() {
        gameStatus = .idle
}
func runningStatus() {
        gameStatus = .running
}
func overStatus() {
        gameStatus = .over
}

实现点击屏幕对应不同的状态时,所作出的应对

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    switch gameStatus {
    case .idle:
        runningStatus()
        break
    case .running:
        for _ in touches {
            bird.physicsBody?.velocity = CGVector(dx: 0, dy: 0)
            // 施加一个均匀作用于物理体的推力
            bird.physicsBody?.applyImpulse(CGVector(dx: 0, dy: 10))
        }
        break
    case .over:
        idleStatus()
        break
    }
}

小鸟:这时候,我们可以把小鸟的位置、受外力影响只为否属性和开始飞的方法移到初始化方法里面

bird.position = CGPoint(x: self.frame.size.width * 0.35, y: self.frame.size.height * 0.6)
 // isDynamic的作用是设置这个物理体当前是否会受到物理环境的影响,默认是true
bird.physicsBody?.isDynamic = false
self.birdStartFly()

接着,在运行中状态,将小鸟受外力影响属性置为是

bird.physicsBody?.isDynamic = true
bird.physicsBody?.collisionBitMask = worldCategory | pipeCategory

然后,在结束状态中,让小鸟停止飞

birdStopFly()

水管:初始化方法,移除屏幕中上次可能存在的水管

removeAllPipesNode()

在运行中状态,开始随机生产水管

startCreateRandomPipes()

已结束状态,停止随机创建水管

stopCreateRandomPipes()

现在运行代码,来看看效果怎么样~


MJBird.gif

我们会看到,只有初始化状态和运行中状态,而碰撞之后,并没有结束,那么下面我们就来实现一下碰撞协议~

/// SKPhysicsContact对象是包含着碰撞的两个物理体的,分别是bodyA和bodyB
func didBegin(_ contact: SKPhysicsContact) {
    if gameStatus != .running {
        return
    }
        bird.physicsBody?.collisionBitMask = worldCategory
        overStatus()
}

这时运行代码发现,碰撞水管之后,背景及水管还在运动,我们应该让其停止,我们首先创建变量

/// 储存陆地、天空和水管
var moving:SKNode!

然后再在didMove()方法里初始化变量,然后把之前的储存水管的变量pipes也加到moving里面

moving = SKNode()
self.addChild(moving)
pipes = SKNode()
moving.addChild(pipes)

self.addChild(sprite)self.addChild(sprite)替换为moving.addChild(sprite)moving.addChild(sprite)

在碰撞协议方法里面,加上让其速度为零的属性

moving.speed = 0

然后在初始化方法里面,让其速度恢复

moving.speed = 1

运行效果如下:


MJBird2.gif

7.分数显示

首先声明一个分数变量和懒加载一个分时显示Label

 /// 分数
 var score: NSInteger = 0
 ///分数Label
 lazy var scoreLabelNode:SKLabelNode = {
     let label = SKLabelNode(fontNamed: "MarkerFelt-Wide")
     label.zPosition = 100
     label.text = "0"
     return label
 }()

考虑到加分问题,肯定是在小鸟通过管道的时候,才计分+1,所以我们在管道右端加一个物理体,来实现与小鸟碰撞之后的加分。
在添加管道代码下面添加如下:

    let contactNode = SKNode()
    contactNode.position = CGPoint(x: pipeDown.size.width, y: self.frame.midY)
    contactNode.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: pipeUp.size.width, height: self.frame.size.height))
    contactNode.physicsBody?.isDynamic = false
    contactNode.physicsBody?.categoryBitMask = scoreCategory
    contactNode.physicsBody?.contactTestBitMask = birdCategory
    pipePair.addChild(contactNode)

然后在碰撞方法中,判断是否相撞,通过管道,分数+1

if (contact.bodyA.categoryBitMask & scoreCategory) == scoreCategory || (contact.bodyB.categoryBitMask & scoreCategory) == scoreCategory {
        score += 1
        print(score)
        scoreLabelNode.text = String(score)
        scoreLabelNode.run(SKAction.sequence([SKAction.scale(to: 1.5, duration: TimeInterval(0.1)),SKAction.scale(to: 1.0, duration: TimeInterval(0.1))]))
    }else{
        moving.speed = 0
        bird.physicsBody?.collisionBitMask = worldCategory
        overStatus()
    }

在运行中状态,显示分数

// 重设分数
 score = 0
 scoreLabelNode.text = String(score)
 self.addChild(scoreLabelNode)
 scoreLabelNode.position = CGPoint(x: self.frame.midX, y: 3 * self.frame.size.height / 4)

初始化状态,移除分数labelNode

 // 移除分数提示
 scoreLabelNode.removeFromParent()

这是运行代码,这款小游戏就基本完成了


birdFly3.gif

8. 优化

上面提到,我们留下两个方法,已经用到了didMove() ,那么update()是做什么的呐:update()方法为SKScene自带的系统方法,在画面每一帧刷新的时候就会调用一次。

在这里,我们可以处理让小鸟掉落的时候脸先着地(233~)

override func update(_ currentTime: TimeInterval) {
    //调整头先着地
    let value = bird.physicsBody!.velocity.dy * (bird.physicsBody!.velocity.dy < 0 ? 0.003 : 0.001)
    bird.zRotation = min(max(-1, value),0.5)
}

碰撞之后,添加一个背景闪光效果

func bgFlash() {
    let bgFlash = SKAction.run({
        self.backgroundColor = SKColor(red: 1, green: 0, blue: 0, alpha: 1.0)}
    )
    let bgNormal = SKAction.run({
        self.backgroundColor = self.skyColor;
    })
    let bgFlashAndNormal = SKAction.sequence([bgFlash,SKAction.wait(forDuration: (0.05)),bgNormal,SKAction.wait(forDuration: (0.05))])
    self.run(SKAction.sequence([SKAction.repeat(bgFlashAndNormal, count: 4)]), withKey: "falsh")
    self.removeAction(forKey: "flash")
}

添加结束提示语:同样懒加载一个SKLabelNode

lazy var gameOverLabel:SKLabelNode = {
     let label = SKLabelNode(fontNamed: "Chalkduster")
     label.text = "Game Over"
     return label
 }()

在结束状态中,添加上这个提示语,同时为了让游戏有个缓冲过程,我们让屏幕2秒内不能点击

 isUserInteractionEnabled = false;
 addChild(gameOverLabel)
 gameOverLabel.position = CGPoint(x: self.size.width * 0.5, y: self.size.height)

 //让gameOverLabel通过一个动画action移动到屏幕中间
 let delay = SKAction.wait(forDuration: TimeInterval(1))
 let move = SKAction.move(by: CGVector(dx: 0, dy: -self.size.height * 0.5), duration: 1)
 gameOverLabel.run(SKAction.sequence([delay,move]), completion:{
       //动画结束 允许用户点击屏幕
       self.isUserInteractionEnabled = true
})

在初始化状态,移除提示语

 // 移除 游戏结束提示
 gameOverLabel.removeFromParent()

OK,现在一款”飞翔的小鸟”简易款就成型了,可以愉快的玩耍了~

代码连接

升级款:
1.全新的UI视图;
2.动听的音乐组合;
3.计分板和引导提示图的展示。

资料参考:

SpriteKit初探
SKScene类
AnchorPoint(锚点)
SKSpriteNode
SKLableNode
SKTextureAtlas(纹理集)
SKAction动作
SKSpriteNode拖动
SKAction常用属性
SKPhysicsBody物理引擎
节点碰撞
SKPhysicsBody的移动和连接
iOS SpriteKit 小游戏开发实例
FlappySwift

上一篇下一篇

猜你喜欢

热点阅读