由于很多小伙伴要demo我就不一一發(fā)了棘街,直接丟在github上自己下載吧:https://github.com/sideslash/FlappyBird
最近利用業(yè)余時間根據(jù)官方文檔和網(wǎng)上的資料學(xué)習(xí)了蘋果官方推出的2D游戲開發(fā)引擎Spritekit基本知識,模仿做了一個前兩年火了一火的小游戲flappy bird練練手承边,現(xiàn)在就來一步一步講講這個游戲我的實現(xiàn)方法遭殉。
因為Apple推行Swift開發(fā)語言,Swift也將是以后iOS方面開發(fā)的主力語言炒刁,所有這篇實例我們也就先拋棄Objective-C,而使用Swift3開發(fā)語言恩沽,如果你還不熟悉Swift的基本語法那趕快去學(xué)學(xué)吧誊稚,如果你已經(jīng)了解了那么就跟著我繼續(xù)吧翔始!
先看看最后最后做完的樣子
1.準備工作
新建工程
先新建一個工程項目,模板選擇Game
語言選擇Swift里伯,開發(fā)庫選擇SpriteKit
刪除示例文件和代碼
然后我們的工程就建立好了城瞎。接著我們就先把那些Xcode自動創(chuàng)建的示例文件和代碼都刪除掉,先看文件目錄欄疾瓮,把GameScene.sks和Actions.sks兩個文件刪除掉脖镀,然后進入Assets.xcassets把里面的那張飛機圖片刪除掉。這樣我們就把用不到的文件都刪掉了狼电,接下來繼續(xù)刪除沒用的代碼
首先進入GameViewController.swift文件蜒灰,找到那個viewDidLoad()方法弦蹂,看到里面下面內(nèi)容。
注意看這一句
if let scene = SKScene(fileNamed: "GameScene") ?{
........
}
這一句是通過一個GameScene的sks文件來創(chuàng)建一個場景實例對象强窖,由于咱們剛剛把GameScene.sks文件刪除了凸椿,所以我們現(xiàn)在是創(chuàng)建不出來場景的,所以我們需要把viewDidLoad()里面的代碼改成下面的內(nèi)容
super.viewDidLoad()
if let view = self.view as! SKView? {
? ? let scene = GameScene(size: view.bounds.size) ?//通過代碼創(chuàng)建一個GameScene類的實例對象
? ? scene.scaleMode = .aspectFill
? ? view.presentScene(scene)
? ? view.ignoresSiblingOrder = true
? ? view.showsFPS = true
? ? view.showsNodeCount = true
}
現(xiàn)在我們就把通過sks文件創(chuàng)建場景對象改成了通過代碼直接創(chuàng)建一個叫做GameScene類的實例對象了翅溺。
到這里我們的GameViewController文件就改完了脑漫。接下來我們進入GameScene.swift我們最終要的場景類的文件看一看
。咙崎。优幸。。我勒個去褪猛。网杆。。握爷。你會發(fā)現(xiàn)Xcode給我們自動添加了這么多示例的代碼跛璧,然并卵,都刪掉新啼!刪到跟下圖一樣只留下didMove()和update()兩個空方法即可追城!
我們在didMove方法里先加上一句代碼,設(shè)置場景的背景色為淡藍色燥撞,現(xiàn)在我們就可以運行一下程序看看顯示的是不是一個淡藍色的界面
self.backgroundColor = SKColor(red: 80.0/255.0, green: 192.0/255.0, blue: 203.0/255.0, alpha: 1.0)
didMove()方法會在當前場景被顯示到一個view上的時候調(diào)用座柱,你可以在里面做一些初始化的工作
這樣看來一切正常,我們自己的場景終于顯示在玩家面前了物舒。
導(dǎo)入資源文件
我自己選用了3張小鳥的png圖片色洞,一張翅膀上抬、一張翅膀放平冠胯、一張翅膀下墜火诸,這樣我們一會就可以做出小鳥在飛的效果。圖片大小都是50*43荠察,你也可以自己在網(wǎng)上找?guī)讖堫愃频膱D片來使用置蜀,尺寸別太大,雖然你可以通過代碼改變小鳥的小大悉盆,但是如果你的圖片本身很大盯荤,你實際需要它顯示的比較小,那么對性能其實有點浪費焕盟,不過對于這種小游戲來說你想怎么弄都沒問題的秋秤。
PS:稍后如果我把我的工程放上網(wǎng)你們也可以直接下載我的工程,直接用里面的圖片素材
導(dǎo)入圖片注意:先新建一個叫player.atlas的文件夾,然后我們把這三張圖片放到這個文件夾下灼卢,然后再將這個文件夾拖到工程里面绍哎,注意要勾選copy item if need。
為什么要這樣做鞋真?
因為當你把一類相關(guān)的貼圖圖片素材放在一個.atlas文件夾里蛇摸,編譯程序的時候Xcode會把這個文件夾里的圖片都導(dǎo)入“紋理圖集”里,相對于只用獨立的圖片文件而言灿巧,使用紋理圖集會非常顯著地提升游戲的渲染性能
然后我們再將另外三張圖片丟入工程的Asserts.xcasserts里即可赶袄,分別是地面(floor),上水管(topPipe)和下水管(bottomPipe)
至此準備工作全部完成,我們終于可以開始敲代碼了抠藕!
2.布置場景和游戲狀態(tài)
PS:由于這個游戲比較小也不復(fù)雜饿肺,所以咱們也就不設(shè)計什么高級的開發(fā)模式來開發(fā)這個游戲了,全部的布局邏輯代碼全部都寫在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后面然后繼續(xù)向左移動,如此循環(huán)下去。
我將anchorPoint設(shè)置為(0,0),即SpriteNode的左下角的點作為這個node的錨點欲主,是為了方便定位floor,如果不熟悉錨點是什么的朋友趕快去搜一搜炫惩!
SKScene場景的默認錨點為(0,0)即左下角,SKSpriteNode的默認錨點為(0.5,0.5)即它的中心點阿浓。
另外SpriteKit的坐標系是向右x增加他嚷,向上y增加。而不像做iOS應(yīng)用開發(fā)時候UIKit是向右x增加芭毙,向下y增加筋蓖!
現(xiàn)在讓我們運行一下程序就可以看到我們的地面出現(xiàn)了!
放置小鳥
我們來講我們的游戲主角小鳥顯示出來稿蹲,同樣給GameScene類增加一個小鳥的變量
var bird: SKSpriteNode!
然后在didMove()方法里扭勉,在添加floor的后面添加下面代碼
bird = SKSpriteNode(imageNamed: "player1")
addChild(bird)
這樣我們就將我們的主角小鳥添加到場景上了鹊奖。等等苛聘!你還沒給小鳥設(shè)置position呢,不是應(yīng)該把小鳥放到屏幕中間開始么?
游戲狀態(tài)
沒錯设哗,但是在我們設(shè)置它位置之前唱捣,我們先構(gòu)想一下我們這個游戲整個運行的流程:
1.一開始小鳥在屏幕中間飛,地面也在移動网梢,但是這個時候還沒有真的開始震缭,所以還不會有水管出現(xiàn)。
2.當玩家準備好了點了一下屏幕战虏,游戲正式開始拣宰,小鳥會受重力作用往下墜落,水管開始出現(xiàn)烦感,此時玩家每點擊一次屏幕小鳥就有會受一次上升的力巡社。
3.如果小鳥碰到水管或者小鳥碰到地面了,則游戲結(jié)束手趣,小鳥停止飛的動作晌该,場景里的水管和地面都停住不動。此時玩家再點擊屏幕則回到上面1初始狀態(tài)绿渣。
可以看到朝群,玩家的操作和場景內(nèi)容的移動與否都與當前游戲的進程狀態(tài)有關(guān)系,我們也可以看出有三個狀態(tài):1初始狀態(tài) 2游戲進行中狀態(tài) 3游戲結(jié)束狀態(tài)
那我們現(xiàn)在GameScene類里面定義一個枚舉來表示不同的狀態(tài)中符,同時給GameScene增加一個游戲狀態(tài)的變量
enum GameStatus {
? ? case idle ? ?//初始化
? ? case running ? ?//游戲運行中
? ? case over ? ?//游戲結(jié)束
}
var gameStatus: GameStatus = .idle ?//表示當前游戲狀態(tài)的變量姜胖,初始值為初始化狀態(tài)
現(xiàn)在我們知道了整個游戲會有三個進程狀態(tài),那么我們就給GameScene增加三個對應(yīng)的方法淀散,分別來處理這個三個狀態(tài)谭期。
func shuffle() ?{?
//游戲初始化處理方法
gameStatus = .idle
}
func startGame() ?{?
//游戲開始處理方法
gameStatus = .running
}
func gameOver() ?{
//游戲結(jié)束處理方法
gameStatus = .over
}
可以看到目前我們只在這三個方法里分別修改了當前游戲的進程狀態(tài)變量。
接下來大家再想想上面那個沒解決的問題吧凉,設(shè)置小鳥初始化的位置該放在那里呢隧出?當然是初始化shuffle()方法里啦,添加下面代碼內(nèi)容到shuffle()方法里
bird.position = CGPoint(x: self.size.width * 0.5, y: self.size.height * 0.5)
那么我們應(yīng)該什么時候來調(diào)用這三個方法呢阀捅?
首先在場景初始化完成的時候胀瞪,肯定要先調(diào)用一下shuffle()初始化,所有我們在didMove()方法里的最后面添加一句
shuffle()
然后再給GameScene添加下面這個方法
override func touchesBegan(_ touches: Set, with event: UIEvent?) {
? ? switch gameStatus {
? ? ? ?case .idle:
? ? ? ? ? startGame() ?//如果在初始化狀態(tài)下饲鄙,玩家點擊屏幕則開始游戲
? ? ? ?case .running:
? ? ? ? ? print("給小鳥一個向上的力") ? //如果在游戲進行中狀態(tài)下凄诞,玩家點擊屏幕則給小鳥一個向上的力(暫時用print一句話代替)
? ? ? case .over:
? ? ? ? ?shuffle() ?//如果在游戲結(jié)束狀態(tài)下,玩家點擊屏幕則進入初始化狀態(tài)
? ? ? }
}
touchesBegan()是SKScene自帶的系統(tǒng)方法忍级,當玩家手指點擊到屏幕上的時候會調(diào)用帆谍,可以看到我們用switch語句來處理了三種不同的游戲狀態(tài)下,玩家點擊屏幕后做出的不同響應(yīng)
現(xiàn)在讓我們來運行一下程序轴咱,可以看到小鳥也正常的出現(xiàn)在屏幕中間了
3.讓內(nèi)容動起來
我們目前可以看到雖然我們看到了小鳥和地面汛蝙,但是怎么都是死的烈涮,這也太假了,那么接下來我們要讓他們都動起來窖剑,讓小鳥好像真的在飛
移動地面
我們先來移動地面坚洽,我們給GameScene添加一個叫做moveScene()的方法,用來使場景內(nèi)的物體向左移動起來西土,暫時我們先讓地面移動讶舰,稍后還會在這個方法里添加讓水管移動的代碼
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是否已經(jīng)完全超出屏幕的左邊需了,超出的floor則移動到另一個floor的右邊跳昼。
那我們該什么時候調(diào)用這個方法呢?我們可以在update()方法里調(diào)用moveScene()方法肋乍。
還記得update()方法么庐舟?我們最開始留下的兩個空方法,一個是didMove()另一個就是update()呀住拭!
update()方法為SKScene自帶的系統(tǒng)方法挪略,在畫面每一幀刷新的時候就會調(diào)用一次
那么就在update()方法里添加一下內(nèi)容代碼
if gameStatus != .over {
? ? moveScene()
}
如果當前游戲狀態(tài)不是結(jié)束的,則每次調(diào)用update()的時候都調(diào)用moveScene()方法滔岳,回想一下我們上面提高的游戲流程是不是應(yīng)該這樣呢杠娱?
運行一下程序,看我們的地面是不是東西來谱煤,就想鳥在向右飛一樣
小鳥動起來
現(xiàn)在我們讓鳥也飛起來吧摊求!
先給GameScene添加兩個新的方法,一個是讓小鳥開始飛刘离,一個是讓小鳥停止飛(游戲結(jié)束室叉,小鳥墜地了就要停止飛)
//開始飛
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紋理對象,他們四個連起來就是小鳥的翅膀從上->中->下->中這樣一個循環(huán)過程
然后用這一組紋理創(chuàng)建了一個飛的動作(flyAction)硫惕,同時設(shè)置紋理的變化時間為0.15秒
然后讓小鳥重復(fù)循環(huán)執(zhí)行這個飛的動作茧痕,同時給這個動作使用了一個叫"fly"的key來標識
在birdStopFly()方法里只有一句代碼,就是把fly這個動作從小鳥身上移除掉
接下來我們分別在shuffle()方法里添加一句讓小鳥開始飛恼除,
birdStartFly()
在gameOver()方法里添加一句讓小鳥停止飛
birdStopFly()
現(xiàn)在運行程序就能看到小鳥像是真的在往右邊飛踪旷!
4.隨機創(chuàng)造水管
現(xiàn)在我們地面有了,小鳥也有了豁辉,該要讓水管上場了令野。
我們先想想水管出現(xiàn)有什么特點
1.成對的出現(xiàn),一個在上一個在下徽级,上下兩個水管中間留有一定的高度的距離讓小鳥能通過
2.上下水管之間的高度距離是隨機的气破,但是有個最小值和最大值
3.一對水管出現(xiàn)之后向左移動,移動出了屏幕左側(cè)就要把它移除掉
4.一對水管出現(xiàn)之后餐抢,間隔一定的時間现使,再產(chǎn)生另一對水管低匙,間隔的時間也是隨機數(shù),也要設(shè)一個最大和最三小值
5.在游戲初始化狀態(tài)下要停止重復(fù)創(chuàng)建水管朴下,同時要移除掉場景里上一句殘留的水管。在游戲進行中狀態(tài)下才重復(fù)創(chuàng)建水管苦蒿。在游戲結(jié)束狀態(tài)下殴胧,停止創(chuàng)建水管,如果場景里還有存在水管佩迟,則停止左移
那么我準備了四個方法來實現(xiàn)水管功能(5個方法不是跟上面5個特點一一對應(yīng)喔M爬摹)
1.方法startCreateRandomPipesAction()? ? 開始重復(fù)創(chuàng)建水管的動作方法
2.方法stopCreateRandomPipesAction() ? ? 停止創(chuàng)建水管的動作方法 ? ?
3.方法createRandomPipes() ? ?具體某一次創(chuàng)建一對水管方法,在此方法里計算上下水管大小隨機數(shù)
4.方法addPipes(topSize: CGSize, bottomSize: CGSize) ?添加一對水管到場景里报强,這個方法有兩個參數(shù)分別是上水管和下水管的大小灸姊,在此方法里僅僅做的是創(chuàng)建兩個SKSpriteNode對象,然后將他們加到場景里
5.方法removeAllPipesNode() ?移除所有正在場景里的水管
我們一個方法一個方法的來
首先添加下面addPipes(topSize: CGSize, bottomSize: CGSize)方法到GameScene里面
func addPipes(topSize: CGSize, bottomSize: CGSize) {
? ? ? ? //創(chuàng)建上水管
? ? ? ? let topTexture = SKTexture(imageNamed: "topPipe") ? ? ?//利用上水管圖片創(chuàng)建一個上水管紋理對象
? ? ? ? let topPipe = SKSpriteNode(texture: topTexture, size: topSize) ?//利用上水管紋理對象和傳入的上水管大小參數(shù)創(chuàng)建一個上水管對象
? ? ? ? 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) //設(shè)置上水管的垂直位置為頂部貼著屏幕頂部秉溉,水平位置在屏幕右側(cè)之外
? ? ? ? //創(chuàng)建下水管力惯,每一句方法都與上面創(chuàng)建上水管的相同意義
? ? ? ? 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: self.floor1.size.height + bottomPipe.size.height * 0.5) ?//設(shè)置下水管的垂直位置為底部貼著地面的頂部,水平位置在屏幕右側(cè)之外
? ? ? ? //將上下水管添加到場景里
? ? ? ? addChild(topPipe)
? ? ? ? addChild(bottomPipe)
}
現(xiàn)在你有個一個helper方法可以添加兩個真實的水管到場景里了召嘶,我們繼續(xù)講下面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
? ? ? ? //調(diào)用添加水管到場景方法
? ? ? ? addPipes(topSize: CGSize(width: pipeWidth, height: topPipeHeight), bottomSize: CGSize(width: pipeWidth, height: bottomPipeHeight))
}
現(xiàn)在我們只要調(diào)用一次這個createRandomPipes()方法甲喝,就能真的創(chuàng)建一個一堆隨機的上下水管并且把他們添加到場景里面了!
創(chuàng)建隨機數(shù)通常使用以下兩個方法
arc4random() -> UInt32?
這個方法會隨機床身給一個無符號Int32以內(nèi)的整數(shù)
arc4random_uniform(_ __upper_bound: UInt32) -> UInt32
這個方法比上面那個方法多一個參數(shù)铛只,這個參數(shù)就是設(shè)置這個能產(chǎn)生隨機數(shù)的最大值埠胖,也就是限定了一個范圍
PS:可以看到我們在這個方法里面計算了好幾個隨機數(shù),最后的目的就是為了計算出上下水管的大小淳玩。這里具體的隨機數(shù)的大小范圍是可以根據(jù)你自己的喜好更改的直撤!比如上下水管的空檔隨機高度,如果你想游戲容易一點就讓這個隨機數(shù)最小值變大一點蜕着,如果你想游戲難一點就讓隨機數(shù)最小值變小谊惭。另外我們水管的寬度是寫死60,你也可以讓這個寬度也是一個隨機數(shù)侮东。圈盔。。
現(xiàn)在我們能創(chuàng)建一對水管了悄雅,那我想重復(fù)創(chuàng)建該怎么辦呢驱敲?那就需要將下面這個方法startCreateRandomPipesAction()添加到GameScene
func startCreateRandomPipesAction() {
? ? ? ? //創(chuàng)建一個等待的action,等待時間的平均值為3.5秒,變化范圍為1秒
? ? ? ? let waitAct = SKAction.wait(forDuration: 3.5, withRange: 1.0) ?
? ? ? ?//創(chuàng)建一個產(chǎn)生隨機水管的action宽闲,這個action實際上就是調(diào)用一下我們上面新添加的那個createRandomPipes()方法
? ? ? ? let generatePipeAct = SKAction.run { ?
? ? ? ? ? ? ? ? self.createRandomPipes()
? ? ? ? }
? ? ? ? //讓場景開始重復(fù)循環(huán)執(zhí)行"等待" -> "創(chuàng)建" -> "等待" -> "創(chuàng)建"众眨。握牧。。娩梨。沿腰。
? ? ? ? //并且給這個循環(huán)的動作設(shè)置了一個叫做"createPipe"的key來標識它
? ? ? ? run(SKAction.repeatForever(SKAction.sequence([waitAct, generatePipeAct])), withKey: "createPipe")
}
現(xiàn)在我們只要調(diào)用一次startCreateRandomPipesAction()方法后,場景就會每隔一段時間就創(chuàng)建一堆水管添加到場景里了狈定。那我們應(yīng)該在哪里調(diào)用這個方法呢颂龙?明顯是在startGame()游戲開始方法里啦
所以在startGame()方法里面最后加上下面這一句
startCreateRandomPipesAction() ?//開始循環(huán)創(chuàng)建隨機水管
既然有個開始循環(huán)創(chuàng)建,那么就把停止循環(huán)創(chuàng)建的方法也加進來吧纽什,添加下面stopCreateRandomPipesAction()方法到GameScene里
func stopCreateRandomPipesAction() {
? ? ? ? self.removeAction(forKey: "createPipe")
}
可以看到這個方法很簡單措嵌,僅僅是通過一個action的key將場景的重復(fù)創(chuàng)建水管的action移除掉即可。
接下來我我們在gameOver()方法里最后添加上下面這一句芦缰,就能讓游戲結(jié)束的時候也停止創(chuàng)建水管了
stopCreateRandomPipesAction()
還有最后一個方法要添加的就是移除掉場景里的所有水管企巢,添加下面方法到GameScene
func removeAllPipesNode() {
? ? ? ? for pipe in self.children where pipe.name == "pipe" { ?//循環(huán)檢查場景的子節(jié)點,同時這個子節(jié)點的名字要為pipe
? ? ? ? ? ? ? ? pipe.removeFromParent() ?//將水管這個節(jié)點從場景里移除掉
? ? ? ? }
}
然后我們在shuffle()方法里的gameStatus = . idle后面加上下面這一句让蕾,這樣我們就能在每一局新開始初始換的時候?qū)⑸弦痪淇赡軞埩粼趫鼍袄锏呐f水管清空
removeAllPipesNode()
好的浪规!現(xiàn)在我們運行一下我們的游戲,記得游戲一開始是初始化狀態(tài)探孝,要點擊一下屏幕才會游戲開始罗丰,看到了么每隔幾秒就會有一對水管天添加到場景里
等等!T俟谩萌抵!說好的水管呢?元镀?绍填??沒有看到呀F芤伞L钟馈!S龈铩卿闹!
沒錯你肯定看不到,因為你記得我們創(chuàng)建了兩個水管SpriteNode之后把他們的位置放在哪里么萝快?我們把他們放在了屏幕右側(cè)之外了锻霎,你當然看不到啦。但是雖然你看不到你也知道它已經(jīng)在場景了揪漩!注意看右下角那個黑色小條的內(nèi)容node 和 fps旋恼,這是方便我們調(diào)試時候用的,顯示游戲場景里的實時的node數(shù)量和刷新率奄容,最開始node是4冰更,當你點擊了一下屏幕游戲開始了之后产徊,每隔幾秒node就會加2,這個2就是我們的上下水管了蜀细!
所以我們還要讓水管動起來舟铜,找到之前寫的moveScene()方法,在移動地面代碼后面加上下面的代碼
//循環(huán)檢查場景的子節(jié)點奠衔,同時這個子節(jié)點的名字要為pipe
for pipeNode in self.children where pipeNode.name == "pipe" {?
? ? ? ? //因為我們要用到水管的size谆刨,但是SKNode沒有size屬性,所以我們要把它轉(zhuǎn)成SKSpriteNode
? ? ? ? if let pipeSprite = pipeNode as? SKSpriteNode {?
? ? ? ? ? ? ? ? //將水管左移1
? ? ? ? ? ? ? ? pipeSprite.position = CGPoint(x: pipeSprite.position.x - 1, y: pipeSprite.position.y)
? ? ? ? ? ? ? ? //檢查水管是否完全超出屏幕左側(cè)了涣觉,如果是則將它從場景里移除掉
? ? ? ? ? ? ? ? if pipeSprite.position.x < -pipeSprite.size.width * 0.5 {
? ? ? ? ? ? ? ? ? ? ? pipeSprite.removeFromParent()
? ? ? ? ? ? ? ?}
? ? ? ? }
}
因為moveScene()方法會在游戲進行中時痴荐,每一幀更新的update()方法里調(diào)用血柳,所以你現(xiàn)在你再運行程序就會看到了水管跟著地面一起往左邊移動了官册!
5.物理世界
到此我們已經(jīng)完成了這個游戲很大一部分了,但是這個游戲還有最重要一部分現(xiàn)在才出場难捌,這就是模擬物理世界膝宁!
可以看到我們現(xiàn)在運行程序,小鳥沒有收到重力作用根吁,不會下墜员淫,點擊屏幕小鳥也不會向上飛,小鳥碰到水管也不會死掉击敌,這就是因為缺少了物理世界的模擬介返。
我覺得物理的模擬是游戲引擎很重要的一個功能,它給了游戲的玩法和開發(fā)更多的可能性沃斤。那么什么是模擬物理世界圣蝎?
比如你可以把一個場景當成我們生活的真實物理環(huán)境,里面會有重力衡瓶,會有磁場會有引力場等等徘公。場景里面的物理體會受各種場的影響,還能跟其他物理體有交互哮针,比如物理體直接碰撞了會互相彈開关面,物理體有自己的質(zhì)量密度體積等等。是不是很神奇十厢!而且這些物理的計算完全有游戲引擎做好了等太,你只要會用就行了!
我們這個游戲其實用不到多復(fù)雜的物理模擬蛮放,僅僅是場景里會有重力澈驼,小鳥會受到重力影響自由落體,然后小鳥會跟水管和地面產(chǎn)生碰撞筛武,整個場景有個邊界缝其,小鳥不能一直往上飛出屏幕挎塌。
配置場景的物理體
找到didMove()方法,在設(shè)置場景背景色代碼后面加上下面內(nèi)容
// Set Scene physics
self.physicsBody = SKPhysicsBody(edgeLoopFrom: self.frame) ?//給場景添加一個物理體内边,這個物理體就是一條沿著場景四周的邊榴都,限制了游戲范圍,其他物理體就不會跑出這個場景
self.physicsWorld.contactDelegate = self //物理世界的碰撞檢測代理為場景自己漠其,這樣如果這個物理世界里面有兩個可以碰撞接觸的物理體碰到一起了就會通知他的代理
加完這兩句之后你會發(fā)現(xiàn)第二句代碼報錯了嘴高!那是因為你讓GameScene成為了物理場景的碰撞檢測代理,但是你并沒有遵守這個代理的協(xié)議和屎,所以趕快讓GameScene這個類遵守下面這個協(xié)議吧
SKPhysicsContactDelegate
現(xiàn)在就不會報錯了拴驮,你可以看到GameScene因為是繼承自SKScene,SKScene是自帶了個物理世界的柴信,有興趣你現(xiàn)在也可以試試打印一下當前物理世界的重力看看 -> print(self.physicsWorld.gravity)套啤,結(jié)果是不是(x:0,y:-9.8),表示重力是沿著屏幕向下的方向随常,重力大小是9.8潜沦,是不是跟高中物理學(xué)的是一樣的呢!
最后先做一個準備工作绪氛,在GameScene類的外面加上下面內(nèi)容
let birdCategory: UInt32 = 0x1 << 0
let pipeCategory: UInt32 = 0x1 << 1
let floorCategory: UInt32 = 0x1 << 2
設(shè)置三個常量來表示小鳥唆鸡、水管和地面物理體,稍后我們后面會用到
配置地面物理體
找到didMove()方法枣察,在添加地面的帶面后面加上下面內(nèi)容
//配置地面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 ?//禁止旋轉(zhuǎn)
bird.physicsBody?.categoryBitMask = birdCategory //設(shè)置小鳥物理體標示
bird.physicsBody?.contactTestBitMask = floorCategory | pipeCategory ?//設(shè)置可以小鳥碰撞檢測的物理體
上面我們就設(shè)置好了小鳥的物理體了刻蟹,contactTestBitMask是來設(shè)置可以與小鳥碰撞檢測的物理體,我們設(shè)置了地面和水管嘿辟,所以通常物理體的categoryBitMask用二進制移位方式來表示舆瘪,這樣在設(shè)置contactTestBitMask的時候就可以直接多個移位的標識做按位取或的運算即可
配置水管物理體
找到addPipes(topSize: CGSize, bottomSize: CGSize)方法,在addChild(topPipe),addChild(bottomPipe)代碼之前加入下面的代碼內(nèi)容
//配置上水管物理體
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
選在我們來運行一下游戲吧红伦,你可以看到游戲一開始在初始化狀態(tài)小鳥就受到重力的影響而掉到地面上了英古,這不是我們想要的,我們希望是玩家點擊了屏幕游戲開始了小鳥才會下落
那么請在shuffle()方法里昙读,設(shè)置小鳥的position的代碼后面加上下面這句
bird.physicsBody?.isDynamic = false
然后再在startGame()方法里召调,開始創(chuàng)建水管代碼之前加上下面這句
bird.physicsBody?.isDynamic = true
isDynamic的作用是設(shè)置這個物理體當前是否會受到物理環(huán)境的影響,默認是true,我們在游戲初始化的時候設(shè)置小鳥不受物理環(huán)境影響唠叛,但是在游戲開始的時候才會受到物理環(huán)境的影響
現(xiàn)在再運行游戲就可以看到初始化的時候小鳥停在屏幕中間只嚣,點擊了屏幕游戲開始了,小鳥才會掉下來
給小鳥一個速度
現(xiàn)在這游戲簡直就是沒法玩艺沼,小鳥一下就掉到地上册舞,怎么點屏幕他都不會網(wǎng)上飛
現(xiàn)在找到touchesBegan()方法,看到這個寫好的switch語句里障般,.running情況只有一句print("給小鳥一個向上的力")调鲸,打印一句話可不會讓小鳥往上飛,現(xiàn)在請將這句print替換為下面這句代碼
bird.physicsBody?.applyImpulse(CGVector(dx: 0, dy: 20))
這個句代碼可以給小鳥的物理體施加一個向上的沖量挽荡,讓小鳥獲得一定的向上速度藐石,但是由于小鳥還受重力影響,所以你得經(jīng)常點擊屏幕才能保持小鳥不掉下去定拟。
Impluse是什么于微?Impulse在物理上就是沖量的意思,沖量=質(zhì)量 * (結(jié)束速度 - 初始速度)办素,即I = m * (v2 - v1)角雷,如果物體的質(zhì)量為1祸穷,那么沖量i = v2 - v1性穿。當一個質(zhì)量為1的物理體applyImpulse(CGVector(dx: 0, dy: 20))的意思就是讓他在y的方向上疊加20m/s的速度。當然如果物理體質(zhì)量m不為1雷滚,那疊加的速度就不是剛好等于沖量的字面量了需曾,而是要除以m了。如一個質(zhì)量為2的物理體同樣applyImpulse(CGVector(dx: 0, dy: 20))祈远,結(jié)果就是它在y的方向上疊加了10m/s的一個速度
檢測碰撞
現(xiàn)在我們的游戲已經(jīng)基本能玩了呆万,但是小鳥碰到水管或者掉到地面上小鳥沒有死掉,游戲還在繼續(xù)车份,現(xiàn)在我們就來完善這個問題
記得我們將當前的GameScene設(shè)置為了物理世界的碰撞檢測的代理么谋减?接下來我們只要實現(xiàn)檢測到碰撞產(chǎn)生的代理方法即可
在GameScene里添加下面這個方法代碼,didBegin()會在當前物理世界有兩個物理體碰撞接觸了則回調(diào)用扫沼,這兩個碰撞了的物理體的信息都在contact這個參數(shù)里面出爹,分別是bodyA和bodyB
func didBegin(_ contact: SKPhysicsContact) {
? ? ? ? //先檢查游戲狀態(tài)是否在運行中,如果不在運行中則不做操作缎除,直接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是否為水管或者地面梢为,如果是則游戲結(jié)束,直接調(diào)用gameOver()方法
? ? ? ?if (bodyA.categoryBitMask == birdCategory && bodyB.categoryBitMask == pipeCategory) ||
? ? ? ? ? ?(bodyA.categoryBitMask == birdCategory && bodyB.categoryBitMask == floorCategory) {
? ? ? ? ? ? ? ?gameOver()
? ? ? ?}
}
現(xiàn)在我們運行游戲就可以正常玩耍了,小鳥碰到地面或者水管游戲就會結(jié)束铸董,小鳥就會落地祟印,水管會停住,如果再點擊一次屏幕就會回到初始狀態(tài)粟害,小鳥回到中間旁理,殘留的水管都消失了
但是這個游戲結(jié)束有點突兀,最好能給個提示告訴玩家游戲結(jié)束了我磁。
我們先給GameScene這個類添加一個變量孽文,來表示游戲結(jié)束提示的label
lazy var gameOverLabel: SKLabelNode = {
? ? ? ? ?let label = SKLabelNode(fontNamed: "Chalkduster")
? ? ? ? ?label.text = "Game Over"
? ? ? ? ?return label
}()
注意這個變量我們用了一個lazy來標示,標示這個label是懶加載的夺艰,也就是只有在gameOverLabel第一次被調(diào)用的時候才會創(chuàng)建芋哭,它的創(chuàng)建代碼用一個大括號包住,結(jié)尾要帶一對()表示馬上執(zhí)行意思郁副。這樣我們就通過懶加載創(chuàng)建一個gameOverLabel减牺,他的text內(nèi)容Game Over提示語。
接下來找到gameOver()方法存谎,在此方法的最后加上下面的代碼拔疚,這樣在gameOver的時候就會有一個提示語從天而降了
//禁止用戶點擊屏幕
isUserInteractionEnabled = false
//添加gameOverLabel到場景里
addChild(gameOverLabel)
//設(shè)置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: {
? ? ? ? //動畫結(jié)束才重新允許用戶點擊屏幕
? ? ? ? self.isUserInteractionEnabled = true
})
不過要記住在游戲回到初始化狀態(tài)下的時候,要把gameOverLabel從場景里移除掉既荚,所以找到shuffle()方法稚失,然后在removeAllPipesNode()方法后面加上下面這一句
gameOverLabel.removeFromParent()
現(xiàn)在我們再來運行一下游戲,就能發(fā)現(xiàn)一切正常了恰聘,可以愉快的玩耍了句各!
6.補充提示
雖然游戲能玩了,但是你不覺得少點什么么晴叨?
沒錯游戲一般都有分數(shù)凿宾,表示玩家這句玩的成績怎么樣,所以這個游戲里我們可以添加一個表示玩家小鳥飛了多遠距離的提示兼蕊。
我們先給GameScene添加一個metersLabel初厚,用它來展示用戶走了多遠的距離,添加下面代碼到你的GameScene
lazy var metersLabel: SKLabelNode = {
? ? ? ? let label = SKLabelNode(text: "meters:0")
? ? ? ? label.verticalAlignmentMode = .top
? ? ? ? label.horizontalAlignmentMode = .center
? ? ? ? return label
}()
可以看到我們同樣使用了懶加載的方式來創(chuàng)建這個metersLabel變量
PS:這里稍微多介紹一點SKLabelNode這個類孙技,如果做過iOS應(yīng)用開發(fā)的朋友應(yīng)該都知道UILabel這個控件产禾,跟UILabel類似SKLabelNode就是SpriteKit中顯示一段文字的空間,首先他是繼承自SKNode绪杏,所以它可以被添加到場景里面下愈,它也可以執(zhí)行各種Action動作。
另外可能還有一個你不適應(yīng)的地方就是他的位置布局問題蕾久,在做iOS應(yīng)用時候UILabel有大小势似,UILabel的原點在它自己左上角,你自然知道怎么放置它了。但是SKLabelNode是沒有size這個屬性的履因,他的frame屬性也只是readonly的障簿,這怎么辦?
SKLabelNode有兩個新的屬性叫做verticalAlignmentMode和horizontalAlignmentMode栅迄,表示這個label在水平和垂直方向上如何布局站故,他們是枚舉類型。比如你把的SKLabelNode的postion位置設(shè)置在(50,100)這個點毅舆,然后把他的verticalAlignmentMode 設(shè)置為.top西篓,則表示這段文字的頂部是position所在位置的y的水平高度上,如果設(shè)置為.bottom憋活,則這段文字的底部水平線高度就是position的y的水平高度岂津。所以horizontalAlignmentMode屬性也是同理,只是它是設(shè)置水平方向上的布局悦即∷背桑可能等我遲點補充一個圖表示會比較清晰,容易理解
現(xiàn)在我們有了這個label的變量辜梳,要將他加到場景上
找到didMove()方法粱甫,然后在設(shè)置場景的物理體的代碼后面加上下面的代碼內(nèi)容
// 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軸上的位置設(shè)置在了100茶宵,你可能會問這不是2D游戲么怎么會有z軸,這里的z軸你也可以理解為圖層的層次順序軸粉洼,zPosition越大就越靠近玩家节预,就是說如果兩個場景里的node某一部分重疊了叶摄,那么就是zPosition大的那個node會覆蓋住小的那個node属韧,zPosition默認值是0,如果兩個都是0的node重疊了那就要看誰是先被添加進場景的蛤吓,先被添加進的會被后添加進的覆蓋住宵喂。
那么為什么metersLabel要設(shè)置一個大一些的zPosition?因為metersLabel是在didMove方法里就添加到場景了会傲,我們又希望它始終不被遮住锅棕,但是那些出現(xiàn)的水管是后添加進場景的node,他們移動到metersLabel上面的時候就會覆蓋住它淌山,所以我們才要做這樣的一個操作裸燎。
現(xiàn)在我們有一個用來顯示小鳥飛了多遠的label了,該要讓它顯示變化的值了
我們給GameScene添加多一個記錄飛行米數(shù)的變量泼疑,添加下面代碼到GameScene
var meters = 0 {
? ? didSet ?{
? ? ? ? ?metersLabel.text = "meters:\(meters)"
? ? }
}
meters是一個Int值就可以了德绿,初始設(shè)置為0,可以看到我們寫了個didSet{...},表示這個變量每次當被設(shè)置了一個新的值就會執(zhí)行一次didSet里面的代碼移稳,我們在這里重新設(shè)置了一個metersLabel現(xiàn)實的內(nèi)容蕴纳。
接下來我們要在游戲運行時候不斷增加meters的值,簡單點的方法就是在每一幀刷新的update()方法里去改變
我找到update()方法个粱,然后添加下面的內(nèi)容到方法里
if gameStatus == .running {
? ? ? meters += 1
}
現(xiàn)在你運行游戲就會看到一旦點擊一下屏幕游戲開始了古毛,飛行的米數(shù)就會不斷的刷刷刷的飛漲。
但是還有一件事情別忘了都许,就是找到shuffle()方法稻薇,在里面添加一句下面的代碼,每次回到游戲初始化狀態(tài)下時胶征,要把上一局的飛行米數(shù)重新清零
meters = 0
現(xiàn)在你再運行游戲就會得到跟文章開篇時候的動圖一樣的效果了颖低!
7.還有什么可以完善的?
至此對于此游戲的基本實現(xiàn)算是寫完了弧烤,不過你也可以繼續(xù)完善這個游戲忱屑,或者用不同的方法來實現(xiàn)試試
1.比如說這里我們的場景內(nèi)容移動(地面和水管)是直接在update()方法里改變position來實現(xiàn),那么能不能換成用SKAction的方法來做到呢暇昂?
2.雖然游戲能玩了莺戒,但是那些會影響到游戲的一些關(guān)鍵參數(shù)是否已經(jīng)是最優(yōu)的選擇了?如果你覺得小鳥的自由落體下墜的太快或者每次小鳥上升的速度太小等等急波,這些可能都要開發(fā)者自己去玩玩嘗試找到最優(yōu)的參數(shù)配置
3.是否可以增加漸進的難度从铲?比如說隨機的產(chǎn)生水管的間隔時間能不能隨游戲進行時間越來越短?等等
4.是否小鳥可以能吃到一些道具讓它在一定時間內(nèi)不懼怕水管澄暮?
5.是否可以添加玩家成績的記錄名段?
等等等等。泣懊。伸辟。。馍刮。這些就看你想不想去完善了試一試信夫,這里就不做一一實現(xiàn)了,謝謝