在这篇教程里,我们会讲解如何使用cocos2d和Tiled Map Editor创建一个基于tiled map的游戏.作为例子,我们会制作一个小游戏.游戏的主要内容是一个忍者在沙漠里寻找可口的西瓜吃.
这篇教程主要学习的内容有:如何创建Tiled Map;如何将地图载入到游戏内;如何让地图跟随玩家滚动;如何使用对象层.
下一节里,我们再介绍:如何在地图上制作可碰撞区域;如何使用tile属性;如何制作可碰撞物体和动态改变地图;如何确定你的小忍者没有吃撑:)
当然,如果你是个iphone开发新手,作为基础知识的准备,我建议你先阅读一下How To Make A Simple iPhone Game with Cocos2D Tutorial Series.
创建游戏骨架
下面我们要创建游戏骨架.并且准备好需要的资源文件.
打开XCode,File\New Project,选择cocos2d Application创建一个新工程.
接下来,下载这个zip文件,这里面包含了游戏需要的资源:
将下载到的资源解包拖入xcode的resources组,记得选中”Copy items into destination group’s folder(if needed)”.
这样,一切准备就绪.
创建游戏地图
Cocos2d支持使用开源软件Tiled Map Editor(貌似被伟大的墙挡住了,天朝的用户可以直接访问它在sourceforge的项目主页,杯具!)创建的TMX格式地图.
如果你访问上面的链接,你会发现有两个版本可用.一个使用Qt应用程序框架编写,另一个使用Java编写.这是因为最初Tiled Map Editor使用Java编写,后来移植到Qt框架上.使用哪个版本都可以.在这篇教程里,我们以使用Qt版本的为例,因为它将作为今后的开发主线.有些 人喜欢使用java版本,是因为还有些老版本上的功能尚未移植到Qt框架上.
运行Tiled Map Editor,新建一个地图.填写如下对话框
在orientation选项内,可以选择Orthogonal(平面直角)或Isometric(45度视角,传说中的2.5D),这里选择Orthogonal.
接下来需要设置地图大小.这里的数值是指有多少格tiled元件,并不是像素.选择50×50即可.
最后,确定tile元件的大小.根据美工提供的元件大小设置.这个教成立,我们使用32×32的大小.
接下来,将tile元件添加到地图内绘制地图.在Map菜单许做呢New Tileset,填写下面的对话框.
点击Browser从电脑里找到tmw_desert_spacing.png文件(下载的资源包内)
保持长宽数据为32×32.
对于margin和spacing,我没有找到文档说明,但是我认为它们的意义是:
* Margin 表示当前tiled在开始搜索实际像素时应该忽略多少个像素 (译者注:我理解应该是两个tiled元件之间的间距)
* Spacing 表示读取下一个tiled数据后应该向前推进多少个像素(译者注:我理解应该是两个tiled元件之间的空隙,不过,这好像与Margin重复了…)
如果你仔细观察tmw_desert_spacing.png,你会发现每个tiled元件之间都有1像素的黑边.这样的图片需要将margin和spacing设置为1
点击OK,tiled元件将被显示在Tilesets窗口内.现在你可以开始绘制地图了.点击工具条上的Stamp(印章)图标,选择一个tiled元件,在地图内需要的位置点击放置地图元件.
按上面的方法绘制一张地图. 至少在地图上绘制几个建筑,因为后面我们要用到它们.
一些快速技巧最好记住:
* 你可以一次添加多个tiled元件到地图里.(画一个方块选中多个tiled元件).
* 可以使用油漆筒按钮填充地图背景.
* 可以在view菜单里放大缩小地图.
画好第图后,双击Layers窗口里的层(一般是取名为Layer1),改名为Background.在File菜单内选择Save,将地图保存到xcode项目内,取名tiledmap.tmx
将Tiled Map添加到Cocos2d Scene中
将刚才创建的tmx文件拖入项目resources内.打开HelloWorldLayer.h文件,添加一些代码.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | #import "cocos2d.h" // HelloWorld Layer @interface HelloWorld : CCLayer { CCTMXTiledMap *_tileMap; CCTMXLayer *_background; } @property (nonatomic, retain) CCTMXTiledMap *tileMap; @property (nonatomic, retain) CCTMXLayer *background; // returns a Scene that contains the HelloWorld as the only child +(id) scene; @end |
在HelloWorldLayer.m添加代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | // Import the interfaces #import "HelloWorldScene.h" // HelloWorld implementation @implementation HelloWorld // Right after the implementation section @synthesize tileMap = _tileMap; @synthesize background = _background; // Replace the init method with the following -(id) init { if( (self=[super init] )) { self.tileMap = [CCTMXTiledMap tiledMapWithTMXFile:@"TileMap.tmx"]; self.background = [_tileMap layerNamed:@"Background"]; [self addChild:_tileMap z:-1]; } return self; } +(id) scene { // 'scene' is an autorelease object. CCScene *scene = [CCScene node]; // 'layer' is an autorelease object. HelloWorld *layer = [HelloWorld node]; // add layer as a child to scene [scene addChild: layer]; // return the scene return scene; } // on "dealloc" you need to release all your retained objects - (void) dealloc { self.tileMap = nil; self.background = nil; [super dealloc]; } @end |
这里我们调用CCTMXTiledMap从map文件创建了一个地图.
关于CCTMXTiledMap的一些简要介绍.它是CCNode的子类.所以我们可以设置position, scale等.这个node包含着地图的层,并且包含一些函数使你可以通过名字找到它们.为了提高性能,每一层使用的都是CCSpriteSheet的子 类. 这也意味着每个tiled元件在每一层都只有一个实例.
接下来我们要做的是利用地图和层的引用把他们添加到HelloWorld层.
编译运行代码,你将能够看到地图的左下角.
看起来不错!不过作为一个游戏,我们还需要做三件事:1.一个游戏主角;2.一个放置主角的起始点;3.移动视图,让我们的视角一直跟随主角.
这些才是开发这个游戏关键工作,我们一个个解决.
对象层和设置Tiled Map的位置.
Tiled Map Editor支持两种层: tile layers(铺展层,前面我们使用过)和object layers(对象层).
Object layers 允许你以一点为中心在地图上圈定一个区域.这个区域内可以触发一些事件.比如:你可以制作一个区域来产生怪物,或者制作一个区域进去就会死亡.在我们的例子里,我们制作一个区域作为主角的产生点.
打开TiledMapEditor,在Layer菜单选择Add Object Layer.新layer取名objects.注意,在object layer里不会绘制tiled元件,它会绘制一些灰色的圆角形状.你可以展开或者移动这些形状.
我们是想选择一个tile元件作为主角的进入点.所以,在地图里点击一个tiled元件,产生的形状的大小无所谓,我们会使用x,y坐标来指定.
接下来,右键选择刚才添加的灰色形状,点击Properties.设置名字为 “SpawnPoint”
也许你可以设置这个对象的Type为Cocos2D的类名.并且它会创建一个对象(比如CCSprite),但是我没有找到源代码里如何完成这些工作.
不管它,我们保留type区域为空,它将创建一个NSMutableDictionary用来访问对象的各种参数,比如x,y坐标.
保存地图回到xcode.修改HelloWorldScene.h
1 2 3 4 5 | // Inside the HelloWorld class declaration CCSprite *_player; // After the class declaration @property (nonatomic, retain) CCSprite *player; |
修改HelloWorldScene.m
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | // Right after the implementation section @synthesize player = _player; // In dealloc self.player = nil; // Inside the init method, after setting self.background CCTMXObjectGroup *objects = [_tileMap objectGroupNamed:@"Objects"]; NSAssert(objects != nil, @"'Objects' object group not found"); NSMutableDictionary *spawnPoint = [objects objectNamed:@"SpawnPoint"]; NSAssert(spawnPoint != nil, @"SpawnPoint object not found"); int x = [[spawnPoint valueForKey:@"x"] intValue]; int y = [[spawnPoint valueForKey:@"y"] intValue]; self.player = [CCSprite spriteWithFile:@"Player.png"]; _player.position = ccp(x, y); [self addChild:_player]; [self setViewpointCenter:_player.position]; |
我们先花一点时间解释一下object layer和object groups.
首先,我们通过CCTMXTiledMap对象的objectGroupNamed方法取回object layers.这个方法返回的是一个CCTMXObjectGroup对象.
接下来,调用CCTMXObjectGroup对象的objectNamed方法得到包含一组重要信息的NSMutableDictionary.包括x,y坐标,宽度,高度等.
在这里,我们主要需要的是x,y坐标.我们取得坐标并用它们来设置主角精灵的位置.
最后,我们要把主角作为视觉中心来显示.现在,添加下面的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | -(void)setViewpointCenter:(CGPoint) position { CGSize winSize = [[CCDirector sharedDirector] winSize]; int x = MAX(position.x, winSize.width / 2); int y = MAX(position.y, winSize.height / 2); x = MIN(x, (_tileMap.mapSize.width * _tileMap.tileSize.width) - winSize.width / 2); y = MIN(y, (_tileMap.mapSize.height * _tileMap.tileSize.height) - winSize.height/2); CGPoint actualPosition = ccp(x, y); CGPoint centerOfView = ccp(winSize.width/2, winSize.height/2); CGPoint viewPoint = ccpSub(centerOfView, actualPosition); self.position = viewPoint; } |
同样做一下简要的解释.想象这个函数是把视线设置到取景中心.我们可以在地图里设置任何x,y坐标,但是有些坐标不能正确的处理显示.比如,我们不能让显示区域超出地图的边界.否则就会出现空白区.
下面的图片更能说明这个问题:
屏幕的宽高计算后,要与显示区域的宽高做相应的适配.我们需要检测屏幕到达地图边缘的情况.
在cocos2d里本来有一些操控camera(可以理解为可视取景区)的方法,但是使用它可能搞得更复杂.还不如靠直接移动layer里的元素来解决更简单有效.
继续看下面这张图:
把整张地图想象为一个大的世界,我们的可见区是其中的一部分.主角实际的坐标并不是世界实际的中心.但是在我们的视觉内,要把主角放在中心点,所以,我们只需要根据主角的坐标便宜,调整世界中心的相对位置就可以了.
实现的方法是把实际中心与屏幕中心做一个差值,然后把HelloWorld Layer设置到相应的位置.
好,现在编译运行,我们会看到小忍者出现在屏幕上.
使主角移动
前面进行的都不错,但是到目前为止,我们的小忍者还不会动.
接下来,我们让小忍者根据用户在屏幕上点击的位置方向来移动(点击屏幕上半部分向上移,依此类推).
修改HelloWorldScene.m的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | // Inside init method self.isTouchEnabled = YES; -(void) registerWithTouchDispatcher { [[CCTouchDispatcher sharedDispatcher] addTargetedDelegate:self priority:0 swallowsTouches:YES]; } -(BOOL) ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event { return YES; } -(void)setPlayerPosition:(CGPoint)position { _player.position = position; } -(void) ccTouchEnded:(UITouch *)touch withEvent:(UIEvent *)event { CGPoint touchLocation = [touch locationInView: [touch view]]; touchLocation = [[CCDirector sharedDirector] convertToGL: touchLocation]; touchLocation = [self convertToNodeSpace:touchLocation]; CGPoint playerPos = _player.position; CGPoint diff = ccpSub(touchLocation, playerPos); if (abs(diff.x) > abs(diff.y)) { if (diff.x > 0) { playerPos.x += _tileMap.tileSize.width; } else { playerPos.x -= _tileMap.tileSize.width; } } else { if (diff.y > 0) { playerPos.y += _tileMap.tileSize.height; } else { playerPos.y -= _tileMap.tileSize.height; } } if (playerPos.x <= (_tileMap.mapSize.width * _tileMap.tileSize.width) && playerPos.y <= (_tileMap.mapSize.height * _tileMap.tileSize.height) && playerPos.y >= 0 && playerPos.x >= 0 ) { [self setPlayerPosition:playerPos]; } [self setViewpointCenter:_player.position]; } |
首先,我们在init方法里设置屏幕接受触摸事件.接下来,覆盖registerWithTouchDispatcher方法来注册我们自己的触摸 事件句柄.这样,ccTouchBegan/ccTouchEnded方法会在触摸发生时回调(单点触摸),并且屏蔽掉ccTouchesBegan /ccTouchesEnded方法的回调(多点触摸)
你可能奇怪,为什么不能使用ccTouchesBegan/ccTouchesEnded方法呢?是的,我们的确可以使用,但是不建议这么做,有两点原因:
* 你不需要再处理NSSets,事件分发器会帮你处理它们,你会在每次触摸得到独立的回调.
* 你可以在ccTouchBegan事件返回YES来告知delegate这事你想要的事件,这样你可以在move/ended/cancelled等后续的事件里方便的处理.这比起使用多点触摸要省去很多的工作.
通常,我们会将触摸的位置转换为view坐标系,然后再转换为GL坐标系.这个例子里的小变化,只是调用了一下 [self convertToNodeSpace:touchLocation].
这是因为触摸点给我们的是显示区的坐标,而我们其实已经移动过地图的位置.所以,调用这个方法来得到便宜后的坐标.
接下来,我们要搞清楚触摸点与主角位置的相对关系.然后根据向量的正负关系,决定主角的移动方向.
我们相应的调节主角的位置,然后设置屏幕中心到主角上.
注意:我们需要做一个安全检查,不要让我们的主角移出了地图.
好了,现在可以编译运行了,尝试触摸屏幕来移动一下小忍者吧.
接下来做什么?
现在你已经了解了如何创建一个基于tiled map的游戏.
这里是根据这篇教程完成的代码:猛击这里下载
下一篇里,我们将学习如何在地图里检测碰撞(或者说,设置不同的通过性),因为现在我们的小忍者是可以穿墙的…