游戏中地图上总有些过不去的沟沟坎坎\山河湖泊.这节我们就来讲讲在coco2dx下如何实现碰撞检测
首先我们基于的tmx地图,所以就要先在地图上做些手脚.
除了建立的基本地形层,我们在编辑器中又建立了一个碰撞层
并在湖面上放置了一些表示不能通过的碰撞障碍
在表示障碍的图块上右键->图块属性
为该图块中增加一个Collidable属性,值设置为true.这项属性值等下我会在程序中使用.下面会有介绍
为了能方便的查看地形层,我们将碰撞层设置成为半透明.地图处理完了,接下来就看我们怎么在代码里处理这些障碍物了.
首先我们建立了一个Map类,Map从CCTMXTiledMap类继承
#include "Map.h"
using namespace cocos2d;
Map* Map::initTileMap(const char *tmxfile)
{
Map *map = new Map;
if(map->initWithTMXFile(tmxfile))
{
CCTMXLayer *clayer = map->layerNamed("collision"); //通过层名字获得该层对象
clayer->setVisible(false);
map->autorelease();
return map;
}
CC_SAFE_DELETE(map);
return NULL;
}
//通过指定的坐标转换为地图块坐标
CCPoint Map::tilePosFromLocation(CCPoint l)
{
int x = l.x/this->getTileSize().width;
int y = l.y/this->getTileSize().height;
y = this->getMapSize().height - y;
return ccp(x,y);
}
//判断该瓦片是否为障碍
bool Map::isTilePosBlocked(CCPoint l)
{
//判断当前块是否为碰撞块
CCPoint tilpos = tilePosFromLocation(l); //将带入的坐标转为块坐标
CCTMXLayer *clayer = this->layerNamed("collision"); //通过层名字获得该层对象
//clayer->setVisible(true);
int tileGID = clayer->tileGIDAt(tilpos); //获得该块的GID标识别
if(tileGID!=0)
{
CCDictionary* properties = this->propertiesForGID(tileGID);
if(strcmp(properties->valueForKey("Collidable")->getCString(),"true")==0)
return true;
}
return false;
}
我们建立了一个静态函数initTileMap,用来做一些初始化工作和内存回收工作.通过map->layerNamed("collision");获得了碰撞层,并设置为不可见,因为碰撞层本来就是一个不可见的抽象概念层.不应该为玩家展示出来.
tilePosFromLocation是将世界坐标转换为地图专用的图块坐标.在讲这个函数之前,我们先要讲几个概念.
首先是世界坐标,世界坐标的原点在画面左下角,Y轴向上,X轴向右
地图图块坐标原点在左上角,X轴向右,Y轴向下.
所以图块的坐标分布如下图
tilePosFromLocation函数的目的就是要把传入的世界坐标转换为地图图块上的坐标.以确定当前的世界坐标位置属于哪个图块范围内.
isTilePosBlocked传入的也是世界坐标点.用来确定当前位置对应图块是否为障碍物.我们可以看到,
第一步:函数中第一行CCPoint tilpos = tilePosFromLocation(l);就把带入的世界坐标转换为图块坐标.再通过CCTMXLayer的tileGIDAt函数获得了该图块的GID值.GID是全局标识符的意思.它是一个唯一的整数,地图中每层的每一块都会被赋予一个GID,这样可以保证每块都有自己的编号,用来确定自己的身份.当某个块为空时GID值为0.
第二步:获得了块的GID值,我们可以用CCTMXTileMap的propertiesForGID函数获得块的属性值.块的属性值设置方法我们在前面已经介绍.此处我们会利用valueForKey函数判断Collidable值是否为true,如果该条件成立则判断当前坐标对应的图块是一个障碍物.
接下来,我们来修改Hero类.由于碰撞检测的功能是需要每时每刻检测的,所以我们在类初始化的时候增加代码
//激活更新函数
schedule(schedule_selector(Hero::myUpdate,0.5f));
参数中Hero::myUpdate是我们的自定义函数.该语句表示在游戏进行的每一帧都会执行myUpdate函数,这样我们把碰撞检测代码放在myUpdate函数中就可以了。
void Hero::myUpdate(float dt)
{
if(hState != STAND && map->isTilePosBlocked(this->getPosition()))
collisionStand();
}
碰撞检测类函数很简单.当英雄的状态不为战立,并且当前英雄所处的位置是障碍块就执行collisionStand函数,停止英雄的走动和走动动画.
不过有个细节我还是要说一下,当人物走到障碍物时确实停止了,但此时人物已经处于障碍图块之中.所以当我们再命令英雄向其他方向移动时仍然会被判断为遇到障碍物再次执行collisionStand函数.导致直接的结果就是人物卡死在某个障碍图块的边缘,无论如何也走不动.所以我们要在人物碰到障碍物时让人物向相反的方向略微后退一点点.这样就能保证人物停止后不会处于障碍图块边缘上,下次再命令人物走动时也不会卡死在障碍图块的边缘了。
我们看collisionStand函数内容:
void Hero::collisionStand()
{
float x = this->getPositionX(); //获得hero的x坐标位置
float y = this->getPositionY(); //获得hero的y坐标位置
int offset = 2; //遇到障碍物后防止卡死进行微小移动的偏移量
//当停止时向人物背向方向略微移动2象素,防止人物在图块中卡死.
if(hState==LEFT)
this->setPosition(x+offset,y);
else if(hState==UP)
this->setPosition(x,y-offset);
else if(hState==RIGHT)
this->setPosition(x-offset,y);
else if(hState==DOWN)
this->setPosition(x,y+offset);
hState = STAND; //设置人物状态为站立
sprite->stopAllActions(); //停止播放走动动画
this->stopAllActions(); //停止人物走动动作
}
if(hState==LEFT) this->setPosition(x+offset,y).这句就是人物向左走碰到障碍后x坐标稍微增加2象素,就把人物移出了障碍图块的边缘.
我们来看看今天的成果吧: