iOS SpriteKit 小遊戲開發實例 - Flappy Bird

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

最近利用業餘時間根據官方文檔和網上的資料學習了蘋果官方推出的2D遊戲開發引擎Spritekit基本知識,模仿做了一個前兩年火了一火的小遊戲flappy bird練練手,現在就來一步一步講講這個遊戲我的實現方法。

因爲Apple推行Swift開發語言,Swift也將是以後iOS方面開發的主力語言,所有這篇實例我們也就先拋棄Objective-C,而使用Swift3開發語言,如果你還不熟悉Swift的基本語法那趕快去學學吧,如果你已經瞭解了那麼就跟着我繼續吧!

先看看最後最後做完的樣子

1.準備工作

 

新建工程

先新建一個工程項目,模板選擇Game


語言選擇Swift,開發庫選擇SpriteKit

 

刪除示例文件和代碼

然後我們的工程就建立好了。接着我們就先把那些Xcode自動創建的示例文件和代碼都刪除掉,先看文件目錄欄,把GameScene.sks和Actions.sks兩個文件刪除掉,然後進入Assets.xcassets把裏面的那張飛機圖片刪除掉。這樣我們就把用不到的文件都刪掉了,接下來繼續刪除沒用的代碼

首先進入GameViewController.swift文件,找到那個viewDidLoad()方法,看到裏面下面內容。

注意看這一句

if let scene = SKScene(fileNamed: "GameScene")  {

........

}

這一句是通過一個GameScene的sks文件來創建一個場景實例對象,由於咱們剛剛把GameScene.sks文件刪除了,所以我們現在是創建不出來場景的,所以我們需要把viewDidLoad()裏面的代碼改成下面的內容

super.viewDidLoad()

if let view = self.view as! SKView? {

    let scene = GameScene(size: view.bounds.size)  //通過代碼創建一個GameScene類的實例對象

    scene.scaleMode = .aspectFill

    view.presentScene(scene)

    view.ignoresSiblingOrder = true

    view.showsFPS = true

    view.showsNodeCount = true

}

 

現在我們就把通過sks文件創建場景對象改成了通過代碼直接創建一個叫做GameScene類的實例對象了。

到這裏我們的GameViewController文件就改完了。接下來我們進入GameScene.swift我們最終要的場景類的文件看一看

。。。。我勒個去。。。。你會發現Xcode給我們自動添加了這麼多示例的代碼,然並卵,都刪掉!刪到跟下圖一樣只留下didMove()和update()兩個空方法即可!

我們在didMove方法裏先加上一句代碼,設置場景的背景色爲淡藍色,現在我們就可以運行一下程序看看顯示的是不是一個淡藍色的界面

self.backgroundColor = SKColor(red: 80.0/255.0, green: 192.0/255.0, blue: 203.0/255.0, alpha: 1.0)

didMove()方法會在當前場景被顯示到一個view上的時候調用,你可以在裏面做一些初始化的工作

這樣看來一切正常,我們自己的場景終於顯示在玩家面前了。

 

導入資源文件

我自己選用了3張小鳥的png圖片,一張翅膀上擡、一張翅膀放平、一張翅膀下墜,這樣我們一會就可以做出小鳥在飛的效果。圖片大小都是50*43,你也可以自己在網上找幾張類似的圖片來使用,尺寸別太大,雖然你可以通過代碼改變小鳥的小大,但是如果你的圖片本身很大,你實際需要它顯示的比較小,那麼對性能其實有點浪費,不過對於這種小遊戲來說你想怎麼弄都沒問題的。

PS:稍後如果我把我的工程放上網你們也可以直接下載我的工程,直接用裏面的圖片素材

導入圖片注意:先新建一個叫player.atlas的文件夾,然後我們把這三張圖片放到這個文件夾下,然後再將這個文件夾拖到工程裏面,注意要勾選copy item if need。

爲什麼要這樣做?

因爲當你把一類相關的貼圖圖片素材放在一個.atlas文件夾裏,編譯程序的時候Xcode會把這個文件夾裏的圖片都導入“紋理圖集”裏,相對於只用獨立的圖片文件而言,使用紋理圖集會非常顯著地提升遊戲的渲染性能

然後我們再將另外三張圖片丟入工程的Asserts.xcasserts裏即可,分別是地面(floor),上水管(topPipe)和下水管(bottomPipe)

 

至此準備工作全部完成,我們終於可以開始敲代碼了!


 

 

2.佈置場景和遊戲狀態

 

PS:由於這個遊戲比較小也不復雜,所以咱們也就不設計什麼高級的開發模式來開發這個遊戲了,全部的佈局邏輯代碼全部都寫在GameScene.swift文件裏。

 

佈置地面

我們先進入GameScene.swift,給GameScene這個類添加兩個地面的變量

var floor1: SKSpriteNode!

var floor2: SKSpriteNode!

然後再在didMove()方法裏添加下面的代碼

// Set floors

floor1 = SKSpriteNode(imageNamed: "floor")

floor1.anchorPoint = CGPoint(x: 0, y: 0)

floor1.position = CGPoint(x: 0, y: 0)

addChild(floor1)

floor2 = SKSpriteNode(imageNamed: "floor")

floor2.anchorPoint = CGPoint(x: 0, y: 0)

floor2.position = CGPoint(x: floor1.size.width, y: 0)

addChild(floor2)

可以看到爲什麼我弄了兩個floor?因爲我們一會要讓floor向左移動,使得看起來小鳥在向右飛,所以我弄了兩個floor頭尾兩連地放着,等會我們就讓兩個floor一起往左邊移動,當左邊的floor完全超出屏幕的時候,就馬上把左邊的floor移動憑藉到右邊的floor後面然後繼續向左移動,如此循環下去。

我將anchorPoint設置爲(0,0),即SpriteNode的左下角的點作爲這個node的錨點,是爲了方便定位floor,如果不熟悉錨點是什麼的朋友趕快去搜一搜!

SKScene場景的默認錨點爲(0,0)即左下角,SKSpriteNode的默認錨點爲(0.5,0.5)即它的中心點。

另外SpriteKit的座標系是向右x增加,向上y增加。而不像做iOS應用開發時候UIKit是向右x增加,向下y增加!

現在讓我們運行一下程序就可以看到我們的地面出現了!

 

放置小鳥

我們來講我們的遊戲主角小鳥顯示出來,同樣給GameScene類增加一個小鳥的變量

var bird: SKSpriteNode!

然後在didMove()方法裏,在添加floor的後面添加下面代碼

bird = SKSpriteNode(imageNamed: "player1")

addChild(bird)

這樣我們就將我們的主角小鳥添加到場景上了。等等!你還沒給小鳥設置position呢,不是應該把小鳥放到屏幕中間開始麼?

 

遊戲狀態

沒錯,但是在我們設置它位置之前,我們先構想一下我們這個遊戲整個運行的流程:

1.一開始小鳥在屏幕中間飛,地面也在移動,但是這個時候還沒有真的開始,所以還不會有水管出現。

2.當玩家準備好了點了一下屏幕,遊戲正式開始,小鳥會受重力作用往下墜落,水管開始出現,此時玩家每點擊一次屏幕小鳥就有會受一次上升的力。

3.如果小鳥碰到水管或者小鳥碰到地面了,則遊戲結束,小鳥停止飛的動作,場景裏的水管和地面都停住不動。此時玩家再點擊屏幕則回到上面1初始狀態。

可以看到,玩家的操作和場景內容的移動與否都與當前遊戲的進程狀態有關係,我們也可以看出有三個狀態:1初始狀態 2遊戲進行中狀態 3遊戲結束狀態

那我們現在GameScene類裏面定義一個枚舉來表示不同的狀態,同時給GameScene增加一個遊戲狀態的變量

enum GameStatus {

    case idle    //初始化

    case running    //遊戲運行中

    case over    //遊戲結束

}

var gameStatus: GameStatus = .idle  //表示當前遊戲狀態的變量,初始值爲初始化狀態

現在我們知道了整個遊戲會有三個進程狀態,那麼我們就給GameScene增加三個對應的方法,分別來處理這個三個狀態。

func shuffle()  { 

//遊戲初始化處理方法

gameStatus = .idle

}
func startGame()  { 

//遊戲開始處理方法

gameStatus = .running

}
func gameOver()  {

//遊戲結束處理方法

gameStatus = .over

}

 

可以看到目前我們只在這三個方法裏分別修改了當前遊戲的進程狀態變量。

接下來大家再想想上面那個沒解決的問題,設置小鳥初始化的位置該放在那裏呢?當然是初始化shuffle()方法裏啦,添加下面代碼內容到shuffle()方法裏

bird.position = CGPoint(x: self.size.width * 0.5, y: self.size.height * 0.5)

那麼我們應該什麼時候來調用這三個方法呢?

首先在場景初始化完成的時候,肯定要先調用一下shuffle()初始化,所有我們在didMove()方法裏的最後面添加一句

shuffle()

然後再給GameScene添加下面這個方法

override func touchesBegan(_ touches: Set, with event: UIEvent?) {

    switch gameStatus {

       case .idle:

          startGame()  //如果在初始化狀態下,玩家點擊屏幕則開始遊戲

       case .running:

          print("給小鳥一個向上的力")   //如果在遊戲進行中狀態下,玩家點擊屏幕則給小鳥一個向上的力(暫時用print一句話代替)

      case .over:

         shuffle()  //如果在遊戲結束狀態下,玩家點擊屏幕則進入初始化狀態

      }

}

touchesBegan()是SKScene自帶的系統方法,當玩家手指點擊到屏幕上的時候會調用,可以看到我們用switch語句來處理了三種不同的遊戲狀態下,玩家點擊屏幕後做出的不同響應

