使用邻接组
邻接组能够实现在绘制瓦片时定义应绘制什么样的边缘图像。例如,如果你正在创建一个小岛,你会拥有以草为中心的瓦片,而边缘瓦片会是延伸到海洋中的海滩。当你绘制时,中心草瓦片铺开,而边缘部分将自动围绕它们展开。
有两种方法来平铺边缘瓦片。你可以使用单色瓦片,它们从一个表面转型到另一个,请参考来自kenney.nl网站的如下图像:
另一种方法是使用透明边缘,就像下图所示的这一组:
如果你使用透明边缘,你必须把瓦片地图节点分层。这样一来,背景会通过透明度显示。前面,你已经绘制了水背景。现在,我们要添加另一个陆地层。
再返回到文件Tiles.sks来编辑你的瓦片集合。按住Ctrl键同时点击Ground Tiles,然后从菜单中选择New\8-Way Adjacency Group。
将new tile group重命名为Grass。当你选择Grass瓦片组时,你会看到一个网格中充满了瓦片。
当然,这些内容对于刚开始的新手来说可能看起来有些困惑,但是所有这些一会儿时间后就会变得清晰明了。
现在,请使用grass筛选媒体库,你会看到屏幕上显示出所有与草相关的瓦片图像。
将瓦片拖到网格中的相关位置。我已经根据其在网格中的位置命名了每一个图像。为中心背景添加三个变体部分,就像你以前那样操作就行了。这一次,选择GrassCenter1变体并把Placement Weight参数更改为10。通过这种方式,当你绘制时你会得到更多的普通瓦片。
当你将图像拖动到目标位置时,你可能会犯错误。我自己就经常混淆角落和边缘瓦片。不过,不用过于担心。只需把正确的图像拖到错误的图像上方,然后从弹出式菜单中选择“Replace tile variant texture”命令即可。请注意,如果你选择另一个选项,你可以通过这种方式添加对应瓦片的变体。
当你完成后,记得要按Command + S命令保存该瓦片集。你的最终网格应该看起来像上面的图片那样。
现在,在项目导航器中打开文件GameScene.sks。
[注意]如果由于某种原因,你想要用你刚添加的新瓦片改变背景水瓦片,那么,很遗憾:目前还不支持这种更改;所以,你需要完全重新绘制它们。你可以删除瓦片地图节点再添加一个新的,或者再建立另一个瓦片集,然后再把节点的瓦片集更改到原来那个。
接下来,把一个新的瓦片地图节点拖至场景中,并将节点的名称更改为 landBackground。你会需要这个数据的,因为以后你还要在代码中引用该节点。现在,把地图的大小更改为32列X24行。同时,确保位置中的X坐标和 Y坐标都设置为0且X和Y缩放比例都设置为1。
现在,双击场景编辑器来编辑瓦片地图。
从工具栏上单击命令“Select Tile”。新的Grass瓦片组将显示在下拉列表框中,还有其他两个瓦片组。
选择最右边的瓦片组Grass,单击工具栏上的画笔工具开始绘画。
你会看到边缘部分是如何神奇地包围住你画的内容的!
还记得Tiles.sks中那令人困惑的网格吗?下面给出瓦片的绘制方式:
好了,点击Done命令,并在属性检查器中取消勾选Enable Automapping项。
双击场景编辑器中的landBackground节点再次编辑瓦片地图。当你点击下拉菜单中的Select Tile命令时将显示所有可用的瓦片。现在,你可以用你所选的瓦片在网格中绘制单个正方形了。
当你画完后单击Done命令。现在,构建并运行你的应用程序来欣赏一下你新创建的新背景吧。
新的SpriteKit瓦片类
除了使用场景编辑器以可视化方式创建瓦片地图外,你还可以在代码中创建瓦片地图。那么,你可能在想:“既然可以使用场景编辑器为什么还要这样做呢?”
假设您需要识别具体的瓦片,例如水瓦片,该怎么办呢?在本教程中,汽车速度的减慢是由水瓦片导致的。在代码中你可以实现这一判断!
另一个原因是随机性。在本教程中,玩家将收集鸭子和气罐。如果你使用编辑器绘制这些东西,那么在每一场游戏中它们都将留在原地。如果您在代码中添加它们,您可以随机地控制它们的位置。
回顾到目前为止你所做的,您创建的每一项其实都有一个相应的新的SpriteKit类。
在文件GameScene.sks中,它们是:
SKTileMapNode:对应于你放置在场景编辑器中的节点。
SKTileSet:对应于您在属性检查器中为瓦片地图节点分配的瓦片集。
打开文件Tiles.sks,在这个文件中你使用了:
SKTileSet:树结构顶部的项,本教程中命名为Ground Tiles。
SKTileGroup:瓦片集中的瓦片组。在本教程中,它们对应于Water Tile、Grass Tile和Grass。
SKTileGroupRule:定义每个瓦片的邻接规则。例如,左上角瓦片、 中心瓦片或右边缘瓦片。
现在,点击Grass组并选择Center规则。
SKTileDefinition:每条规则都有一组瓦片定义。这些都是瓦片的变体。例如,中心瓦片具有三个SKTileDefinition变体,在绘画时可随机使用它们。
然后,选择屏幕底部的一个中心变体。于是,在属性检查器中,你会看到每个 SKTileDefinition变体可用的所有属性。
如果你创建了动画瓦片,那么你会看到每一帧的信息,而且你能够控制帧速率。设想一下你的水瓦片拍打着草瓦片的情形吧。
对应于每个变体的属性检查器也是你可以提供这些变体的用户数据的地方。以后当你想要确定某对象是一只鸭子还是一种气罐时,这将很有用。
编程控制瓦片
***,我们需要编写一些代码。
当汽车在水瓦片上行驶时其速度应显著放缓。有两种方法可以实现这一目的。
您可以将一个布尔型用户数据isWater添加到所有的水瓦片上,然后在代码中检查用户数据。或者,因为在单独的图层上创建了水,你可以使用landBackground瓦片地图节点中的透明度来测试查看是否汽车的位置下部的瓦片为空。
打开文件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
在此,我们把瓦片地图节点加载到了landBackground属性中。
现在,再在方法update(_:)中添加如下代码:
- let position = car.position
- let column = landBackground.tileColumnIndex(fromPosition: position)
- let row = landBackground.tileRowIndex(fromPosition: position)
方法update(_:)每帧都会执行;这是一个检测小车位置的好地方。在这里,你把小车的位置数据转换成行列数据。就是通过这种方式提取瓦片地图中的实际瓦片数据的。
接下来把如下代码添加到方法update(_:)后面:
let tile = landBackground.tileDefinition(atColumn: column, row: row)
tile现在包含在指定行列位置的瓦片的SKTileDefinition信息。
***,把如下代码添加到方法update(_:)后面:
- if tile == nil {
- maxSpeed = waterMaxSpeed
- print("water")
- } else {
- maxSpeed = landMaxSpeed
- print("grass")
- }
如果没有瓦片绘制,则小车的***速度将被减小到合适的值。你可以使用landBackground的透明度属性来决定这个值。本游戏中,透明的瓦片被认定是水。
***,重新构建与运行程序,结果如下:
小车的***速度将比其在水中时减小多了。当然,你可以通过控制台输出来进一步确定这个结论。
收集对象
现在,我们来使用前面创建的对象创建一个瓦片地图,以便汽车收集之用。
首先,我们随机地把橡胶鸭子和气罐充满整个瓦片地图。只是注意一点:鸭子自然要放到水瓦片处,而气罐置于草瓦片上。
接下来,为对象创建新的瓦片集。运行命令“File\New\File”并选择 “iOS\Resource\SpriteKit Tile Set”模板,然后单击“Next”按钮。把瓦片集命名为ObjectTiles并单击命令“Create”。
打开文件ObjectTiles.sks,更改瓦片集名称为ObjectTiles。
接下来,把“new tile group”更改为Duck,然后把rubberduck图像从媒体库拖到图块上。
现在,选择鸭瓦片,注意到对应瓦片位于底部。在属性检查器中,单击User Data下的+(你可能需要在检查器上向下滚动一段距离)。然后,双击userData1,将其更改为duck。至于是什么类型是没关系的,因为我们只是检查一下是否存在此值,所以可以保留其为一个整数类型吧。
接下来,按住Ctrl键的同时点击瓦片集列表中的“Object Tiles”并选择“New\Single Tile Group”。把此组重命名为“GasCan”并把图像gascan拖动到瓦片上。
然后,选择气罐瓦片,并像以前一样添加用户数据,并命名该用户数据为gascan。
[注意]在一开始时,我想在代码中创建瓦片地图结点并使用命名的瓦片集填充它,但是在写作本文时,我还无法使用名称initializer来检索SKTileSet。因此,目前情况下,仅是使用瓦片集名字在场景编辑器中创建了一个空的瓦片地图结点。
现在,打开文件GameScene.sks,并把一个tile map node添加到场景中。然后在属性检查器中命名为objects,同时设置其X和Y坐标都为0,Map Size为32X24。
对瓦片集方面,从下拉菜单中选择“Object Tiles”,你会注意到Tile Size会被自动设置大小,参考下图。
接下来,打开文件GameScene.swift,添加如下新属性:
var objectsTileMap:SKTileMapNode!
然后,在方法loadSceneNodes()中添加如下代码:
- guard let objectsTileMap = childNode(withName: "objects")
- as? SKTileMapNode else {
- fatalError("Objects node not loaded")
- }
- self.objectsTileMap = objectsTileMap
这将加载瓦片地图结点,于是你可以通过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)
- }
至今,你已经设置了需要随机放置对象的所有属性。大致思路如下:
1.从场景编辑器的瓦片地图结点中检索瓦片集。
2.从瓦片集中检索瓦片组列表。
3.从瓦片组数组的瓦片中检索瓦片定义。其中,tileGroups.first(where:)是Swift 3中提供的一个新方法,用于查找一个对象数组中的***次出现。
4.建立往地图上放置对象所需要的常量。在此,可以把numberOfObjects的值修改为某个合适的值。
现在,在上面同一个方法中,继续添加如下代码:
- // 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.以循环方式放置64个对象。
6.随机选择一列和一行。
7.如果选择的瓦片是单色,则选择了气罐;否则,选择的是鸭子。这将确保鸭子在水中,而气罐在草上。
8.把鸭子或者气罐放在瓦片上,放到选择的行列处。
现在,在didMove(to:)方法的***调用下面的新方法:
setupObjects()
现在,重新构建和运行一下工程。你会注意到背景中的水中布满了鸭子,而草上放着气罐。请参考下图。
***一件事是编写代码来把所有鸭子摘起来放到小车上。这些代码类似于你之前已经写过的检测你是否位于水瓦片上,当然不包括你使用用户数据来控制鸭子和气体那一部分。
返回到文件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)
- }()
在此,你创建了两个动作来控制当物体收集时播放声音。这些声音数据也包含在示例工程中。
现在,在方法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)
- }
在这里,我们检查用户数据以确定它是否包含gascan或duck。然后,播放相应的声音并将瓦片组设置为零。这将从视图中删除瓦片。
再次构建和运行一下应用程序吧,再试试收集气罐和鸭子!这比实现精灵碰撞简单多了吧!
小结
通过本文,你应当学会了如何使用Tile Map Editor。这真是SpriteKit系列工具家族中***的补充。
本文游戏***版本的下载地址是https://cdn1.raywenderlich.com/wp-content/uploads/2016/06/RDRescue-Finished.zip。想更多地了解有关细节,你不妨观看一下苹果WWDC 2016大会的有关视频(https://developer.apple.com/videos/play/wwdc2016/610/)。