在上一篇里,我们已经学会了如何创建一个基于tiled map的简单游戏。学会了如何制作地图,如何将地图载入到游戏,如何让主角在屏幕上移动。
在这篇教程里,我们将学习如何在地图里创建可碰撞(不可穿越)区域,如何使用tile属性,如何使用可碰撞物体和动态修改地图,如何确定你的主角没有产生穿越。
Tiled Maps和碰撞
你可能注意到了,上一篇里完成的游戏,小忍者可以穿过各种障碍。它是忍者,不是上帝!
所以,我们要想办法让地图里的障碍物产生碰撞(不可穿越)。有很多办法可以解决这个问题(包括使用对象层objects layers),但是我准备告诉你种新技术,我认为这种技术更有效,同时也是作为学习课程的好素材。使用meta layer和层属性。
废话少说,我们开始吧。
用Tiled Map Editor打开之前创建的地图,点击Layer菜单的Add Tile Layer取名Meta。我们会在这一层上放置一些假的Tile指示特殊的tile元件。点击Map菜单的New Tileset,选择meta_tile.png图片。将Margin和Spacing设置为1。
你会在Tilesets窗口看到meta_tiles的标签。
这些tiles元件其实没什么特别的,只是带有透明特性的红色和绿色方块。我们拟定红色表示“可碰撞”的(绿色的后面会用到)。
选中Meta层,选择印章(stamp)工具,选择红色tile元件。把它绘制到忍者不能穿越的地方。绘制好之后,看起来应该是这样的:
接下来,我们要给这些Tile元件设置一些标记属性,这样在代码里我们可以确定哪些tile元件是不可穿越的。在Tilesets窗口里右键点击红色tile元件。添加一个新的属性Collidable”,设置值为true。
保存地图,回到xcode。修改HelloWorldScene.h文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// Inside the HelloWorld class declaration CCTMXLayer *_meta; // After the class declaration @property (nonatomic, retain) CCTMXLayer *meta; [\cc] 修改HelloWorldScene.m文件 [cc lang="objc"] // Right after the implementation section @synthesize meta = _meta; // In dealloc self.meta = nil; // In init, right after loading background self.meta = [_tileMap layerNamed:@"Meta"]; _meta.visible = NO; // Add new method - (CGPoint)tileCoordForPosition:(CGPoint)position { int x = position.x / _tileMap.tileSize.width; int y = ((_tileMap.mapSize.height * _tileMap.tileSize.height) - position.y) / _tileMap.tileSize.height; return ccp(x, y); } |
简单的对上面的代码做一些解释。我们定义了一个CCTMXLayer对象meta作为类成员。注意,我们将这个层设置为不可见,因为它只是用来处理碰撞的。
接下来我们编写了一个tileCoordForPosition方法,用来将x,y坐标转换为地图网格坐标。地图左上角为(0,0)右下角为(49,49)。
上面带有坐标显示的截图来自java版本的编辑器。顺便说一声,我觉得在Qt版本里这个功能可能不再会被移植了。
不管怎么样,用地图网格坐标要比用x,y坐标方便。得到x坐标比较方便,但是y坐标有点麻烦,因为在cocos2d里,是以左下作为原点的。也就是说,y坐标的向量与地图网格坐标是相反的。
接下来,我们要修改一下setPlayerPosition方法。
1 2 3 4 5 6 7 8 9 10 11 12 |
CGPoint tileCoord = [self tileCoordForPosition:position]; int tileGid = [_meta tileGIDAt:tileCoord]; if (tileGid) { NSDictionary *properties = [_tileMap propertiesForGID:tileGid]; if (properties) { NSString *collision = [properties valueForKey:@"Collidable"]; if (collision && [collision compare:@"True"] == NSOrderedSame) { return; } } } _player.position = position; |
这里,我们将主角的坐标系从x,y坐标(左下原点)系转换为tile坐标系(左上原点)。接下来,我们使用meta layer里的tileGIDAt函数获取tile坐标系里的GID。
噢?什么是GID? GID应该是“全局唯一标识”(我认为).但是在这个例子里,把它作为tile层的id更贴切。
我们使用GID来查找tile层的属性,返回值是一个包含属性列表的dictionary。我们检查“Collidable”属性是否设置为ture。如果是,则说明不可以穿越。
很好,编译运行工程,你再也不能走入你在tile里设置为红色的区域了。
动态改变Tiled Maps
现在,你的小忍者可以在地图上漫游了,不过,整个游戏还是略显沉闷。
假设我们的小忍者非常饿,那么我们设置一些食物,让小忍者可以找到并吃掉它们。
为了实现这个想法,我们要创建一个前端层,承载所有用于触碰(吃掉)的物体。这样,我们可以在忍者吃掉它们的同时,方便的从层上删除它。并且背景层不受任何影响。
打开Tiled Map Editor,Layer菜单的Add Tile Layer。命名新层为Foreground。选中这个层,添加一些可触碰的物件。我比较喜欢用西瓜。
接下来,要让西瓜变为可触碰的。这次我们用绿色方块来标记。记得要在meta_tiles里做这件事。
同样的,给绿色方块添加属性“Collectable”设置值为 “True”.
保存地图,回到xcode。修改代码:
1 2 3 4 5 6 |
//in HelloWorldScene.h: // Inside the HelloWorld class declaration CCTMXLayer *_foreground; // After the class declaration @property (nonatomic, retain) CCTMXLayer *foreground; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
//in HelloWorldScene.m // Right after the implementation section @synthesize foreground = _foreground; // In dealloc self.foreground = nil; // In init, right after loading background self.foreground = [_tileMap layerNamed:@"Foreground"]; // Add to setPlayerPosition, right after the if clause with the return in it NSString *collectable = [properties valueForKey:@"Collectable"]; if (collectable && [collectable compare:@"True"] == NSOrderedSame) { [_meta removeTileAt:tileCoord]; [_foreground removeTileAt:tileCoord]; } |
这里有个基本的原则,要同时删除meta layer 和the foreground layer的匹配对象。
编译运行,小忍者可以吃到美味的甜西瓜了。
创建分数计数器
小忍者现有吃有喝很开心,但是,我们想知道到底他吃了多少个西瓜。
通常,我们在layer上看着顺眼的地方加个label来显示数量。但是,我们一直在移动层,这样会给我们带来很多的困扰。
这是一个演示在一个场景里使用多个层的好例子。我们保留HelloWorld层来进行游戏,同时,增加一个HelloWorldHud层用来显示label(Hub = heads up display)。
当然,这两个层需要一些方法来互相通讯。Hub层需要知道小忍者吃到了西瓜。有很多很多方法实现两个层之间的通信,但是我们使用尽量简单的方法来实现。我 们会让HelloWorld层管理一个HelloworldHub层的引用,在忍者迟到西瓜的时候,可以调用一个方法来通知Hub层。
修改代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// HelloWorldScene.h // Before HelloWorld class declaration @interface HelloWorldHud : CCLayer { CCLabel *label; } - (void)numCollectedChanged:(int)numCollected; @end // Inside HelloWorld class declaration int _numCollected; HelloWorldHud *_hud; // After the class declaration @property (nonatomic, assign) int numCollected; @property (nonatomic, retain) HelloWorldHud *hud; |
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 |
// HelloWorldScene.m // At top of file @implementation HelloWorldHud -(id) init { if ((self = [super init])) { CGSize winSize = [[CCDirector sharedDirector] winSize]; label = [CCLabel labelWithString:@"0" dimensions:CGSizeMake(50, 20) alignment:UITextAlignmentRight fontName:@"Verdana-Bold" fontSize:18.0]; label.color = ccc3(0,0,0); int margin = 10; label.position = ccp(winSize.width - (label.contentSize.width/2) - margin, label.contentSize.height/2 + margin); [self addChild:label]; } return self; } - (void)numCollectedChanged:(int)numCollected { [label setString:[NSString stringWithFormat:@"%d", numCollected]]; } @end // Right after the HelloWorld implementation section @synthesize numCollected = _numCollected; @synthesize hud = _hud; // In dealloc self.hud = nil; // Add to the +(id) scene method, right before the return HelloWorldHud *hud = [HelloWorldHud node]; [scene addChild: hud]; layer.hud = hud; // Add inside setPlayerPosition, in the case where a tile is collectable self.numCollected++; [_hud numCollectedChanged:_numCollected]; |
没什么稀奇的,第二个层继承CCLayer,并且在右下角添加了一个label。我们将第二个层添加到场景(Scene)里并且把hub层的引用传递给HelloWorld层。然后修改HelloWorld层调用通知计数改变的方法。
编译运行,应该可以在右下角看到吃瓜计数器了。
音效和音乐
众所周知,没有音效和音乐的游戏,称不上是个完整的游戏。
接下来,我们做一些简单的修改,让我们的游戏带有音效和背景音。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// HelloWorldScene.m // At top of file #import "SimpleAudioEngine.h" // At top of init for HelloWorld layer [[SimpleAudioEngine sharedEngine] preloadEffect:@"pickup.caf"]; [[SimpleAudioEngine sharedEngine] preloadEffect:@"hit.caf"]; [[SimpleAudioEngine sharedEngine] preloadEffect:@"move.caf"]; [[SimpleAudioEngine sharedEngine] playBackgroundMusic:@"TileMap.caf"]; // In case for collidable tile [[SimpleAudioEngine sharedEngine] playEffect:@"hit.caf"]; // In case of collectable tile [[SimpleAudioEngine sharedEngine] playEffect:@"pickup.caf"]; // Right before setting player position [[SimpleAudioEngine sharedEngine] playEffect:@"move.caf"]; |
接下来做点什么呢?
通过这篇教程,你应该对coco2d有了一些基本的了解。
这里是按照整篇教程完成的工程文件,猛击这里下载
如果你感兴趣,我的好朋友Geek和Dad编写了一篇后续教程:Enemies and Combat: How To Make a Tile-Based Game with Cocos2D Part 3! 。这篇教程将告诉你,如何在游戏里添加敌人,武器,胜负场景等。