iOS 10平臺SpriteKit新特性之Tile Maps(下)
譯文使用鄰接組
鄰接組能夠?qū)崿F(xiàn)在繪制瓦片時定義應(yīng)繪制什么樣的邊緣圖像。例如,如果你正在創(chuàng)建一個小島,你會擁有以草為中心的瓦片,而邊緣瓦片會是延伸到海洋中的海灘。當你繪制時,中心草瓦片鋪開,而邊緣部分將自動圍繞它們展開。
有兩種方法來平鋪邊緣瓦片。你可以使用單色瓦片,它們從一個表面轉(zhuǎn)型到另一個,請參考來自kenney.nl網(wǎng)站的如下圖像:
另一種方法是使用透明邊緣,就像下圖所示的這一組:
如果你使用透明邊緣,你必須把瓦片地圖節(jié)點分層。這樣一來,背景會通過透明度顯示。前面,你已經(jīng)繪制了水背景?,F(xiàn)在,我們要添加另一個陸地層。
再返回到文件Tiles.sks來編輯你的瓦片集合。按住Ctrl鍵同時點擊Ground Tiles,然后從菜單中選擇New\8-Way Adjacency Group。
將new tile group重命名為Grass。當你選擇Grass瓦片組時,你會看到一個網(wǎng)格中充滿了瓦片。
當然,這些內(nèi)容對于剛開始的新手來說可能看起來有些困惑,但是所有這些一會兒時間后就會變得清晰明了。
現(xiàn)在,請使用grass篩選媒體庫,你會看到屏幕上顯示出所有與草相關(guān)的瓦片圖像。
將瓦片拖到網(wǎng)格中的相關(guān)位置。我已經(jīng)根據(jù)其在網(wǎng)格中的位置命名了每一個圖像。為中心背景添加三個變體部分,就像你以前那樣操作就行了。這一次,選擇GrassCenter1變體并把Placement Weight參數(shù)更改為10。通過這種方式,當你繪制時你會得到更多的普通瓦片。
當你將圖像拖動到目標位置時,你可能會犯錯誤。我自己就經(jīng)?;煜锹浜瓦吘壨咂?。不過,不用過于擔心。只需把正確的圖像拖到錯誤的圖像上方,然后從彈出式菜單中選擇“Replace tile variant texture”命令即可。請注意,如果你選擇另一個選項,你可以通過這種方式添加對應(yīng)瓦片的變體。
當你完成后,記得要按Command + S命令保存該瓦片集。你的最終網(wǎng)格應(yīng)該看起來像上面的圖片那樣。
現(xiàn)在,在項目導(dǎo)航器中打開文件GameScene.sks。
[注意]如果由于某種原因,你想要用你剛添加的新瓦片改變背景水瓦片,那么,很遺憾:目前還不支持這種更改;所以,你需要完全重新繪制它們。你可以刪除瓦片地圖節(jié)點再添加一個新的,或者再建立另一個瓦片集,然后再把節(jié)點的瓦片集更改到原來那個。
接下來,把一個新的瓦片地圖節(jié)點拖至場景中,并將節(jié)點的名稱更改為 landBackground。你會需要這個數(shù)據(jù)的,因為以后你還要在代碼中引用該節(jié)點。現(xiàn)在,把地圖的大小更改為32列X24行。同時,確保位置中的X坐標和 Y坐標都設(shè)置為0且X和Y縮放比例都設(shè)置為1。
現(xiàn)在,雙擊場景編輯器來編輯瓦片地圖。
從工具欄上單擊命令“Select Tile”。新的Grass瓦片組將顯示在下拉列表框中,還有其他兩個瓦片組。
選擇最右邊的瓦片組Grass,單擊工具欄上的畫筆工具開始繪畫。
你會看到邊緣部分是如何神奇地包圍住你畫的內(nèi)容的!
還記得Tiles.sks中那令人困惑的網(wǎng)格嗎?下面給出瓦片的繪制方式:
好了,點擊Done命令,并在屬性檢查器中取消勾選Enable Automapping項。
雙擊場景編輯器中的landBackground節(jié)點再次編輯瓦片地圖。當你點擊下拉菜單中的Select Tile命令時將顯示所有可用的瓦片?,F(xiàn)在,你可以用你所選的瓦片在網(wǎng)格中繪制單個正方形了。
當你畫完后單擊Done命令。現(xiàn)在,構(gòu)建并運行你的應(yīng)用程序來欣賞一下你新創(chuàng)建的新背景吧。
新的SpriteKit瓦片類
除了使用場景編輯器以可視化方式創(chuàng)建瓦片地圖外,你還可以在代碼中創(chuàng)建瓦片地圖。那么,你可能在想:“既然可以使用場景編輯器為什么還要這樣做呢?”
假設(shè)您需要識別具體的瓦片,例如水瓦片,該怎么辦呢?在本教程中,汽車速度的減慢是由水瓦片導(dǎo)致的。在代碼中你可以實現(xiàn)這一判斷!
另一個原因是隨機性。在本教程中,玩家將收集鴨子和氣罐。如果你使用編輯器繪制這些東西,那么在每一場游戲中它們都將留在原地。如果您在代碼中添加它們,您可以隨機地控制它們的位置。
回顧到目前為止你所做的,您創(chuàng)建的每一項其實都有一個相應(yīng)的新的SpriteKit類。
在文件GameScene.sks中,它們是:
SKTileMapNode:對應(yīng)于你放置在場景編輯器中的節(jié)點。
SKTileSet:對應(yīng)于您在屬性檢查器中為瓦片地圖節(jié)點分配的瓦片集。
打開文件Tiles.sks,在這個文件中你使用了:
SKTileSet:樹結(jié)構(gòu)頂部的項,本教程中命名為Ground Tiles。
SKTileGroup:瓦片集中的瓦片組。在本教程中,它們對應(yīng)于Water Tile、Grass Tile和Grass。
SKTileGroupRule:定義每個瓦片的鄰接規(guī)則。例如,左上角瓦片、 中心瓦片或右邊緣瓦片。
現(xiàn)在,點擊Grass組并選擇Center規(guī)則。
SKTileDefinition:每條規(guī)則都有一組瓦片定義。這些都是瓦片的變體。例如,中心瓦片具有三個SKTileDefinition變體,在繪畫時可隨機使用它們。
然后,選擇屏幕底部的一個中心變體。于是,在屬性檢查器中,你會看到每個 SKTileDefinition變體可用的所有屬性。
如果你創(chuàng)建了動畫瓦片,那么你會看到每一幀的信息,而且你能夠控制幀速率。設(shè)想一下你的水瓦片拍打著草瓦片的情形吧。
對應(yīng)于每個變體的屬性檢查器也是你可以提供這些變體的用戶數(shù)據(jù)的地方。以后當你想要確定某對象是一只鴨子還是一種氣罐時,這將很有用。
編程控制瓦片
***,我們需要編寫一些代碼。
當汽車在水瓦片上行駛時其速度應(yīng)顯著放緩。有兩種方法可以實現(xiàn)這一目的。
您可以將一個布爾型用戶數(shù)據(jù)isWater添加到所有的水瓦片上,然后在代碼中檢查用戶數(shù)據(jù)?;蛘?,因為在單獨的圖層上創(chuàng)建了水,你可以使用landBackground瓦片地圖節(jié)點中的透明度來測試查看是否汽車的位置下部的瓦片為空。
打開文件GameScene.swift,將landBackground屬性添加到GameScene:
var landBackground:SKTileMapNode!
接下來,把如下代碼添加到方法loadSceneNodes中:
- guard let landBackground = childNode(withName: "landBackground")
- as? SKTileMapNode else {
- fatalError("Background node not loaded")
- }
- self.landBackground = landBackground
在此,我們把瓦片地圖節(jié)點加載到了landBackground屬性中。
現(xiàn)在,再在方法update(_:)中添加如下代碼:
- let position = car.position
- let column = landBackground.tileColumnIndex(fromPosition: position)
- let row = landBackground.tileRowIndex(fromPosition: position)
方法update(_:)每幀都會執(zhí)行;這是一個檢測小車位置的好地方。在這里,你把小車的位置數(shù)據(jù)轉(zhuǎn)換成行列數(shù)據(jù)。就是通過這種方式提取瓦片地圖中的實際瓦片數(shù)據(jù)的。
接下來把如下代碼添加到方法update(_:)后面:
let tile = landBackground.tileDefinition(atColumn: column, row: row)
tile現(xiàn)在包含在指定行列位置的瓦片的SKTileDefinition信息。
***,把如下代碼添加到方法update(_:)后面:
- if tile == nil {
- maxSpeed = waterMaxSpeed
- print("water")
- } else {
- maxSpeed = landMaxSpeed
- print("grass")
- }
如果沒有瓦片繪制,則小車的***速度將被減小到合適的值。你可以使用landBackground的透明度屬性來決定這個值。本游戲中,透明的瓦片被認定是水。
***,重新構(gòu)建與運行程序,結(jié)果如下:
小車的***速度將比其在水中時減小多了。當然,你可以通過控制臺輸出來進一步確定這個結(jié)論。
收集對象
現(xiàn)在,我們來使用前面創(chuàng)建的對象創(chuàng)建一個瓦片地圖,以便汽車收集之用。
首先,我們隨機地把橡膠鴨子和氣罐充滿整個瓦片地圖。只是注意一點:鴨子自然要放到水瓦片處,而氣罐置于草瓦片上。
接下來,為對象創(chuàng)建新的瓦片集。運行命令“File\New\File”并選擇 “iOS\Resource\SpriteKit Tile Set”模板,然后單擊“Next”按鈕。把瓦片集命名為ObjectTiles并單擊命令“Create”。
打開文件ObjectTiles.sks,更改瓦片集名稱為ObjectTiles。
接下來,把“new tile group”更改為Duck,然后把rubberduck圖像從媒體庫拖到圖塊上。
現(xiàn)在,選擇鴨瓦片,注意到對應(yīng)瓦片位于底部。在屬性檢查器中,單擊User Data下的+(你可能需要在檢查器上向下滾動一段距離)。然后,雙擊userData1,將其更改為duck。至于是什么類型是沒關(guān)系的,因為我們只是檢查一下是否存在此值,所以可以保留其為一個整數(shù)類型吧。
接下來,按住Ctrl鍵的同時點擊瓦片集列表中的“Object Tiles”并選擇“New\Single Tile Group”。把此組重命名為“GasCan”并把圖像gascan拖動到瓦片上。
然后,選擇氣罐瓦片,并像以前一樣添加用戶數(shù)據(jù),并命名該用戶數(shù)據(jù)為gascan。
[注意]在一開始時,我想在代碼中創(chuàng)建瓦片地圖結(jié)點并使用命名的瓦片集填充它,但是在寫作本文時,我還無法使用名稱initializer來檢索SKTileSet。因此,目前情況下,僅是使用瓦片集名字在場景編輯器中創(chuàng)建了一個空的瓦片地圖結(jié)點。
現(xiàn)在,打開文件GameScene.sks,并把一個tile map node添加到場景中。然后在屬性檢查器中命名為objects,同時設(shè)置其X和Y坐標都為0,Map Size為32X24。
對瓦片集方面,從下拉菜單中選擇“Object Tiles”,你會注意到Tile Size會被自動設(shè)置大小,參考下圖。
接下來,打開文件GameScene.swift,添加如下新屬性:
var objectsTileMap:SKTileMapNode!
然后,在方法loadSceneNodes()中添加如下代碼:
- guard let objectsTileMap = childNode(withName: "objects")
- as? SKTileMapNode else {
- fatalError("Objects node not loaded")
- }
- self.objectsTileMap = objectsTileMap
這將加載瓦片地圖結(jié)點,于是你可以通過Objects瓦片集來訪問它。
接下來,在方法loadSceneNodes()后面,添加一個新的方法,如下:
- func setupObjects() {
- // 1
- let tileSet = objectsTileMap.tileSet
- // 2
- let tileGroups = tileSet.tileGroups
- // 3
- guard let duckTile = tileGroups.first(where: {$0.name == "Duck"}) else {
- fatalError("No Duck tile definition found")
- }
- guard let gascanTile = tileGroups.first(where: {$0.name == "Gas Can"}) else {
- fatalError("No Gas Can tile definition found")
- }
- // 4
- let numberOfObjects = 64
- let columns = UInt32(objectsTileMap.numberOfColumns)
- let rows = UInt32(objectsTileMap.numberOfRows)
- }
至今,你已經(jīng)設(shè)置了需要隨機放置對象的所有屬性。大致思路如下:
1.從場景編輯器的瓦片地圖結(jié)點中檢索瓦片集。
2.從瓦片集中檢索瓦片組列表。
3.從瓦片組數(shù)組的瓦片中檢索瓦片定義。其中,tileGroups.first(where:)是Swift 3中提供的一個新方法,用于查找一個對象數(shù)組中的***次出現(xiàn)。
4.建立往地圖上放置對象所需要的常量。在此,可以把numberOfObjects的值修改為某個合適的值。
現(xiàn)在,在上面同一個方法中,繼續(xù)添加如下代碼:
- // 5
- for _ in 1...numberOfObjects {
- // 6
- let column = Int(arc4random_uniform(columns))
- let row = Int(arc4random_uniform(rows))
- let groundTile = landBackground.tileDefinition(atColumn: column, row: row)
- // 7
- let tile = groundTile == nil ? duckTile : gascanTile
- // 8
- objectsTileMap.setTileGroup(tile, forColumn: column, row: row)
- }
這段代碼的大致邏輯如下:
5.以循環(huán)方式放置64個對象。
6.隨機選擇一列和一行。
7.如果選擇的瓦片是單色,則選擇了氣罐;否則,選擇的是鴨子。這將確保鴨子在水中,而氣罐在草上。
8.把鴨子或者氣罐放在瓦片上,放到選擇的行列處。
現(xiàn)在,在didMove(to:)方法的***調(diào)用下面的新方法:
setupObjects()
現(xiàn)在,重新構(gòu)建和運行一下工程。你會注意到背景中的水中布滿了鴨子,而草上放著氣罐。請參考下圖。
***一件事是編寫代碼來把所有鴨子摘起來放到小車上。這些代碼類似于你之前已經(jīng)寫過的檢測你是否位于水瓦片上,當然不包括你使用用戶數(shù)據(jù)來控制鴨子和氣體那一部分。
返回到文件GameScene.swift,然后在GameScene的頂部添加兩個屬性:
- lazy var duckSound:SKAction = {
- return SKAction.playSoundFileNamed("Duck.wav", waitForCompletion: false)
- }()
- lazy var gascanSound:SKAction = {
- return SKAction.playSoundFileNamed("Gas.wav", waitForCompletion: false)
- }()
在此,你創(chuàng)建了兩個動作來控制當物體收集時播放聲音。這些聲音數(shù)據(jù)也包含在示例工程中。
現(xiàn)在,在方法update(_:)的***添加如下代碼:
- let objectTile = objectsTileMap.tileDefinition(atColumn: column, row: row)
- if let _ = objectTile?.userData?.value(forKey: "gascan") {
- run(gascanSound)
- objectsTileMap.setTileGroup(nil, forColumn: column, row: row)
- }
- if let _ = objectTile?.userData?.value(forKey: "duck") {
- run(duckSound)
- objectsTileMap.setTileGroup(nil, forColumn: column, row: row)
- }
在這里,我們檢查用戶數(shù)據(jù)以確定它是否包含gascan或duck。然后,播放相應(yīng)的聲音并將瓦片組設(shè)置為零。這將從視圖中刪除瓦片。
再次構(gòu)建和運行一下應(yīng)用程序吧,再試試收集氣罐和鴨子!這比實現(xiàn)精靈碰撞簡單多了吧!
小結(jié)
通過本文,你應(yīng)當學(xué)會了如何使用Tile Map Editor。這真是SpriteKit系列工具家族中***的補充。
本文游戲***版本的下載地址是https://cdn1.raywenderlich.com/wp-content/uploads/2016/06/RDRescue-Finished.zip。想更多地了解有關(guān)細節(jié),你不妨觀看一下蘋果WWDC 2016大會的有關(guān)視頻(https://developer.apple.com/videos/play/wwdc2016/610/)。