- 作者:Mandarava(鳗驼螺)
- 参考:
SpriteBuilder开发TileMap的例子似乎很少,SpriteBuilder官网有一些简单的说明,没有完整的教程。于是我参考了《如何使用COCOS2DX3.0制作基于TILEMAP的游戏》来写了这篇教程;原文是基于Cocos2dX写的,而本文则是基于SpriteBuilder+Cocos2d-Swift来完成相同的游戏效果,行文结构保持与原文相同,但部份细节存在差异,同时省略了部份原理性的内容。之所以不用Cocos2dX,因为它没有Swift版;而Cocos2d的Swift版是Cocos2d-Swift,因为SpriteBuilder和它结合的比较好,已经集成了Cocos2d-Swift,可以自动为你创建Cocos2d-Swift的Xcode项目,所以这里选择使用SpriteBuilder。通过SpriteBuilder可以开发iOS、OS X、Android平台的游戏,Windows平台至少目前不支持。
需要用到的工具和资源
- SpriteBuilder可以直接在Mac Appstore搜索并下载安装,或者点击这里进入官网下载。
- Tiled地图编辑器,点击这里进入官网下载。
- 本文需要的游戏素材,点击这里下载(密码: a5c6)。由于原教程中的素材在被SpriteBuilder放大到其它尺寸后会出现问题,所以这里我改用了另一张背景素材图片。
使用Tile制作地图
这部份内容主要涉及Tiled地图编辑工具的使用,这里只简要给出创建我们游戏所需的Tile地图的步骤,详细的使用方法可以参考原文或其它有关教程。
在这里,我们用到的背景素材图片是512x512像素的,每行16个Tile,每列也是16个Tile,所以每个Tile的尺寸是32x32像素。
-
新建地图:打开Tiled地图编辑器,点击菜单文件-新建,打开新地图窗口,根据素材资源的尺寸设定参数,这里我们的设定如下。注意,地图“块”(即Tile)的尺寸以像素为单位,这里按背景素材Tile尺寸设置为32像素宽32像素高;而地图的尺寸是以“块”为单位,通过块的数量和块的尺寸可以最终计算地图的实际尺寸,如下图地图实际尺寸是1600x1600像素。
-
导入素材:点击菜单 地图>新图块,打开新图块对话框,点击“浏览”按钮找到我们先前下载的素材资源包中的bg_views.png文件打开,确认块宽度和高度为32像素,边距和间距设置为0(PS:这里我更换后的背景素材块边界上没有黑边,所以这里设置成0)。
- 点击确定后在右侧的图层窗口看到导入的Tile素材了,现在可以用它们来制作地图:点击一个tile,然后在左侧地图上任意位置单击即可将素材填充到地图中。使用顶部工具栏中的图章、填充桶、橡皮擦等工具来编辑地图。
-
首先用填充桶工具将当前地图都填满草皮。并双击将该“块层 1”重新命名为“Background”,这一层作为地皮。
-
点击新建按钮新建一个图层,并命名为“Background2”,作为景物层。选中该层,这次可以随意发挥,设计你的游戏场景。比如下图的效果,一圈小树围起一口老井,嗯,很有想法!值得一提的是,最好在地图的左下角开始布置景物,因为刚启动游戏时会默认显示这一区域,否则到时你可能需要调整地图的坐标位置才能看到你的伟大设计。到这里就完成了游戏的背景设计。
- 点击菜单 文件>保存 将地图文件保存到与素材同级的目录下(因为涉及到素材路径的问题,如果用文本编辑器打开保存的TileMap.tmx就会发现素材路径的格式,素材与地图文件需要保持这种路径结构),命名为:TileMap.tmx。
将Tile地图添加到游戏场景中
接下来我们就可以将TileMap.tmx导入游戏中来,不过在此之前,需要先用SpriteBuilder创建游戏项目,整个过程如下:
-
打开SpriteBuilder,点击菜单 File>New>Project,打开新建对话框,命名为:TiledGameTest,底部的Primary Language选择Swift作为开发语言。
- 删除场景上默认存在的背景和一个文本为“SpriteBuilder”的标签,这些用不到。
-
将先前下载的素材资源所在的文件夹整个拖到SpriteBuilder左侧的资源列表中(这个素材文件夹里应该还有个之前我们保存的TileMap.tmx,不过非图片资源会自动过滤掉,可以不管它),结果如下图。
-
点击菜单File>Project Settings,打开设置对话框,将Default scalling from 改成 1x(phone)。
- 点击左上角的 Publish 按钮发布项目。
-
用Finder打开TiledGameTest项目所在的文件夹,使用Xcode(本教程使用Xcode 7)打开TiledGameTest.xcodeproj项目。PS:用Xcode 7打开项目时可能需要进行Convert。
-
展开资源浏览器中的Published-iOS文件夹可以看到SpriteBuilder已经为我们生成了不同屏幕尺寸的素材资源,省时省力。
-
在Finder中找到前面保存的TileMap.tmx,将其拖到TileGameResources下,结果如图。如果为了可以直接编辑这个tmx,可以将它拖到 resources-phone 目录中(PS:因为之前在SpriteBuilder中设置了“Default scalling from”为“1x(phone)”,也就是说对应该目录下的图片尺寸是图片未经缩放的原始尺寸,或者说就是我们的原始素材),这样如果要修改地图可以直接编辑这里的TileMap.tmx。
- 打开MainScene.swift,然后添加需要的成员变量
private var tiledMap:CCTiledMap! //地图
private var background:CCTiledMapLayer! //背景图层:地皮
private var background2:CCTiledMapLayer! //背景图层:景物
添加didLoadFromCCB:方法,并修改如下:
func didLoadFromCCB(){
tiledMap=CCTiledMap(file: "TileGameResources/TileGame.tmx") //读取地图
background=tiledMap.layerNamed("Background") //获取名为“Background”的图层
background2=tiledMap.layerNamed("Background2") //获取名为“Background2”的图层
addChild(tiledMap)
}
一个CCTiledMap就是一个CCNode,它包含一个或多个CCTiledMapLayer。CCTiledMap提供了一个通过图层名称来获取图层的方法:layerNamed:。最后将这个地图加入到当前场景中。PS:二个Background这里都没用到,只是展示如何获取它们。
-
编译运行一下看看效果如何。以下是在iPhone 6s Plus模拟器中的效果,唉,还不错哦:
Tile对象层和设置Tile地图位置
接下来需要用到TiledMap的对象层,在地 图上圈出一些区域作为事件发生源。这里,我们用来显示游戏主角。
-
重新回到Tiled地图编辑器中,点击菜单 图层>添加对象层,并命名为Objects。选中Objects图层,点击顶部的矩形按钮在适当的位置绘制一个小矩形。
-
右键单击小矩形点“属性”,在左侧属性面板中设置名称为:SpawnPoint。这个矩形所处的坐标将来就是我们的游戏主角显示的地方。保存TileMap.tmx,并将其更新到项目中。
- 回到Xcode中,在MainScene.swift中继续添加变量:player,我们的主角。
private var player:CCSprite!
- 修改didLoadFromCCB:方法,创建并添加主角player到场景中。
let objects=tiledMap.objectGroupNamed("Objects") //获取地图中的对象层
let spawnPoint=objects.objectNamed("SpawnPoint") //获取spawnPoint
let x=spawnPoint["x"] as! CGFloat //获取spawnPoint的x,y坐标
let y=spawnPoint["y"] as! CGFloat
player=CCSprite(imageNamed: "TileGameResources/Player.png") //创建player
player.anchorPoint=ccp(0.5,0.0)
player.position=ccp(x,y)
self.addChild(player)
-
运行一下,可以在游戏中看到主角了(如果看不到,那是因为你的主角没有布置到地图的左下角,原因前面已经说过了;也可以参考原教程的方式在didLoadFromCCB:方法中使用setViewPointCenter:方法在游戏启动后就将主角置于屏幕中央,该方法的实现方式在本文的下一节):
使忍者移动
我们的目的是在每次点击屏幕后使主角向点击的位置移动一个Tile块,要完成这个效果:
- 首先在MainScene.swift的didLoadFromCCB:方法中增加下面的代码:
userInteractionEnabled=true //允许当前节点与用户互动,如触摸点击
仍然是该方法中,在初始化tiledMap之后添加代码:
self.contentSize=tiledMap.mapSize //将场景与tileMap尺寸保持一致(这一步不设置后面点击移动会出问题,主要原因是场景尺寸是默认的屏幕尺寸在移动后造成无法覆盖住屏幕区域而当点击处于场景外时无法激活点击事件)
- 添加一个设置主角位置的方法,以后用它来设置主角的坐标:
private func setPlayerPosition(position:CGPoint){
player.position=position
}
- 重载touchBegan:withEvent:方法,实现当玩家点击一下屏幕时使主角向手指方向在横向或竖向移动一个Tile距离。
override func touchBegan(touch:CCTouch, withEvent event:CCTouchEvent){
let touchLocation=touch.locationInNode(self)
var playerPos=player.position
let diff=ccp(touchLocation.x-playerPos.x, touchLocation.y-playerPos.y)
if abs(diff.x)>abs(diff.y){
if diff.x>0{
playerPos.x += tiledMap.tileSize.width
}else{
playerPos.x -= tiledMap.tileSize.width
}
}else{
if diff.y>0{
playerPos.y += tiledMap.tileSize.height
}else{
playerPos.y -= tiledMap.tileSize.height
}
}
if playerPos.x <= tiledMap.mapSize.width*tiledMap.tileSize.width && playerPos.y <= tiledMap.mapSize.height*tiledMap.tileSize.height && playerPos.y >= 0 && playerPos.x >= 0{
setPlayerPosition(playerPos)
}
setViewPointCenter(player.position) //将玩家的坐标设置为视图的中心
}
- 这里最后一个方法setViewPointCenter:方法是将视图中心对齐到指定坐标,如可以用来实现将主角始终置于屏幕中央。方法代码(实现原理请参看原教程):
func setViewPointCenter(position:CGPoint){
let winSize=CCDirector.sharedDirector().viewSize()
var x=max(position.x, winSize.width/2)
var y=max(position.y, winSize.height/2)
x=min(x, (tiledMap.mapSize.width*tiledMap.tileSize.width)-winSize.width/2)
y=min(y, (tiledMap.mapSize.height*tiledMap.tileSize.height)-winSize.height/2)
let actualPosition=ccp(x,y)
let centerOfView=ccp(winSize.width/2, winSize.height/2)
let viewPoint=ccp(centerOfView.x-actualPosition.x, centerOfView.y-actualPosition.y)
self.position=viewPoint
}
- 到这里我们游戏场景就完全建立起来了,主角可以在地图中移动了。
Tile地图和碰撞检测
目前,我们的主角可以任意移动,小树大树都无法阻挡它,这显然不科学。接下来就要实现碰撞检测,让主角无法穿跃场景中那一圈小树,还要加一些花能让主角来采--这个游戏的名字终于有了,叫做:采花大盗。
- 回到Tiled地图编辑器,菜单 图层>添加图层 ,将新图层命名为“Meta”。这个层里会加入一些假的Tile代表一些特殊的Tile。
-
点击菜单 地图>新图块 ,点“浏览”找到素材包中的“meta_tiles.png”打开,设置如下。这个图因为Tile块有1像素的黑边,所以边距、间距都设置为1像素。
确定后,在右侧的图块中可以看到一红一绿的Tile了。
-
这二个块中,我们把半透明的红色块当作“可碰撞”的,选中“Meta”层,将红色块绘制在不能穿跃的物体上,如小树、大树、井等物体上,如下图所示:
-
接下来设置红色块的属性,我们在代码中就可以根据属性来识别这个块是否是可碰撞的。在右侧的“图块”面板,右键单击红色块点“图块属性”,在左侧的“属性”面板中添加一个新的属性,命名为:Collidable,值设置为“True”。
- 保存编辑后的地图并更新到项目中。回到Xcode的MainScene.swift中,添加类变量:
private var meta:CCTiledMapLayer!
在didLoadFromCCB:方法中添加代码:
meta=tiledMap.layerNamed("Meta") //获取Meta图层
meta.visible=false //只作碰撞检测用,所以不需要显示
- 添加一个用于将点坐标转换成Tile坐标的方法。我们的地图是由一个个Tile块组成的,本例中就是50块x50块(前面我们设置地图大小时就是50块x50块),所谓Tile坐标系就是由这些Tile块构成的一个坐标系,X轴表示Tile所在的列索引,Y轴表示Tile所在的行索引;值得注意的是,这个坐标系是将左上角作为坐标系原点的,左上角的Tile坐标为(0,0),右下角的坐标在本例中就是(49,49)。简单的讲,我们的目的是根据点坐标找出这个点所在的Tile块的行列索引。(如果还是不明白的请阅读原教程,我这里就不再拿图做说明了);代码如下:
func tileCoordForPosition(position: CGPoint)->CGPoint{
let x=position.x/tiledMap.tileSize.width
let y=tiledMap.mapSize.height-position.y/tiledMap.tileSize.height
return ccp(x,y)
}
- 将原来的setPlayerPosition:方法替换成如下:
private func setPlayerPosition(position:CGPoint){
let tileCoord=self.tileCoordForPosition(position) //将坐标转换成Tile坐标
let tileGid=meta.tileGIDAt(tileCoord) //获取Tile的GID
if tileGid != 0{
let properties=tiledMap.propertiesForGID(tileGid) //通过GID获取对应Tile的属性
if !properties.isEmpty{
if let collision=properties["Collidable"] as? String where collision=="True"{ //检查这个Tile是否有Collidable属性并是否设置成了“True”
return //如果是,就不能移动到这个位置,所以不更新player的坐标就直接退出方法
}
}
}
player.position=position
}
这里,将player的x,y坐标转换成Tile坐标,然后使用meta图层中的getTileGIDAt:方法来获取指定位置点的Tile的GID号。可以把GID理解成Tile的唯一的标识符。再通过GID获取对应的Tile块的各项属性,检测一下其属性中是否包含“Collidable”键,并且值为“True”,如果是,表示这个Tile是不能被行走的,通过禁止更新主角的坐标来防止其穿跃。
- 编译运行看效果,现在主角已经不能通过小树大树了。
- 好了,通过类似的方法,可以给场景布置一些花朵,然后使用之前还没有用过的绿色半透明Tile来指示那些花在哪里,在加上碰撞检测,实现采花大盗采花的效果已经变得很简单了,这里我就不再重复了。唯一需要指出的是在花朵被采集后如何移除花朵和绿色Tile?这个可以通过层CCTiledMapLayer的removeTileAt:方法来做到,如:meta.removeTileAt(tileCoord)。这一部份如果不明白的可以参考原教程。
- 原教程后面还有一点内容,不过都是是些细节上的东西且与TiledMap关系不大,我的这篇文章就不再补充了。
by Mandarava(鳗驼螺)