SpriteKit實戰—FlappyBirdSwift——飛翔的鳥小遊戲

轉發自:https://www.jianshu.com/p/304e84a12b91

Spritekit是iOS 7之後蘋果官方推出的2D遊戲開發框架,最近利用業餘時間認真學習了這方面的知識,並利用網上資源及教程用Swift語言仿寫了一個以前比較火的小遊戲FlappyBird

 

1.準備

新建一個Project項目,模板選擇Game,語言選擇Swift,開發庫選擇SpriteKit

 

 

刪除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)
    }

這時候就可以運行代碼了

導入資源文件
新建一個.atlas爲後綴的文件,放入小鳥的圖片,然後將這個文件夾拖到工程中。然後把其他圖片放在Asserts.xcasserts裏就可以了。


注:因爲當你把一類相關的貼圖圖片素材放在一個.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)

運行代碼,如下圖所示:

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")
 }

運行效果圖如下,看小鳥是不是真的飛起來了,哈哈:

4.隨機創建管道

首先添加變量

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

實現上下管道的隨機創建及消失,需要下面四個方法:

  • 方法creatSpawnPipes() 創建具體某一次某一對水管的方法
  • 方法startCreateRandomPipes() 開始隨機重複創建水管的動作方法
  • 方法stopCreateRandomPipes() 停止創建水管的動作方法
  • 方法removeAllPipesNode() 移除所有正在場景裏的水管
///創建一對水管
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()
}

寫到這裏,我們已經完成了這個遊戲效果的一半了,但是細心的你會發現,點擊屏幕無效果,小鳥不受重力影響下落,小鳥與水管相撞沒反應等。。。
那麼牽扯出了下一部分,物理世界~

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()

現在運行代碼,來看看效果怎麼樣~

 

我們會看到,只有初始化狀態和運行中狀態,而碰撞之後,並沒有結束,那麼下面我們就來實現一下碰撞協議~

/// 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

運行效果如下:

 

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()

這是運行代碼,這款小遊戲就基本完成了

 

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


 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章