[译]在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(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! 。这篇教程将告诉你,如何在游戏里添加敌人,武器,胜负场景等。

你可能感兴趣的:([译]在Tiled Map中使用碰撞检测(二) TMX地图中的碰撞检测)