現在讓我們來運行一下程序,可以看到小鳥也正常的出現在屏幕中間了


 

 

3.讓內容動起來

 

我們目前可以看到雖然我們看到了小鳥和地面,但是怎麼都是死的,這也太假了,那麼接下來我們要讓他們都動起來,讓小鳥好像真的在飛

移動地面

我們先來移動地面,我們給GameScene添加一個叫做moveScene()的方法,用來使場景內的物體向左移動起來,暫時我們先讓地面移動,稍後還會在這個方法裏添加讓水管移動的代碼

func moveScene() {

    //make floor move

    floor1.position = CGPoint(x: floor1.position.x - 1, y: floor1.position.y)

    floor2.position = CGPoint(x: floor2.position.x - 1, y: floor2.position.y)

    //check floor position

    if floor1.position.x < -floor1.size.width {

        floor1.position = CGPoint(x: floor2.position.x + floor2.size.width, y: floor1.position.y)

    }

    if floor2.position.x < -floor2.size.width {

        floor2.position = CGPoint(x: floor1.position.x + floor1.size.width, y: floor2.position.y)

    }

}

我們在這個方法裏先讓兩個floor向左移動1的位置,然後檢查兩個floor是否已經完全超出屏幕的左邊,超出的floor則移動到另一個floor的右邊。

那我們該什麼時候調用這個方法呢?我們可以在update()方法裏調用moveScene()方法。

還記得update()方法麼?我們最開始留下的兩個空方法,一個是didMove()另一個就是update()呀!

update()方法爲SKScene自帶的系統方法,在畫面每一幀刷新的時候就會調用一次

那麼就在update()方法裏添加一下內容代碼

if gameStatus != .over {

    moveScene()

}

如果當前遊戲狀態不是結束的,則每次調用update()的時候都調用moveScene()方法,回想一下我們上面提高的遊戲流程是不是應該這樣呢?

運行一下程序,看我們的地面是不是東西來,就想鳥在向右飛一樣

 

小鳥動起來

現在我們讓鳥也飛起來吧!

先給GameScene添加兩個新的方法,一個是讓小鳥開始飛,一個是讓小鳥停止飛(遊戲結束,小鳥墜地了就要停止飛)

//開始飛

func birdStartFly() {

    let flyAction = SKAction.animate(with: [SKTexture(imageNamed: "player1"),

                                                                       SKTexture(imageNamed: "player2"),

                                                                       SKTexture(imageNamed: "player3"),

                                                                       SKTexture(imageNamed: "player2")],

                                                           timePerFrame: 0.15)

    bird.run(SKAction.repeatForever(flyAction), withKey: "fly")

}

//停止飛

func birdStopFly() {

    bird.removeAction(forKey: "fly")

}

在birdStartFly()方法裏

我們用了準備的3張小鳥的圖片生成了四個SKTexture紋理對象,他們四個連起來就是小鳥的翅膀從上->中->下->中這樣一個循環過程

然後用這一組紋理創建了一個飛的動作(flyAction),同時設置紋理的變化時間爲0.15秒

然後讓小鳥重複循環執行這個飛的動作,同時給這個動作使用了一個叫"fly"的key來標識

在birdStopFly()方法裏只有一句代碼,就是把fly這個動作從小鳥身上移除掉

 

接下來我們分別在shuffle()方法裏添加一句讓小鳥開始飛,

birdStartFly()

在gameOver()方法裏添加一句讓小鳥停止飛

birdStopFly()

現在運行程序就能看到小鳥像是真的在往右邊飛!

 

 

4.隨機創造水管

現在我們地面有了,小鳥也有了,該要讓水管上場了。

我們先想想水管出現有什麼特點

1.成對的出現,一個在上一個在下,上下兩個水管中間留有一定的高度的距離讓小鳥能通過

2.上下水管之間的高度距離是隨機的,但是有個最小值和最大值

3.一對水管出現之後向左移動,移動出了屏幕左側就要把它移除掉

4.一對水管出現之後,間隔一定的時間,再產生另一對水管,間隔的時間也是隨機數,也要設一個最大和最三小值

5.在遊戲初始化狀態下要停止重複創建水管,同時要移除掉場景裏上一句殘留的水管。在遊戲進行中狀態下才重複創建水管。在遊戲結束狀態下,停止創建水管,如果場景裏還有存在水管,則停止左移

那麼我準備了四個方法來實現水管功能(5個方法不是跟上面5個特點一一對應喔!)

1.方法startCreateRandomPipesAction()    開始重複創建水管的動作方法

2.方法stopCreateRandomPipesAction()     停止創建水管的動作方法    

3.方法createRandomPipes()    具體某一次創建一對水管方法,在此方法裏計算上下水管大小隨機數

4.方法addPipes(topSize: CGSize, bottomSize: CGSize)  添加一對水管到場景裏,這個方法有兩個參數分別是上水管和下水管的大小,在此方法裏僅僅做的是創建兩個SKSpriteNode對象,然後將他們加到場景裏

