[译]在Tiled Map中使用碰撞检测(二) TMX地图中的碰撞检测

On 2010年06月20日, in iPhone, by 毛叔

在上一篇里,我们已经学会了如何创建一个基于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 ( 5020 )
            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! 。这篇教程将告诉你,如何在游戏里添加敌人,武器,胜负场景等。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值