Learn IPhoneand iPad Cocos2d Game Delevopment》第10章 。
相册空间已满,无法直接贴站外图片。要查看图片,请点击链接。
使用 Tilemaps
接下来两章,我将介绍基于贴图的游戏世界。你也许玩过Ultima这样的角色扮演游戏,或者刚刚把你Facebook上的朋友加进了Farmville。那么我可以肯定,你已经玩过了使用tilemap技术的游戏。
在tilemap游戏中,图形由小图片组成,称作tiles(贴片),它们是紧挨在一起的;把它们放入一个个小格子里这就组成了我们的游戏世界。这个概念令人兴奋,因为相比把整个世界当成一个贴图来绘制,这样更节约内存,同时允许更多的变化。
本章将使用所有贴图种类中最简单的一种贴图:直角贴图,来介绍一般的贴图概念。它由正方形或矩形的贴片组成图形,并以顶视图的方式呈现游戏世界。例如Ultima系列一直以来都使用了贴图技术。Ultima1-5使用正方形贴片,顶视图视角;Ultima6-7 仍然使用直角贴图,但使用了半等角投影透视视角。Ultima8:Pagan,是整个系列中唯一使用等角投影贴图的游戏。等角投影贴图在下一章讨论。
我还会解释如何滚动一个tilemap地图,如何让一个贴片始终保持在地图中心,如何保持屏幕不会移出tilemap区域。触摸你不希望聚焦的贴片会导致滚动,这意味着你会学到如何判断被触摸的贴片是哪一个。
贴图是什么?
贴图Tilemaps是用一个个贴片去组成2D游戏世界的技术。仅仅用几张有着相同尺寸的图片就可以创建出庞大的世界地图。这意味着在大地图中使用贴图能有效地节省内存。这种技术应用于早期的电脑游戏。许多传统RPG类游戏用正方形的贴片创建精彩的游戏世界。这些Tilemaps游戏看起来如图所示:
点击打开链接
Tilemaps通常用编辑器生成,有一种名叫Tiled(QT) 的编辑器可以直接支持cocos2d。Tiled是免费的,开源,并且允许你在多个图层中编辑直角贴图和等角投影贴图。Tiled也允许你加入触发器区域和对象,以及编辑贴片属性——这样你可以在代码中判断贴片的类型。
提示:Qt指诺基亚Qt框架,其内置了Tiled。因为还有一个Tiled的java版,因此用Tiled(Qt)加以区别。java版Tiled已不再更新,但其中包含的几个特殊的功能仍然值得一看。但在这两章里,我使用和讨论的仍然是Tile(Qt)。
随着时间的推移,方块贴图技术也得到一些改进,通过使用另一种贴图技术——过渡贴片。例如,在紧挨草地贴片的地方,不直接使用水的贴片,而是使用额外的过渡贴片(例如,这个贴片中一边包含了水,一边包含了草地,中间是二者的分际线),这样便可以创建出一种更平滑的过渡效果。如果不这样做,你就要使用更多的贴片,花更多的心思考虑一个贴片如何才能过渡到另外的一个贴片,并让贴片种类保持在一定水平。过渡贴片是值得一提的。
上图中使用了许多过渡贴片。在其名为Desert(沙漠)的贴片集中只有4种地形的贴片:沙土、砾石(在tilemap的下半部分)、砖石(在左上部)、泥土(在右上部)。除了沙土之外的3种贴片,每一种都有12种额外的贴片用于过渡到沙土背景贴片。
贴片并不一定得是正方形;也可以创建矩形贴片的直角贴图。在亚洲地区的RPG游戏里,经常使用这种贴图,例如DragonQuest4-6。当使用直角透视的时候,这使设计者创建的对象看起来高比宽长。这制造并呈现了深度感。等角透视贴图则通过斜45度透视来加深这一点。它使用伪3D风格的贴片,使游戏世界获得视觉深度。等角透视贴图能够“欺骗”我们的大脑,仿佛这就是一个3D世界,尽管所有的图片仍然是平面的。等角透视贴图通过用一个个菱形的贴图达到深度感,并允许距离观察者较近的贴图遮挡住较远的。下图为一个等角透视贴图的例子。
点击打开链接等角透视贴图地图说明tilemaps地图不一定是平面的。使用方块贴图技术你可以达到这样的效果:仿佛每个贴片天生就严丝合缝地放在其他贴片上面。因此,Tiled支持多图层以创建一种类似3D的效果,如下图所示。
点击打开链接
在等角透视贴图中,能够使用分层的贴片,许多Farmville玩家视频展现这一效果。有的Farmville玩家仅仅在庄稼地里不用一砖一石就建造出房屋甚至摩天大厦。其实就是利用了人的错觉,用等角透视贴图很容易做到这一点。
使用Zwoptex准备图片
在本章的Tilemap01项目的Resouces/individualtile images目录中,你会找到许多方块状的贴片图像。把所有图片加到Zwoptex中,并把画布大小设为256*256——这个大小已经足够。点击Apply按钮,Zwoptex自动把它们安排妥当。结果显示如下:
点击打开链接注意,Zwoptex用随机顺序排放这些贴片。很不幸,写这本书的时候,Zwoptex1.04还不支持按名称排列贴片。否则,这个布局应当是按照贴片在磁盘中的文件名排放的。这个功能对许多Zwoptex用户来说很重要,因此在以后的版本可能会支持这个功能。查看你的Zwoptex版本是否支持这个功能,如果支持,首先分别编辑你的贴片文件,然后用Zwoptex从这些贴图文件中创建排序的贴图集。
你仍然可以使用随机排序的图片,但挡你添加或删除贴片并点击“Apply”之后,这些贴片又恢复到原来的位置。Zwoptex好像会对贴片进行随机重排。在使用CCSpriteBatchNode时,这根本不成为问题,因为你可以通过名称引用某个图片。
对于Tiled,则不一样了。保持贴片位置不变是至关重要的,因为Tiled是通过位置+偏移来引用贴片的。
这意味着,如果贴片改变了在贴图集中的位置,使用该贴图集的tilemap地图将完全变成另外一个样子。tilemap仍然会引用贴片在贴图集中的同一位置,但那个位置已经替换成一个水的贴片,而原来是一个草地贴片。
办法是加一些空的贴片填充到贴图集中(贴图集大小至少要和你需要的一样)。目的是简单地做出一个绘图空间。关键是把所有的空贴片加到Zwoptex以创建一个贴图集结构,其中包含了贴片所占据的空间,但贴片实际是空的。然后关闭Zwoptex,你不再需要它了,因为你可以用任何图片编辑程序打开这个贴图纹理集,并且在图片不透明的地方进行编辑。Zwoptex已经在贴图集中标明了每个贴片所在的原始位置。
如果你比我更有艺术天分,可能会用图形处理程序直接创建tilemap地图。那么你需要保证图形的背景必须是透明的。这可防止地图显示在游戏中时,在贴片的边缘出现缝隙。而且,所有的贴片都应是同样的宽和高,并且每个贴片之间的间隔也必须是固定的。
使用图形处理程序可能比仅仅创建一些空白的贴片,然后用Zwoptex对齐要花更多的时间。后者只需处理一次,而且更加快捷。
Tiled 地图编辑器
创建cocos2d使用的tilemaps地图,最常用的工具是Tiled地图编辑器。它生成的TMX文件被cocos2d引擎所支持。Tiled的免费的,在编写本书的时候,版本是0.5。你在它的主页www.mapeditor.org上就可以下载它。
如果你愿意支持Tiled的开发工作,请捐助该项目:
http://sourceforge.net/donate/index.php?group_id=161281.
新建 Tilemap
下载Tiled后,解压并安装。启动Tiled,选择View菜单并勾选Tilesets和Layers选项。这将显示图层列表,并在Tiled窗口右边显示当前贴片集。然后选择File ➤ New 创建一个 tilemap。这将弹出新地图对话框:点击打开链接
当前,Tiled支持直角贴图和等角透视贴图。地图的尺寸是以贴片数为单位,而不是像素。比如这里,新地图将包括30*20个贴片,贴片大小为32*32像素。贴片尺寸必须和你的贴片文件尺寸吻合,否则它们会被对齐。
新地图是空的,而且也没有加载任何贴片集。通过菜单 Map ➤ New 可以加载贴片集。这会打开 NewTileset dialog 对话框:
点击打开链接在其中,你可以浏览正确的贴片集图片。一个贴片集是一个图片文件名,在该图片中包含了多个等大贴片,因此你也可以称之为只包含等大图片的贴图集。
我将使用dg_grounds32.png贴片集。这些贴片由David E. Gervais 创作,并依据 Creative Commons License 发布, 这意味着你在尊重原作者的期刊下,你可以任意分享和编辑这些图片。在http://pousse.rapiere.free.fr/tome/index.htm 你可以下载到他的更多作品.
在上图中,我已经通过Browse按钮加入了dg_grounds32.png贴片集,它就位于Tilemap01工程的Resources目录下。如果你钩上“Use transparent color” 勾选框, 透明区域被绘制为粉红色(默认)。你可以保持不选择该选项,因为目前使用的贴片没有透明区域。
贴片的宽、高是每个贴片在贴片集中的大小。它们应当是32*32像素,等同于你创建地图时的贴片大小。Margin和Spacing分别指定贴片边框的宽度,以及贴片之间的间距。在这里,没有Margin和Spacing,我都设为0。
如果你用Zwoptex对齐贴片并创建了贴图集结构,你必须用Zwoptex的Margin和Spacing值来设定这两个值。默认,Zwoptex使用2个像素的边距。
载入贴片集图片时,确保其位于项目的资源目录下。还要确保把tilemapTMX文件保存到和贴片集文件的同一目录。否则Cocos2d无法加载贴片集,加载TMX文件时会导致运行时错误。这种错误是由于TMX文件引用贴片集时采用了相对路径。如果它们不在同一目录,当程序被安装到模拟器或设备后,cocos2d找不到图片,因为目录结构不存在。
编辑 Tilemap
贴片集加载后,你会看到一个空白地图,激发你的创意并完成一个tilemap地图。有一个办法可以去掉这个空白地图。使用一个默认的贴图地图是很好的开始。这里,我使用油漆桶工具(BucketFill)并选择青草贴片,因此我的地图现在是一片葱茏的草地:
点击打开链接Tiled有4中编辑模式,在工具栏最右边有4个图标:
1、Stamp Brush(快捷键B)
它允许你用贴片集中选择的贴片进行绘图;
2、Bucket Fill(快捷键F)
允许你用指定贴片填充区域;
3、Eraser(快捷键E)
擦除贴片;
4、Rectangular Select(快捷键R)
允许你选择一个范围,然后拷贝、粘贴选区内的贴片。
大部分时候,你在从贴片集中选择贴片,然后用Stamp Brush在地图上绘制。通过放置一个个贴片绘制基于贴片的游戏世界。
你还可以在多个图层中编辑贴片,通过在图层面板,你可以加入更多的图层。选择菜单Layer->Add Tile Layer可以创建新图层。用多图层的方式,你能在cocos2d中在地图的不同区域中切换。在TileMap01项目中,我用图层的方式,在冬夏之间进行切换。
你也可以用菜单Layer->Add Object Layer增加一个层,用于加入对象。在Tiled中对象是一些简单的矩形,你可以通过代码在其中绘制并读取。你可以用它们触发某些事件——例如,当玩家进入某个区域时产生怪。我随机加入了几个以显示它们用cocos2d代码是如何工作的。
Tiled还有一些功能是在右键菜单中。例如:刚才提到的矩形对象通过右键->RemoveObject可以删除掉。注意,只有Layers面板中的某个图层处于选中状态时,右键菜单才有效。
通过右键并点击属性项,你也可以编辑对象、图层、贴片的属性。使用菜单Layer➤ AddTile Layer,创建一个图层,将其命名为 GameEventLayer。选中 GameEventLayer, 选择 Map ➤ New Tileset ,加载 game-events.png(和 dg_grounds32.png在同一目录)。 其中有3个贴片。 在其中某个贴片上右键,选 Tile Properties, 然后添加一个isWater 属性, 如图所示。
点击打开链接提示: 注意每创建一个图层都会带来额外的开销,尤其是你把贴片放在多个图层的同一地方。这将导致两个图层都被绘制,并影响游戏性能。推荐尽可能地减少图层的数量。对大多数游戏来说2-4个图层足矣。加入新的tile图层后应随时注意游戏在设备上运行时的帧率。
现在,你可以在地图中使用这些带有isWater属性的贴片了。画出一条河吧。如果你想看看当前绘制的图层下面是什么,可以在Layer面板中通过滑块改变GameEventLayer的透明度,或者点击图层前面的“隐藏/取消隐藏”检查框。
确认在保存TMX tilemap地图前所有图层的检查框都是选中的。cocoas2d不会加载未勾选该检查框的图层。
最终,tilemap大概如图所示。
点击打开链接把它保存在Resources文件夹,和贴图集图片放在一起。
在Cocos2d中使用直角贴图
要在Cocos2d中使用TMX贴图,首先要将TMX文件和相应的贴图集图片文件加到Xcode项目的Resources组中。在TileMap01项目中,我加入了orthogonal.tmx和 dg_grounds32.png 、game- events.png。加载和显示tilemap地图是很简单的;只要在TileMapLayer类的init方法中加入以下代码:
CCTMXTiledMap* tileMap = [CCTMXTiledMaptiledMapWithTMXFile:@"orthogonal.tmx"];
[self addChild:tileMap z:-1 tag:TileMapNode];
CCTMXLayer* eventLayer = [tileMaplayerNamed:@"GameEventLayer"]; eventLayer.visible = NO;
CCTMXTiledMap类用TMX文件名进行初始化并以tag值为标记加到了self中。你也可以把它申明为成员变量。接下来通过layerNamed方法获得GameEventLayer对象。GameEventLayer是在Tiled中的图层名。因为gameevents 图层是通过代码方式来决定某些贴片的属性的,所以这个图层不应当显示出来。注意,如果你在Tiled中取消了某个图层的选择框,它也不会显示,但你也无法访问其贴片及贴片属性。
如果现在运行该项目,你会看到如下界面:
点击打开链接现在你还不能用这个地图做些什么,但我会改变这一点。在TileMap02项目,我会找到isWater贴片。我增加了ccTouchesBegan方法,如下所示,作用是判断玩家是否碰到了某个贴片。
-(void) ccTouchesBegan:(NSSet *)toucheswithEvent:(UIEvent *)event
{
CCNode* node = [self getChildByTag:TileMapNode];
NSAssert([node isKindOfClass:[CCTMXTiledMapclass]], @"not a CCTMXTiledMap");
CCTMXTiledMap* tileMap = (CCTMXTiledMap*)node;
// 把触摸点位置转换为贴片坐标
CGPoint touchLocation = [selflocationFromTouches:touches];
CGPoint tilePos = [selftilePosFromLocation:touchLocation tileMap:tileMap];
// 检查玩家是否碰到了水 (e.g., 通过贴片的 isWater 属性)
bool isTouchOnWater = NO;
CCTMXLayer* eventLayer = [tileMaplayerNamed:@"GameEventLayer"];
int tileGID = [eventLayer tileGIDAt:tilePos];
}
if (tileGID != 0) {
NSDictionary* properties = [tileMappropertiesForGID:tileGID];
if (properties) {
NSString* isWaterProperty = [propertiesvalueForKey:@"isWater"]; isTouchOnWater = ([isWaterPropertyboolValue] == YES);
}
// 如果玩家碰到了水,进行某些动作
if (isTouchOnWater) {
}
} else {
}
[[SimpleAudioEngine sharedEngine]playEffect:@"alien-sfx.caf"];
// 取得winter图层,并将它变成可视状态
CCTMXLayer* winterLayer = [tileMaplayerNamed:@"WinterLayer"]; winterLayer.visible =!winterLayer.visible;
获取CCTMXTiledMap 对象没有什么特别的地方。触摸位置首先转换为屏幕坐标,然后使用tilePosFromLocation方法很快就把屏幕坐标转换成贴片坐标(tilemap中的贴片索引)。
这里提到了全局标识GIDs的概念,它是指分配给每个贴片的唯一整型值(在一个tilemap中)。在地图中,贴片被以从1开始的连续数字编号。GID为0,表示空贴片。CCTMXLayer的tileGIDAt方法会根据指定的贴片坐标返回贴片的GID。
然后,从tilemap获得名为GameEventLayer的CCTMXLayer。这是那个定义了isWater贴片并以河流图片绘制过的图层。tileGIDAt方法返回贴片的唯一id。如果id为0,意味着在图层的这个位置没有任何贴片——如果这样,说明该贴片已经移出,则触摸到的贴片也不会是一个isWater贴片。
CCTMXTileMap有一个propertiesForGID方法,它返回一个NSDictionary,包含了该GID所代表的贴片的有效的属性——在Tiled中我们曾经编辑过这些属性。dictionary把所有的键值对都当作NSSTring储存。如果你想看看某个NSDictionary都有些什么,可以用CCLOG语句打印出来:
CCLOG(@"NSDictionary 'properties'contains:\n%@", properties);
这将在控制台窗口中打印类似如下的内容:
2010–08-30 19:50:52.344 Tilemap[978:207]NSDictionary 'properties' contains: {
isWater = 1;
}
你在处理tilempas的过程中,会与各种NSDictionary对象打交道。打印它们的内容可以让你快速查看NSDictionary或其他任何iPhoneSDK集合类中的内容 。有时,这是一种有用的技巧。
NSDictionary中的每个属性通过NSDictionary的valueForKey方法来检索,并返回NSString。要想从NSString转换为bool值,只需使用NSString的boolValue方法。类似地,NSString的intValue和floatValue方法可得到整数和浮点数。
ccTouchesBegan方法结尾,判断了玩家是否触碰到了水,是的话则发出某个声音。然后,检索WinterLayer图层并让其显示。季节变化当然没有这么简单。这只是演示如何利用Tiled中的多图层改变整个地图,而无需单独加载一个完整的tilemap地图。
如果只想单个贴片,可以使用removeTileAt和setTileGID方法移除或替换某个图层的贴片:
[winterLayerremoveTileAt:tilePos];
[winterLayer setTileGID:tileGID at:tilePos];
定位触摸的贴片位置
Locating Touched Tiles
在这两行代码中,我曾提到过tilePosFromLocation方法:
// 把触摸点位置转换为贴片坐标
CGPoint touchLocation = [selflocationFromTouches:touches];
CGPoint tilePos = [selftilePosFromLocation:touchLocation tileMap:tileMap];
首先,触摸位置被转换成屏幕坐标。这句代码以前就学习过,但我仍然重新罗列一下具体的实现代码以供参考:
-(CGPoint) locationFromTouch:(UITouch*)touch
{
CGPoint touchLocation = [touch locationInView:[touch view]];
return [[CCDirector sharedDirector]convertToGL:touchLocation];
}
-(CGPoint) locationFromTouches:(NSSet*)touches {
return [self locationFromTouch:[touchesanyObject]];
}
在把触摸点位置转换为屏幕坐标后,tilePosFromLocation方法被调用。它需要两个参数:触摸位置以及一个tileMap指针。这个方法包含了一些数学运算,我会作一些简单解释:
-(CGPoint) tilePosFromLocation:(CGPoint)locationtileMap:(CCTMXTiledMap*)tileMap
{
// 必须减去地图的位置,因为地图是滚动的
CGPoint pos = ccpSub(location,tileMap.position);
//必须转换为int,因为返回结果是整数
pos.x = (int)(pos.x / tileMap.tileSize.width);
pos.y = (int)((tileMap.mapSize.height *tileMap.tileSize.height - pos.y) /tileMap.tileSize.height);
CCLOG(@"touch at (%.0f, %.0f) is attileCoord (%i, %i)", location.x, location.y, (int)pos.x, (int)pos.y);
NSAssert(pos.x >= 0 && pos.y >= 0&& pos.x < tileMap.mapSize.width && pos.y <tileMap.mapSize.height,
@"%@: coordinates (%i, %i) out ofbounds!", NSStringFromSelector(_cmd), (int)pos.x, (int)pos.y);
return pos;
}
如果你曾经用过tilemaps,这些代码你会很熟悉,否则,你可能会一片茫然。等我来解释一下。首先是将触摸位置减去当前地图的位置。在后面的Tilemap03项目中使用了贴图滚动,因此地图的位置很多时候并不是0,0。
为了使视角能够向上(北)、右(东)进行滚动,你必须把地图位置改变为负数。因为tilemap从位置0,0开始,即屏幕左下角。地图的0,0点和屏幕的0,0点是重合的。如果你把地图移到100,100,看起来好像是把视点向左下移。你经常会以为自己正在移动视角,其实没有。移动的是tilemap图层,要向地图中心滚动,你必须把地图坐标向负轴方向偏移。
接下来是简单计算:要获得tilemap的偏移量(我们知道永远是负值),我们必须让触摸位置和tileMap.position相减。减去一个负数实际上是加上一个正数:
location(240, 160) – tileMap.position(-100,-100) = pos(340, 260)
因为地图图层从(0,0)移到了(-100,-100),而触摸位置在(240,160),这整个偏移就应当是(340,260)。
如果考虑进滚动的偏移量,我们就能得到贴片的坐标。另外,你要知道(0,0)贴片的贴片坐标是在地图的左上角。于此不同,屏幕坐标原点(0,0)却位于屏幕左下角,而地图坐标是从左上角开始。下图显示了一系列贴片的x,y坐标。这张截图是在Tiledjava版中启用 View ➤ Show Coordinates菜单得到的,这个功能Tiled Qt版不支持。
点击打开链接因此为免混淆,使用下行代码计算贴片的x坐标:
pos.x = (int)(pos.x / tileMap.tileSize.width);
tileMap.tileSize属性是贴图集中贴片大小(在这里是32*32)。如果触摸点的x坐标是340,则上面的代码会计算:
340 / 32 = 10.625
这当然不对,我们所有的贴片坐标都没有小数!因为触摸点位于贴片的内部(例如在一个32*32的方块内)。简单地把计算结果去除小数部分转换成int值:
pos.x = (int)10.625 // pos.x == 10
这个转换把小数点后面的数字消除。把小数部分消去是安全的,因为它们不但无用——反而有害。如果你不去掉小数部分,直接使用非整型的坐标检索一个贴片,例如10.625,将导致一个运行时错误,因为只有x坐标为10和11的贴片,不存在贴片x坐标为10.625的贴片。
计算贴片的y坐标则更复杂一些:
pos.y = (int)((tileMap.mapSize.height *tileMap.tileSize.height - pos.y) / tileMap.tileSize.height);
注意括号的使用,这将确保最后才进行除运算。如果使用数字这个公式可能更容易理解:
pos.y = (int)((20 * 32 – 260) / 32)
在上式中,tileMap.mapsize是30*20个贴片,而每个贴片为32*32像素。
用tileMap.tileSize.height乘以tileMap.mapSize.height,得到tilemap的像素高度。这是必需的,因为tilemap的y轴是从上到下开始计算,而屏幕的y轴是从下到上的。通过计算出tilemap的最下端的y轴坐标,然后减去当前y坐标260,就能得到当前触点在tilemap中的y坐标(像素)。由于这个结果是像素坐标,你需要除以tileSize.height然后取整,以再次折算成贴片坐标。
CCLOG和NSAssert用于在控制台窗口查看计算结果,并确保贴片坐标不会出现不合理的值。这是一种学习手段,也是一种预防措施。
代码优化和提高可读性
由于地图尺寸是固定不变的,你可以通过在类中增加一个成员变量来减少计算量,用该变量来保存地图的像素高度:
floattileMapHeightInPixels;
在init方法中,在地图被加载的时候,计算一次tileMapHeightInPixels就行了:
CCTMXTiledMap*tileMap=[CCTMXTiledMap tiledMapWithTMXFile:@"orthogonal.tmx"];
tileMapHeightInPixels= tileMap.mapSize.height * tileMap.tileSize.height;
现在你可以把计算公式进行重写,这样每次调用tilePosFromLocation方法时能够节省一次乘法运算:
pos.y =(int)((tileMapHeightInPixels - pos.y) / tileMap.tileSize.height);
当然,这只能导致一个很小的性能改善,不能帮你赢得任何性能优化的奖项。但通过一个可读性更好的变量名,能使计算公式更加简单,易于阅读。
使用 Object Layer
本章,我创建了一个包含了objectlayer(图层名ObjectLayer)的例子:orthogonal.tmx。使用Layer->Add Object Layer菜单,可以创建Object层。然后点击tilemap并在其中绘制一个矩形框。我觉得objectlayer这个名字有点让人混淆,因为绝大部分游戏其实是把它当作一个“陷阱区域”使用,而不是真正意思上的对象。
在Tilemap03项目中,我在ccTouchesBegan方法中增加了许多代码与objectlayer互动。下面列出了其中一部分代码(在isWater判断之后):
// 检查是否触摸到某个矩形对象
CCTMXObjectGroup*objectLayer = [tileMap
objectGroupNamed:@"ObjectLayer"];
boolisTouchInRectangle = NO;
int numObjects = [objectLayer.objectscount];
for (int i =0; i < numObjects; i++) {
NSDictionary* properties = [objectLayer.objectsobjectAtIndex:i]; CGRect rect = [self getRectFromObjectProperties:properties
tileMap:tileMap];
if (CGRectContainsPoint(rect, touchLocation)) {
isTouchInRectangle = YES;
break;
}
}
因为object layers是一种特别的层,你不能用tilemap的layerNamed方法获取objectlayer。在cocos2d,object layer其实是CCTMXObjectGroup类,这又是一个命名不当的例子,因为Tiled把它引用为objectlayer,而不是object group。通过tilemap的objectGroupNamed方法你可以获得object layer对应的CCTMXObjectGroup,你只需要指定该objectlayer在Tiled中的名字。
紧接着,我遍历了objectLayer的objecdts数组,它包含了由NSDictionary对象组成的列表。想起来了吗?在前面我们曾经提到过的,tilemap的propertiesForGID方法返回的是NSDictionary属性集,这里和它其实是同样的东西。但有一点不同,propertiesForGID方法返回的是只读的NSDictionary。
这些NSDictionary只是简单地包含了每个矩形框的坐标。用getRectFromObjectProperties方法可以返回这个矩形:
-(CGRect)getRectFromObjectProperties:(NSDictionary*)dict tileMap:(CCTMXTiledMap*)tileMap{
float x, y, width, height;
x = [[dict valueForKey:@"x"] floatValue]+ tileMap.position.x;
y = [[dict valueForKey:@"y"] floatValue]+ tileMap.position.y;
width = [[dict valueForKey:@"width"]floatValue];
height = [[dict valueForKey:@"height"]floatValue];
return CGRectMake(x, y, width, height);
}
键x,y,width,height的值由Tiled赋值。通过valueForKey可以轻易地检索它们的值,然后用floatValue方法把它们从NSString转换为浮点值。x,y值需要加上tileMap的位置,因为矩形需要跟随tilemap一起移动。最后,调用CGRectMake函数返回一个CGRect。
ccTouchesBegan方法中剩下来的代码简单地通过CGRectContainsPoint方法判断触摸点是否包含在这个矩形区域内。如果是,isTounchInRectangle标志置为true,并且退出for循环。因为没有必要再判断其他矩形是否包含了触点了。在ccTouchesBegan最后,isTouchInRectangle标志被用于判断是否在触点位置显示特殊效果。如果你触摸到矩形范围,这段代码会产生粒子爆炸效果:
if (isTouchOnWater) {
[[SimpleAudioEnginesharedEngine] playEffect:@"alien-sfx.caf"];
} else if(isTouchInRectangle) {
CCParticleSystem* system =[CCQuadParticleSystem
particleWithFile: @"fx-explosion.plist"];
system.autoRemoveOnFinish= YES;
system.position= touchLocation;
[selfaddChild:system z:1];
}
绘制Object Layer
当你运行Tilemap03项目时,你会注意到对象层的矩形框已经绘制在tilemap上了。
点击打开链接这不是tilemaps或者对象层的标准特性。这是用OpenGL ES代码绘制的矩形框。每个CCNode都会有一个–(void)draw 方法,你可以覆盖该方法,加入自己的OpenGL ES代码。我习惯于用这些代码进行调试,画一些看得见的线、圆、或者矩形,以便于碰撞测试或者查看物体间距离。在这个例子里通过这种方法,能够实实际际地看见对象层的位置。用可见的方式胜于在调试器中查看坐标值,因为可视化的方式要比比较和计算数值更直观。
-(void) draw 方法会在播放帧时自动调用。但是,要有限度地使用该方法去改变节点的属性,因为这会对节点的绘制造成影响。下面是TileMapLayer类的draw方法。
-(void) draw
{
CCNode* node = [self getChildByTag:TileMapNode];
NSAssert([node isKindOfClass:[CCTMXTiledMapclass]], @"not a CCTMXTiledMap");
CCTMXTiledMap* tileMap = (CCTMXTiledMap*)node;
// 获取对象层
CCTMXObjectGroup* objectLayer = [tileMapobjectGroupNamed:@"ObjectLayer"];
// 线宽:3 像素
glLineWidth(3.0f);
glColor4f(1, 0, 1, 1);
int numObjects = [[objectLayer objects] count];
for (int i = 0; i < numObjects; i++) {
NSDictionary* properties = [[objectLayerobjects] objectAtIndex:i]; CGRect rect = [selfgetRectFromObjectProperties:properties
tileMap:tileMap];
[self drawRect:rect];
}
glLineWidth(1.0f);
glColor4f(1, 1, 1, 1);
}
首先,通过tag获得一个tilemap,并调用objectGroupNamed方法获得对象层的CCTMXObjectGroup对象。然后把线宽设为3个像素(glLineWidth方法),颜色设置为紫色(glColor4f方法)。这将影响后续的OpenGLES画线的线宽和颜色——不仅仅是当前方法,也会对其他用OpenGL ES节点绘制的行为有影响(例如,任何定义在CCDrawingPrimitives.h头文件中的用于画线、圆、多边形的方法)。这也是为什么我在画完之后又重置glLineWidth和glColor4f的原因。在OpenGL代码中保持使用前的状态是一种良好的风格,否则,你可能会改变其他绘制代码的输出结果。OpenGL采用了状态机制,因此你所改变的每个设置都会被记住并且会影响到下一个绘制方法。为此,你对OpenGL设置进行改变之后,应当在你绘制完毕后把它们设置回默认状态。
注意: draw方法中的代码总是在z顺序为0的地方绘制。而且它会在所有z顺序为0的其他节点之前绘制。这意味着任何OpenGLES节点都会被z顺序0的其他节点所覆盖。为此,我不得不把tileMap放在了z顺序-1,因为矩形框要绘制在tilemap之上。
我遍历了所有对象层中的对象,从他们的NSDictionary属性集中获得对象的CGRect,然后传递给drawRect方法。但不幸的是,cocos2d遗漏了这个有用的方法,因此我使用ccDrawLine简单实现了这个方法:
drawn before all other nodes at z-order 0, whichmeans that any
-(void) drawRect:(CGRect)rect {
// 矩形由4个点线构成:pos1、pos2、pos3、pos4
pos1 = CGPointMake(rect.origin.x,rect.origin.y);
pos2 = CGPointMake(rect.origin.x, rect.origin.y+ rect.size.height);
pos3 = CGPointMake(rect.origin.x +rect.size.width, rect.origin.y +
rect.size.height);
pos4 = CGPointMake(rect.origin.x +rect.size.width, rect.origin.y);
ccDrawLine(pos1, pos2); ccDrawLine(pos2, pos3);ccDrawLine(pos3, pos4); ccDrawLine(pos4, pos1);
}
用CGPoint创建了矩形的4个顶点,然后用ccDrawLine方法把两点连成线段。你可能需要把这个方法放在安全的地方并记住,因为很可能再次用到它。
注意,draw方法和drawRect方法用 #ifdef DEBUG和 #endif 语句包括起来。这表示在编译发布版本时对象层的矩形不会被绘制,因为我只需要在调试时需要它们,而最终用户并不会看见它们。
#ifdef DEBUG
-(void) drawRect:(CGRect)rect {
...
}
-(void) draw{
}
#endif
滚动地图
终于来到最后的部分:滚动。实际上这很简单,因为只需移动CCTMXTiledMap就行了。在Tilemap04工程中,我在捕捉到了触摸点的贴片坐标之后,在ccTouchesBegan方法中调用了centerTileMapOnTileCoord方法:
ccTouchesBegan:(NSSet *)toucheswithEvent:(UIEvent *)event{
...
// 从触摸点获得贴片坐标
CGPoint touchLocation = [selflocationFromTouches:touches];
CGPoint tilePos = [selftilePosFromLocation:touchLocation tileMap:tileMap];
// 移动tilemap,使得触摸点位于屏幕的中心
[self centerTileMapOnTileCoord:tilePos tileMap:tileMap];
...
}
下面是 centerTileMapOnTileCoord 方法, 它移动了tilemap并使触摸到的贴片居于屏幕的中心,并且如果地图已经到达屏幕边缘则停止滚动。
-(void) centerTileMapOnTileCoord:(CGPoint)tilePostileMap:(CCTMXTiledMap*)tileMap{
// 把 tilemap 中心对齐指定的贴片位置
CGSize screenSize = [[CCDirector sharedDirector]winSize];
CGPoint screenCenter =CGPointMake(screenSize.width * 0.5f, screenSize.height *
0.5f);
// 贴片坐标以左上角为坐标原点
tilePos.y = (tileMap.mapSize.height - 1) -tilePos.y;
// 屏幕坐标以左下角为原点
CGPoint scrollPosition = CGPointMake(-(tilePos.x* tileMap.tileSize.width),
-(tilePos.y * tileMap.tileSize.height));
// 贴片中心和屏幕中心的偏移点
scrollPosition.x += screenCenter.x -tileMap.tileSize.width * 0.5f;
scrollPosition.y += screenCenter.y -tileMap.tileSize.height * 0.5f;
// 确保地图滚动到地图边缘的时候停止
scrollPosition.x = MIN(scrollPosition.x, 0);
scrollPosition.x = MAX(scrollPosition.x,-screenSize.width);
scrollPosition.y = MIN(scrollPosition.y, 0);
scrollPosition.y = MAX(scrollPosition.y,-screenSize.height);
CCAction* move = [CCMoveToactionWithDuration:0.2f position: scrollPosition];
[tileMap stopAllActions];
[tileMap runAction:move];
}
计算出屏幕中心位置后,我改变了tilePos的y坐标,因为tilemap的y轴方向是从上到下。而屏幕的y轴方向是从下向上。实际上,我转换了tilePos的y轴,使它的方向从下向上。另外,我把地图的高度减去一,因为贴片坐标实际上是从0开始。也就是说,如果地图的高度是10,它的贴片坐标只能是0-9之间。
接下来,创建了一个scrollPosition变量,用于计算地图将移动到的位置。第1步是把贴片坐标和地图的贴片大小相乘。你可能奇怪,为什么我让贴片的像素坐标取负值。因为如果我想将贴片从右上端向左下运动,必须减少地图的坐标值。
接着,修改了scrollPosition的坐标,使贴片与屏幕中心点对齐。你要考虑到贴片自己的中心是位于贴片大小一半的地方,需要从screenCenter中扣除。
通过O-C的MIN和MAX宏,我们保证了scrollPosition的位置一定在地图的边界范围内,不会显示任何地图以外的东西。MIN和MAX返回两个参数中最小和最大的值,它们比使用if…else语句进行条件赋值要简练。
最后,用一个CCMoveTo动作滚动地图节点,以使触摸到的贴片位于屏幕中央。这将使地图滚动到你轻击贴片的位置。你可以用同样的方法滚动地图到任何贴片上——比如,玩家所在的位置。
小结
你现在已经对tilemaps有一个不错的概念了,并且知道如何用Tiled地图编辑器创建多图层的tilemap,并在游戏中运用图层属性。
用cocos2d加载和显示tilemap是件简单的事情,但获取贴片和对象层,读取并修改它们的属性则显得有些复杂。你也学到了如何判断触摸点的贴片坐标,并且使用贴片坐标进行地图的滚动,以便触摸点贴片位于屏幕的中央。
我还讲解了一点点的OpenGL ES编程知识,用它我们可以自己在tilemap上绘制对象层矩形,以便调试。