5.方法removeAllPipesNode()  移除所有正在場景裏的水管

我們一個方法一個方法的來

首先添加下面addPipes(topSize: CGSize, bottomSize: CGSize)方法到GameScene裏面

func addPipes(topSize: CGSize, bottomSize: CGSize) {

        //創建上水管

        let topTexture = SKTexture(imageNamed: "topPipe")      //利用上水管圖片創建一個上水管紋理對象

        let topPipe = SKSpriteNode(texture: topTexture, size: topSize)  //利用上水管紋理對象和傳入的上水管大小參數創建一個上水管對象

        topPipe.name = "pipe"   //給這個水管取個名字叫pipe

        topPipe.position = CGPoint(x: self.size.width + topPipe.size.width * 0.5, y: self.size.height - topPipe.size.height * 0.5) //設置上水管的垂直位置爲頂部貼着屏幕頂部,水平位置在屏幕右側之外



        //創建下水管,每一句方法都與上面創建上水管的相同意義

        let bottomTexture = SKTexture(imageNamed: "bottomPipe")

        let bottomPipe = SKSpriteNode(texture: bottomTexture, size: bottomSize)

        bottomPipe.name = "pipe"

        bottomPipe.position = CGPoint(x: self.size.width + bottomPipe.size.width * 0.5, y: floor1.size.height + bottomPipe.size.height * 0.5)  //設置下水管的垂直位置爲底部貼着地面的頂部,水平位置在屏幕右側之外



        //將上下水管添加到場景裏

        addChild(topPipe)

        addChild(bottomPipe)

}

現在你有個一個helper方法可以添加兩個真實的水管到場景裏了,我們繼續講下面createRandomPipes()方法代碼添加到GameScene裏面

func createRandomPipes() {

        //先計算地板頂部到屏幕頂部的總可用高度

        let height = self.size.height - self.floor1.size.height

        //計算上下管道中間的空檔的隨機高度,最小爲空檔高度爲2.5倍的小鳥的高度,最大高度爲3.5倍的小鳥高度

        let pipeGap = CGFloat(arc4random_uniform(UInt32(bird.size.height))) + bird.size.height * 2.5

        //管道寬度在60

        let pipeWidth = CGFloat(60.0)

        //隨機計算頂部pipe的隨機高度,這個高度肯定要小於(總的可用高度減去空檔的高度)

        let topPipeHeight = CGFloat(arc4random_uniform(UInt32(height - pipeGap)))

         //總可用高度減去空檔gap高度減去頂部水管topPipe高度剩下就爲底部的bottomPipe高度

        let bottomPipeHeight = height - pipeGap - topPipeHeight

        //調用添加水管到場景方法

        addPipes(topSize: CGSize(width: pipeWidth, height: topPipeHeight), bottomSize: CGSize(width: pipeWidth, height: bottomPipeHeight))

}

現在我們只要調用一次這個createRandomPipes()方法,就能真的創建一個一堆隨機的上下水管並且把他們添加到場景裏面了!

創建隨機數通常使用以下兩個方法

arc4random() -> UInt32 

這個方法會隨機牀身給一個無符號Int32以內的整數

arc4random_uniform(_ __upper_bound: UInt32) -> UInt32

這個方法比上面那個方法多一個參數,這個參數就是設置這個能產生隨機數的最大值,也就是限定了一個範圍


PS:可以看到我們在這個方法裏面計算了好幾個隨機數,最後的目的就是爲了計算出上下水管的大小。這裏具體的隨機數的大小範圍是可以根據你自己的喜好更改的!比如上下水管的空檔隨機高度,如果你想遊戲容易一點就讓這個隨機數最小值變大一點,如果你想遊戲難一點就讓隨機數最小值變小。另外我們水管的寬度是寫死60,你也可以讓這個寬度也是一個隨機數。。。


 

現在我們能創建一對水管了,那我想重複創建該怎麼辦呢?那就需要將下面這個方法startCreateRandomPipesAction()添加到GameScene

func startCreateRandomPipesAction() {

        //創建一個等待的action,等待時間的平均值爲3.5秒,變化範圍爲1秒

        let waitAct = SKAction.wait(forDuration: 3.5, withRange: 1.0)  

       //創建一個產生隨機水管的action,這個action實際上就是調用一下我們上面新添加的那個createRandomPipes()方法

        let generatePipeAct = SKAction.run {  

                self.createRandomPipes()

        }

        //讓場景開始重複循環執行"等待" -> "創建" -> "等待" -> "創建"。。。。。

        //並且給這個循環的動作設置了一個叫做"createPipe"的key來標識它

        run(SKAction.repeatForever(SKAction.sequence([waitAct, generatePipeAct])), withKey: "createPipe")

}

