本教程基于子龙山人翻译的cocos2d的IPHONE教程,用cocos2d-x for XNA引擎重写,加上我一些加工制作。教程中大多数文字图片都是原作者和翻译作者子龙山人,还有不少是我自己的理解和加工。感谢原作者的教程和子龙山人的翻译。本教程仅供学习交流之用,切勿进行商业传播。
子龙山人翻译的Iphone教程地址:http://www.cnblogs.com/zilongshanren/archive/2011/04/11/2012852.html
Iphone教程原文地址:http://www.raywenderlich.com/1163/how-to-make-a-tile-based-game-with-cocos2d
程序截图:
这篇教程是《使用cocos2d-x制作基于Tile地图的游戏》的第二部分。在上一个教程中,我们创建了一个简单的基于tiled地图的游戏,里面有一个忍者在沙漠里寻找可口的食物!
在第一部分教程中,我们介绍了如何基于tiled创建地图,怎样把地图增加到游戏中去,以及如何滚动地图来跟随主角移动、还有如何使用对象层。
在这部分教程中,我们将会介绍如何在地图中制作可以碰撞的区域,如何使用tile属性,如果收集游戏物品并且动态地修改地图、如何确保你的忍者不会吃得太饱!
因此,让我们继续我们上篇教程所学并且让它更像一个真实的游戏吧!
tiled地图和碰撞检测
你可能已经注意到了,目前我们的忍者可以毫无阻拦地穿过墙壁和障碍物。他是一个忍者,但是即使是真正的忍者,他也没这么厉害啊!
因此,我们需要找到一种方法,通过把一些tile标记成“可碰撞的”,这样的话,我们就可以防止玩家穿过那些点的位置。有很多方法可以做得到(包括使用对象层),但是,我想向你们展示一种新的技术。我认为它更高效,并且也是一次好的学习锻炼--使用一个元层(meta layer)和层属性。
让我们开始动手吧!再一次启动Tiled软件,点击“图层\添加图层”,并且命名为“Meta”,然后选择OK。我们将在这个层里面加入一些假的tile代表一些“特殊tile”。
因此,现在我们需要增加我们的特殊tile。点击“地图\新图块...”,在你的Resources文件夹下面找到mate_tiles.png,然后选择打开。点击确定。
这时,你可以在图块区域看到一个新的标签。打开它,而且你会看到2个tile:一个红色的和一个绿色的。
这些tile并没有什么特殊的东西--我只是制作了一个简单的图片,里面包含了一个红色的和一个绿色的半透明tile。接下来,我们把红色的tile当作是“可碰撞的”(后面我们会用到绿色的),然后,合适地绘制我们的场景。
因此,确保Meta层被选中,选择图章工具,选择红色的tile,然后把任何你不想让忍者通过的地图都涂一遍。当你做完的时候,应该看起来像下面的图示一样:
接下来,我们可以设置tile的属性,这样的话,我们在代码中就可以识别这个tile是“可以碰撞的(穿不过去的)”。在图块上右键,点击图块属性。如下图所示。
然后然后添加Collidable属性并设置为True。点击确定。然后保存地图并且更新到工程的Resource文件夹(假如你不是直接编辑工程中的TMX文件的话)。
然后往TileMapLayer类中添加一个变量声明;
CCTMXLayer meta = null;
在init方法的loading background后面添加以下代码:
this.meta = tileMap.layerNamed("Meta");
meta.visible = false;
并且添加一个方法:
CCPoint tileCoordForPosition(CCPoint position) { int x = Convert.ToInt32(position.x / tileMap.TileSize.width); int y = Convert.ToInt32((tileMap.TileSize.height * tileMap.MapSize.height - position.y) / tileMap.TileSize.height); return new CCPoint(x, y); }
好了,让我们先停一会儿。像之前一样,我会meta层声明了一个成员变量,而且从tile map中加载了一个引用。注意,我们把这个字当作是不可见的,因为我们并不想看见这些对象,它们的存在只是为了说明,那个区域是可以碰撞的。
接下来,我们增加一个新的帮助方法,这个方法可以帮助我们把x,y坐标转换成”tile坐标“。每一个tile都有一个坐标,从左上角的(0,0)开始,到右下角的(31,31)。(本例中,地图的大小是32×32)
上面的截屏是java版本的tiled界面。我现在用的地图版本是不支持显示坐标的。可以显示方格,不过,只是说下原理。不管怎么说,我们将要使用的一些功能会使用tile坐标,而不是x,y坐标。因此,我们需要一种方式,将x,y坐标转换成tile坐标。这正是那个函数所需要做的。
获得x坐标非常容易--我们只需要让它除以一个tile的宽度就可以了。为了得到y坐标,我们不得不翻转一些东西,因为,在cocos2d里面(0,0)是在左下角的,而不是在左上角。
接下来,把setPlayerPosition替换成以下内容:
void setPlayerPosition(CCPoint position) { CCPoint tileCoord = this.tileCoordForPosition(position); int tileGid = meta.tileGIDAt(tileCoord); if (tileGid != 0) { Dictionary<string, string> properties = tileMap.propertiesForGID(tileGid); if (properties != null) { string collision = String.Empty, temp; if (properties.TryGetValue("Collidable", out temp)) { collision = temp; } if (collision.Length > 0 && collision.Equals("True")) { return; } } } player.position = position; }
在这里,我们把玩家的x,y坐标转换成tile坐标。然后,我们使用meta层中的tileGIDAt方法来获取指定位置点的GID号。
对了,什么是GID呢?GID代表”全球唯一标志符“(我个人意见)。但是,在这个例子中,我认为它只是我们使用的tile的一种标识,它可以是我们想要移动的红色区域。
当我们使用GID来查找指定tile的属性的时候。它返回一个属性字典,因此,我们可以遍历一下,看是否有”可碰撞的“物体被设置成”true“,或者是gij仅仅就是那样。编译并运行工程,因此还没有设置玩家的位置。
就这么多!编译并运行程序,它将会向你展示,现在你不能够通过那些红色的tile组成的地方了吧:
PS:这里在做教程的时候发现了点问题。就是地图精度的问题。看下图
这里我的Player的坐标如果是在网格靠近下面话,那么,在碰撞判定的时候,由于tileCoordForPosition函数的精度计算问题。靠近下面的话,计算的时候四舍五入。那么,就会发现障碍物的坐标上升了一行。所以上图的player的框我设置略微靠近上面的线。这个网格可以在“视图-显示网格”可以使之显示出来。
动态修改Tiled Map
目前为此,我们的忍者已经有一个比较有意思的冒险啦,但是,这个世界有一点点无趣。而且简单无任务事可做!加上,我们的忍者看起来比较贪吃,而且背景将会随着玩家移动而移动。因此,让我们创建一些东西让忍者来玩吧!
为了使之可行,我将不得不创建一个前景层,这样做可以让用户收集东西。那样的话,我们仅仅从前景层中删除不用的tile(当tile被玩角拾取的时候),这个过程中,背景将会随之移动。
因此,打开地图,选择”图层\添加图层...“,把这个层命名为”Foreground“,然后选择OK。确保前景层被选择,而且增加一对可以拾取的物品在游戏中。我喜欢放置一些向西瓜或者别的什么东西。随意放置些东西吧。
现在,我们需要把这些tile标记成可以拾取的,类似的,参照我们是如何把tile标志成可以碰撞的。选择Meta层,转换到Meta_tiles。现在,我们需要使这些tile可以拾取。
接下来,我们需要为tile增加属性,这样把它标记成可拾取的。点键点击图块选项卡里的绿色的tile,然后右键选择“属性...”,再增加一个新的属性,命名为“Collectable”,值设置为“True”。
返回TIleMapLayer类并且增加一个变量声明;
CCTMXLayer foreground = null;
同时,相应地修改TIleMapLayer类:
//在init方法,loadingbackground后面添加 foreground = tileMap.layerNamed("Foreground"); //在setPlayerPosition方法判断如果是墙壁就返回的if语句后面添加 string collectable = String.Empty; if (properties.TryGetValue("Collectable", out temp)) { collectable = temp; } if (collectable.Length > 0 && collectable.Equals("True")) { meta.removeTileAt(tileCoord); CCSprite sprite = foreground.tileAt(tileCoord); if (sprite != null) sprite.visible = false; }
这里是一个常用的方法,用来保存前景层的句柄。不同之处在于,我们测试玩家正朝之移动的tile是否含有“Collectable”属性。如果有,我们就使用removeTileAt方法来把tile从mata层和前景层中移除掉。编译并运行工程,现在你的忍者可以尝尝西瓜的滋味啦!
创建一个计分器
我们忍者非常高兴地吃西瓜啦,但是,作为一个游戏玩家,我们想知道自己到底吃了多少个西瓜。你懂的,我们并不想让他吃得太胖。
通常的做法是,我们在层上面添加一个label。但是,等一下:我们在不停地移动这个层,那样的话,label就会看不到了,怎么办?
这是一个非常好的机会,如果在一个场景中使用多个层--这正是我们现在面临的难题。我们将保留TileMapLayer层,但是,我们会再增加一个TileMapHud层来显示我们的label。(Hud意味着Heads up display,大家可以google一下,游戏中常用的技术)
当然,这两个层之间需要一种方式联系起来--Hud层应该知道什么时候忍者吃了一个西瓜。有许许多多的方式可以使2个不同的层相互通信,但是,我只介绍最简单的。我们在HelloWorld层里面保存一个HelloWorldHud层的句柄,这样的话,当忍者吃了一个西瓜就可以调用Hud层的一个方法来进行通知。
因此,在TileMapScene.cs里面增加下面的代码:
class TileMapHud : CCLayer { CCLabelTTF label; public override bool init() { if (!base.init()) return false; CCSize winSize = CCDirector.sharedDirector().getWinSize(); label = CCLabelTTF.labelWithString("0", "Arial", 18); label.Color = new ccColor3B(0, 0, 0); int margin = 10; label.position = new CCPoint(winSize.width - label.contentSize.width / 2 - margin, label.contentSize.height / 2 + margin); this.addChild(label); return true; } public void numCollectedChanged(int numCollected) { label.setString(numCollected.ToString()); } public static new TileMapHud node() { TileMapHud layer = new TileMapHud(); if (layer.init()) return layer; return null; } }
一切很明了。我们的第二个层从CCLayer派生,只是在它的右下角加了一个label。现在要做的是在TileMapLayer类里面添加一个声明,作为Hud层的引用,并且添加这样一个方法,作为这个TileMapLayer的初始化用。
int numCollected = 0; TileMapHud hud = null; public static TileMapLayer initWithHudLayer(TileMapHud hud) { TileMapLayer ret = new TileMapLayer(); if (ret.init()) { ret.hud = hud; return ret; } return null; }
现在修改TileMapScene的构造函数如下:
public TileMapScene() { TileMapHud hud = TileMapHud.node(); this.addChild(TileMapLayer.initWithHudLayer(hud)); this.addChild(hud); }
并且修改setPlayerPosition方法。在处理tile是否含有“Collectable”属性,并且属性为True的时候。
this.numCollected++; hud.numCollectedChanged(numCollected);
编译并运行,如果一切ok,你将会在屏幕右下角看到统计忍者吃西瓜的Label。
来点音效和音乐
如果没有很cool的音效和背景音乐的话,这就不能算作是一个完整的游戏教程了。
增加音效和音乐非常简单,先添加CocosDenshion.dll的引用,只需在TileMapLayer类作如下修改:
//在Init添加如下: //Effect preload SimpleAudioEngine.sharedEngine().preloadEffect(@"Resources/hit"); SimpleAudioEngine.sharedEngine().preloadEffect(@"Resources/move"); SimpleAudioEngine.sharedEngine().preloadEffect(@"Resources/pickup"); SimpleAudioEngine.sharedEngine().playBackgroundMusic(@"Resources/TilesMap",true); // In case for collidable tile SimpleAudioEngine.sharedEngine().playEffect(@"Resources/hit"); // In case of collectable tile SimpleAudioEngine.sharedEngine().playEffect(@"Resources/pickup"); // Right before setting player position SimpleAudioEngine.sharedEngine().playEffect("Resources/move");
现在,我们的忍者可以开怀大吃了!
何去何从?
通过这个教程的学习,你对cocos2d里面的tiled map的使用,应该有一个非常好的理解了。这里有这个教程的完整源代码(http://dl.dbank.com/c02vzbkoyl)。
接下来,继续第三部分<cocos2d-x for wp7>使用cocos2d-x制作基于Tile地图的游戏:加入敌人和战斗(三)
如果你看了这个教程,有什么好的意见或建议,可以自由发言,谢谢!