zoukankan      html  css  js  c++  java
  • Cocos2d开发系列(九)

    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上绘制对象层矩形,以便调试。



  • 相关阅读:
    flock对文件锁定读写操作的问题 简单
    hdu 2899 Strange Fuction(二分)
    hdu 2199 Can you solve this equation? (二分)
    poj 3080 Blue Jeans (KMP)
    poj 2823 Sliding Window (单调队列)
    poj 2001 Shortest Prefixes (trie)
    poj 2503 Babelfish (trie)
    poj 1936 All in All
    hdu 3507 Print Article (DP, Monotone Queue)
    fzu 1894 志愿者选拔 (单调队列)
  • 原文地址:https://www.cnblogs.com/encounter/p/2188457.html
Copyright © 2011-2022 走看看