現在我們只要調用一次startCreateRandomPipesAction()方法後,場景就會每隔一段時間就創建一堆水管添加到場景裏了。那我們應該在哪裏調用這個方法呢?明顯是在startGame()遊戲開始方法裏啦

所以在startGame()方法裏面最後加上下面這一句

startCreateRandomPipesAction()  //開始循環創建隨機水管

既然有個開始循環創建,那麼就把停止循環創建的方法也加進來吧,添加下面stopCreateRandomPipesAction()方法到GameScene裏

func stopCreateRandomPipesAction() {

        self.removeAction(forKey: "createPipe")

}

可以看到這個方法很簡單,僅僅是通過一個action的key將場景的重複創建水管的action移除掉即可。

接下來我我們在gameOver()方法裏最後添加上下面這一句,就能讓遊戲結束的時候也停止創建水管了

stopCreateRandomPipesAction()

還有最後一個方法要添加的就是移除掉場景裏的所有水管,添加下面方法到GameScene

func removeAllPipesNode() {

        for pipe in self.children where pipe.name == "pipe" {  //循環檢查場景的子節點,同時這個子節點的名字要爲pipe

                pipe.removeFromParent()  //將水管這個節點從場景裏移除掉

        }

}

然後我們在shuffle()方法裏的gameStatus = . idle後面加上下面這一句,這樣我們就能在每一局新開始初始換的時候將上一句可能殘留在場景裏的舊水管清空

removeAllPipesNode()

好的!現在我們運行一下我們的遊戲,記得遊戲一開始是初始化狀態,要點擊一下屏幕纔會遊戲開始,看到了麼每隔幾秒就會有一對水管天添加到場景裏

等等!!!說好的水管呢????沒有看到呀!!!!!

沒錯你肯定看不到,因爲你記得我們創建了兩個水管SpriteNode之後把他們的位置放在哪裏麼?我們把他們放在了屏幕右側之外了,你當然看不到啦。但是雖然你看不到你也知道它已經在場景了!注意看右下角那個黑色小條的內容node 和 fps,這是方便我們調試時候用的,顯示遊戲場景裏的實時的node數量和刷新率,最開始node是4,當你點擊了一下屏幕遊戲開始了之後,每隔幾秒node就會加2,這個2就是我們的上下水管了!

所以我們還要讓水管動起來,找到之前寫的moveScene()方法,在移動地面代碼後面加上下面的代碼

//循環檢查場景的子節點,同時這個子節點的名字要爲pipe

for pipeNode in self.children where pipeNode.name == "pipe" { 

        //因爲我們要用到水管的size,但是SKNode沒有size屬性,所以我們要把它轉成SKSpriteNode

        if let pipeSprite = pipeNode as? SKSpriteNode { 

                //將水管左移1

                pipeSprite.position = CGPoint(x: pipeSprite.position.x - 1, y: pipeSprite.position.y)

                //檢查水管是否完全超出屏幕左側了,如果是則將它從場景裏移除掉

                if pipeSprite.position.x < -pipeSprite.size.width * 0.5 {

                      pipeSprite.removeFromParent()

               }

        }

}

因爲moveScene()方法會在遊戲進行中時,每一幀更新的update()方法裏調用,所以你現在你再運行程序就會看到了水管跟着地面一起往左邊移動了!

 

 

5.物理世界

 

到此我們已經完成了這個遊戲很大一部分了,但是這個遊戲還有最重要一部分現在纔出場,這就是模擬物理世界!

可以看到我們現在運行程序,小鳥沒有收到重力作用,不會下墜,點擊屏幕小鳥也不會向上飛,小鳥碰到水管也不會死掉,這就是因爲缺少了物理世界的模擬。

我覺得物理的模擬是遊戲引擎很重要的一個功能,它給了遊戲的玩法和開發更多的可能性。那麼什麼是模擬物理世界?

比如你可以把一個場景當成我們生活的真實物理環境,裏面會有重力,會有磁場會有引力場等等。場景裏面的物理體會受各種場的影響,還能跟其他物理體有交互,比如物理體直接碰撞了會互相彈開,物理體有自己的質量密度體積等等。是不是很神奇!而且這些物理的計算完全有遊戲引擎做好了,你只要會用就行了!

我們這個遊戲其實用不到多複雜的物理模擬,僅僅是場景裏會有重力,小鳥會受到重力影響自由落體,然後小鳥會跟水管和地面產生碰撞,整個場景有個邊界,小鳥不能一直往上飛出屏幕。

 

配置場景的物理體

找到didMove()方法,在設置場景背景色代碼後面加上下面內容

// Set Scene physics

self.physicsBody = SKPhysicsBody(edgeLoopFrom: self.frame)  //給場景添加一個物理體,這個物理體就是一條沿着場景四周的邊,限制了遊戲範圍,其他物理體就不會跑出這個場景

self.physicsWorld.contactDelegate = self //物理世界的碰撞檢測代理爲場景自己,這樣如果這個物理世界裏面有兩個可以碰撞接觸的物理體碰到一起了就會通知他的代理

加完這兩句之後你會發現第二句代碼報錯了!那是因爲你讓GameScene成爲了物理場景的碰撞檢測代理,但是你並沒有遵守這個代理的協議,所以趕快讓GameScene這個類遵守下面這個協議吧

SKPhysicsContactDelegate

class GameScene: SKScene,SKPhysicsContactDelegate {
    ...
}

現在就不會報錯了,你可以看到GameScene因爲是繼承自SKScene,SKScene是自帶了個物理世界的,有興趣你現在也可以試試打印一下當前物理世界的重力看看 -> print(self.physicsWorld.gravity),結果是不是(x:0,y:-9.8),表示重力是沿着屏幕向下的方向,重力大小是9.8,是不是跟高中物理學的是一樣的呢!

最後先做一個準備工作,在GameScene類的外面加上下面內容

let birdCategory: UInt32 = 0x1 << 0

let pipeCategory: UInt32 = 0x1 << 1

let floorCategory: UInt32 = 0x1 << 2

設置三個常量來表示小鳥、水管和地面物理體,稍後我們後面會用到

 

配置地面物理體

找到didMove()方法,在添加地面的帶面後面加上下面內容

//配置地面1的物理體

floor1.physicsBody = SKPhysicsBody(edgeLoopFrom: CGRect(x: 0, y: 0, width: floor1.size.width, height: floor1.size.height))

floor1.physicsBody?.categoryBitMask = floorCategory

//配置地面2的物理體

floor2.physicsBody = SKPhysicsBody(edgeLoopFrom: CGRect(x: 0, y: 0, width: floor2.size.width, height: floor2.size.height))

floor2.physicsBody?.categoryBitMask = floorCategory

這裏要說明的是物理體的categoryBitMask,這個用來表示當前物理體是哪一個物理體,我們用我們剛剛準備好的floorCategory來表示他,等會碰撞檢測的時候需要通過這個來判斷。

 

配置小鳥物理體

找到didMove()方法,在添加小鳥的代碼後面,shuffle()方法前面加入下面代碼

bird.physicsBody = SKPhysicsBody(texture: bird.texture!, size: bird.size)

bird.physicsBody?.allowsRotation = false  //禁止旋轉

bird.physicsBody?.categoryBitMask = birdCategory //設置小鳥物理體標示

bird.physicsBody?.contactTestBitMask = floorCategory | pipeCategory  //設置可以小鳥碰撞檢測的物理體

上面我們就設置好了小鳥的物理體了,contactTestBitMask是來設置可以與小鳥碰撞檢測的物理體,我們設置了地面和水管,所以通常物理體的categoryBitMask用二進制移位方式來表示,這樣在設置contactTestBitMask的時候就可以直接多個移位的標識做按位取或的運算即可

 

配置水管物理體

找到addPipes(topSize: CGSize, bottomSize: CGSize)方法,在addChild(topPipe),addChild(bottomPipe)代碼之前加入下面的代碼內容

//配置上水管物理體

topPipe.physicsBody = SKPhysicsBody(texture: topTexture, size: topSize)

topPipe.physicsBody?.isDynamic = false

topPipe.physicsBody?.categoryBitMask = pipeCategory

//配置下水管物理體

bottomPipe.physicsBody = SKPhysicsBody(texture: bottomTexture, size: bottomSize)

bottomPipe.physicsBody?.isDynamic = false

bottomPipe.physicsBody?.categoryBitMask = pipeCategory

選在我們來運行一下游戲吧,你可以看到遊戲一開始在初始化狀態小鳥就受到重力的影響而掉到地面上了,這不是我們想要的,我們希望是玩家點擊了屏幕遊戲開始了小鳥纔會下落

那麼請在shuffle()方法裏,設置小鳥的position的代碼後面加上下面這句

bird.physicsBody?.isDynamic = false

然後再在startGame()方法裏,開始創建水管代碼之前加上下面這句

bird.physicsBody?.isDynamic = true

isDynamic的作用是設置這個物理體當前是否會受到物理環境的影響,默認是true,我們在遊戲初始化的時候設置小鳥不受物理環境影響,但是在遊戲開始的時候纔會受到物理環境的影響

現在再運行遊戲就可以看到初始化的時候小鳥停在屏幕中間,點擊了屏幕遊戲開始了,小鳥纔會掉下來

 

給小鳥一個速度

現在這遊戲簡直就是沒法玩,小鳥一下就掉到地上,怎麼點屏幕他都不會網上飛

現在找到touchesBegan()方法,看到這個寫好的switch語句裏,.running情況只有一句print("給小鳥一個向上的力"),打印一句話可不會讓小鳥往上飛,現在請將這句print替換爲下面這句代碼

bird.physicsBody?.applyImpulse(CGVector(dx: 0, dy: 20))

這個句代碼可以給小鳥的物理體施加一個向上的衝量,讓小鳥獲得一定的向上速度,但是由於小鳥還受重力影響,所以你得經常點擊屏幕才能保持小鳥不掉下去。

Impluse是什麼?Impulse在物理上就是衝量的意思,衝量=質量 * (結束速度 - 初始速度),即I = m * (v2 - v1),如果物體的質量爲1,那麼衝量i = v2 - v1。當一個質量爲1的物理體applyImpulse(CGVector(dx: 0, dy: 20))的意思就是讓他在y的方向上疊加20m/s的速度。當然如果物理體質量m不爲1,那疊加的速度就不是剛好等於衝量的字面量了,而是要除以m了。如一個質量爲2的物理體同樣applyImpulse(CGVector(dx: 0, dy: 20)),結果就是它在y的方向上疊加了10m/s的一個速度

檢測碰撞

現在我們的遊戲已經基本能玩了,但是小鳥碰到水管或者掉到地面上小鳥沒有死掉,遊戲還在繼續,現在我們就來完善這個問題

記得我們將當前的GameScene設置爲了物理世界的碰撞檢測的代理麼?接下來我們只要實現檢測到碰撞產生的代理方法即可

在GameScene裏添加下面這個方法代碼,didBegin()會在當前物理世界有兩個物理體碰撞接觸了則回調用,這兩個碰撞了的物理體的信息都在contact這個參數裏面,分別是bodyA和bodyB

func didBegin(_ contact: SKPhysicsContact) {

        //先檢查遊戲狀態是否在運行中,如果不在運行中則不做操作,直接return

        if gameStatus != .running { return }

      //爲了方便我們判斷碰撞的bodyA和bodyB的categoryBitMask哪個小,小的則將它保存到新建的變量bodyA裏的,大的則保存到新建變量bodyB裏

        var bodyA : SKPhysicsBody

        var bodyB : SKPhysicsBody

        if contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask {

            bodyA = contact.bodyA

            bodyB = contact.bodyB

       }else {

            bodyA = contact.bodyB

            bodyB = contact.bodyA

       }

       接下來判斷bodyA是否爲小鳥,bodyB是否爲水管或者地面,如果是則遊戲結束,直接調用gameOver()方法

       if (bodyA.categoryBitMask == birdCategory && bodyB.categoryBitMask == pipeCategory) ||

           (bodyA.categoryBitMask == birdCategory && bodyB.categoryBitMask == floorCategory) {

               gameOver()

       }

}

現在我們運行遊戲就可以正常玩耍了,小鳥碰到地面或者水管遊戲就會結束,小鳥就會落地,水管會停住,如果再點擊一次屏幕就會回到初始狀態,小鳥回到中間,殘留的水管都消失了

但是這個遊戲結束有點突兀,最好能給個提示告訴玩家遊戲結束了。

我們先給GameScene這個類添加一個變量,來表示遊戲結束提示的label

lazy var gameOverLabel: SKLabelNode = {

         let label = SKLabelNode(fontNamed: "Chalkduster")

         label.text = "Game Over"

         return label

}()

注意這個變量我們用了一個lazy來標示,標示這個label是懶加載的,也就是只有在gameOverLabel第一次被調用的時候纔會創建,它的創建代碼用一個大括號包住,結尾要帶一對()表示馬上執行意思。這樣我們就通過懶加載創建一個gameOverLabel,他的text內容Game Over提示語。

接下來找到gameOver()方法,在此方法的最後加上下面的代碼,這樣在gameOver的時候就會有一個提示語從天而降了

//禁止用戶點擊屏幕

isUserInteractionEnabled = false

//添加gameOverLabel到場景裏

addChild(gameOverLabel)

//設置gameOverLabel其實位置在屏幕頂部

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

//讓gameOverLabel通過一個動畫action移動到屏幕中間

gameOverLabel.run(SKAction.move(by: CGVector(dx:0, dy:-self.size.height * 0.5), duration: 0.5), completion: {

        //動畫結束才重新允許用戶點擊屏幕

        self.isUserInteractionEnabled = true

})

不過要記住在遊戲回到初始化狀態下的時候,要把gameOverLabel從場景裏移除掉,所以找到shuffle()方法,然後在removeAllPipesNode()方法後面加上下面這一句

gameOverLabel.removeFromParent()

現在我們再來運行一下游戲,就能發現一切正常了,可以愉快的玩耍了!

 

 

6.補充提示

雖然遊戲能玩了,但是你不覺得少點什麼麼?

沒錯遊戲一般都有分數,表示玩家這句玩的成績怎麼樣,所以這個遊戲裏我們可以添加一個表示玩家小鳥飛了多遠距離的提示。

我們先給GameScene添加一個metersLabel,用它來展示用戶走了多遠的距離,添加下面代碼到你的GameScene

lazy var metersLabel: SKLabelNode = {

        let label = SKLabelNode(text: "meters:0")

        label.verticalAlignmentMode = .top

        label.horizontalAlignmentMode = .center

        return label

}()

可以看到我們同樣使用了懶加載的方式來創建這個metersLabel變量

 


PS:這裏稍微多介紹一點SKLabelNode這個類,如果做過iOS應用開發的朋友應該都知道UILabel這個控件,跟UILabel類似SKLabelNode就是SpriteKit中顯示一段文字的空間,首先他是繼承自SKNode,所以它可以被添加到場景裏面,它也可以執行各種Action動作。

另外可能還有一個你不適應的地方就是他的位置佈局問題,在做iOS應用時候UILabel有大小,UILabel的原點在它自己左上角,你自然知道怎麼放置它了。但是SKLabelNode是沒有size這個屬性的,他的frame屬性也只是readonly的,這怎麼辦?

SKLabelNode有兩個新的屬性叫做verticalAlignmentMode和horizontalAlignmentMode,表示這個label在水平和垂直方向上如何佈局,他們是枚舉類型。比如你把的SKLabelNode的postion位置設置在(50,100)這個點,然後把他的verticalAlignmentMode 設置爲.top,則表示這段文字的頂部是position所在位置的y的水平高度上,如果設置爲.bottom,則這段文字的底部水平線高度就是position的y的水平高度。所以horizontalAlignmentMode屬性也是同理,只是它是設置水平方向上的佈局。可能等我遲點補充一個圖表示會比較清晰,容易理解


 

現在我們有了這個label的變量,要將他加到場景上

找到didMove()方法,然後在設置場景的物理體的代碼後面加上下面的代碼內容

// Set Meter Label

metersLabel.position = CGPoint(x: self.size.width * 0.5, y: self.size.height)

metersLabel.zPosition = 100

addChild(metersLabel)

我們把metersLabel放在了屏幕的頂部中間,然後注意第二句metersLabel.zPosition = 100,我們把label在z軸上的位置設置在了100,你可能會問這不是2D遊戲麼怎麼會有z軸,這裏的z軸你也可以理解爲圖層的層次順序軸,zPosition越大就越靠近玩家,就是說如果兩個場景裏的node某一部分重疊了,那麼就是zPosition大的那個node會覆蓋住小的那個node,zPosition默認值是0,如果兩個都是0的node重疊了那就要看誰是先被添加進場景的,先被添加進的會被後添加進的覆蓋住。

那麼爲什麼metersLabel要設置一個大一些的zPosition?因爲metersLabel是在didMove方法裏就添加到場景了,我們又希望它始終不被遮住,但是那些出現的水管是後添加進場景的node,他們移動到metersLabel上面的時候就會覆蓋住它,所以我們纔要做這樣的一個操作。

 

現在我們有一個用來顯示小鳥飛了多遠的label了,該要讓它顯示變化的值了

我們給GameScene添加多一個記錄飛行米數的變量,添加下面代碼到GameScene

var meters = 0 {

    didSet  {

         metersLabel.text = "meters:\(meters)"

    }

}

meters是一個Int值就可以了,初始設置爲0,可以看到我們寫了個didSet{...},表示這個變量每次當被設置了一個新的值就會執行一次didSet裏面的代碼,我們在這裏重新設置了一個metersLabel現實的內容。

接下來我們要在遊戲運行時候不斷增加meters的值,簡單點的方法就是在每一幀刷新的update()方法裏去改變

我找到update()方法,然後添加下面的內容到方法裏

if gameStatus == .running {

      meters += 1

}

現在你運行遊戲就會看到一旦點擊一下屏幕遊戲開始了,飛行的米數就會不斷的刷刷刷的飛漲。

但是還有一件事情別忘了,就是找到shuffle()方法,在裏面添加一句下面的代碼,每次回到遊戲初始化狀態下時,要把上一局的飛行米數重新清零

meters = 0

 

現在你再運行遊戲就會得到跟文章開篇時候的動圖一樣的效果了!

 

 

7.還有什麼可以完善的?

至此對於此遊戲的基本實現算是寫完了,不過你也可以繼續完善這個遊戲,或者用不同的方法來實現試試

1.比如說這裏我們的場景內容移動(地面和水管)是直接在update()方法裏改變position來實現,那麼能不能換成用SKAction的方法來做到呢?

2.雖然遊戲能玩了,但是那些會影響到遊戲的一些關鍵參數是否已經是最優的選擇了?如果你覺得小鳥的自由落體下墜的太快或者每次小鳥上升的速度太小等等,這些可能都要開發者自己去玩玩嘗試找到最優的參數配置

3.是否可以增加漸進的難度?比如說隨機的產生水管的間隔時間能不能隨遊戲進行時間越來越短?等等

4.是否小鳥可以能吃到一些道具讓它在一定時間內不懼怕水管?

5.是否可以添加玩家成績的記錄?

等等等等。。。。。這些就看你想不想去完善了試一試,這裏就不做一一實現了,謝謝

 

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