上一篇主要介绍了在cocos2d中tilemap的加载使用方法,这一篇就简要查看一下cocos2d这部分的源码,看看cocos2d是如何解析tilemap中的数据的。
创建tilemap的代码如下:
TMXTiledMap* tilemap = TMXTiledMap::create("Map/tilemap.tmx");
this->addChild(tilemap);
可以看到cocos2d中调用了TMXTiledMap类的create方法,其内容为:
TMXTiledMap * TMXTiledMap::create(const std::string& tmxFile) {
TMXTiledMap *ret = new (std::nothrow) TMXTiledMap();
if (ret->initWithTMXFile(tmxFile)) {
ret->autorelease();
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}
create方法没做什么特别的事情,主要还是跳转到了initWithTMXFile方法:
bool TMXTiledMap::initWithTMXFile(const std::string& tmxFile) {
CCASSERT(tmxFile.size()>0, "TMXTiledMap: tmx file should not be empty");
setContentSize(Size::ZERO);
TMXMapInfo *mapInfo = TMXMapInfo::create(tmxFile);
if (! mapInfo) {
return false;
}
CCASSERT( !mapInfo->getTilesets().empty(), "TMXTiledMap: Map not found. Please check the filename.");
buildWithMapInfo(mapInfo);
return true;
}
从init方法里面得知,tmx文件的解析工作不是交给TMXTiledMap类做的,而是将tmx的路径传给了TMXMapInfo类,TMXMapInfo处理好地图的各项数据,自己再使用TMXMapInfo的结果创建地图。
这里先查看TMXMapInfo的内容,TMXMapInfo类定义在cocos/2d/CCTMXXMLParser.h文件中,注意一下这里TMXMapInfo类不仅继承了Ref,还继承了SAXDelegator类,具体内容等一下再看。
class CC_DLL TMXMapInfo : public Ref, public SAXDelegator
TMXMapInfo的create方法东西也不多,只是做了创建工作,初始化交给了initWithTMXFile:
TMXMapInfo * TMXMapInfo::create(const std::string& tmxFile) {
TMXMapInfo *ret = new (std::nothrow) TMXMapInfo();
if (ret->initWithTMXFile(tmxFile)) {
ret->autorelease();
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}
转到initWithTMXFile:
bool TMXMapInfo::initWithTMXFile(const std::string& tmxFile) {
internalInit(tmxFile, "");
return parseXMLFile(_TMXFileName.c_str());
}
internalInit就是一些成员变量的的初始化工作,parseXMLFile的内容如下:
bool TMXMapInfo::parseXMLFile(const std::string& xmlFilename) {
SAXParser parser;
if (false == parser.init("UTF-8") ) {
return false;
}
parser.setDelegator(this);
return parser.parse(FileUtils::getInstance()->fullPathForFilename(xmlFilename).c_str());
}
从这里看到,tmx文件的解析最后是交给了SAXParser,且SAXParser的delegator设置为了TMXMapInfo。
上面说TMXMapInfo继承了SAXDelegator,所以就将TMXMapInfo和SAXParser关联起来了。
此方法最后调用了SAXParser的parse方法进行解析,解析的结果也将交由TMXMapInfo来处理。
SAXParser及Delegator的定义在cocos/platform/CCSAXParser.h中。SAXParser的parse方法如下:
bool SAXParser::parse(const char* xmlData, size_t dataLength) {
tinyxml2::XMLDocument tinyDoc;
tinyDoc.Parse(xmlData, dataLength);
XmlSaxHander printer;
printer.setSAXParserImp(this);
return tinyDoc.Accept( &printer );
}
cocos2d对tmx文件的解析是交给了第三方的xml解析工具tinyxml2来解析的,前上一篇中讲了,tmx文件实际上就是一个xml文件,所以用xml解析器进行解析就可以了。
在这里XmlSaxHander类继承了tinyxml2::XMLVisitor类,并且重写了其中的Visit等方法,就是说,在tinyxml2解析tmx的过程中,会调用XmlSaxHander的相应方法,比如XmlSaxHander::VisitEnter:
bool XmlSaxHander::VisitEnter( const tinyxml2::XMLElement& element, const tinyxml2::XMLAttribute* firstAttribute ) {
// log(" VisitEnter %s",element.Value());
std::vector<const char*> attsVector;
for( const tinyxml2::XMLAttribute* attrib = firstAttribute; attrib; attrib = attrib->Next() ) {
// log("%s", attrib->Name());
attsVector.push_back(attrib->Name());
// log("%s",attrib->Value());
attsVector.push_back(attrib->Value());
}
// nullptr is used in c++11
//attsVector.push_back(nullptr);
attsVector.push_back(nullptr);
SAXParser::startElement(_ccsaxParserImp, (const CC_XML_CHAR *)element.Value(), (const CC_XML_CHAR **)(&attsVector[0]));
return true;
}
attsVector储存了tinyxml2解析出的属性名称以及其值,并且在return之前,又调用了SAXParser的startElement方法,传入了attsVector。
SAXParser的startElement方法是调用的其Delegator的startElement方法,而我们知道TMXMapInfo继承了SAXDelegator并进行了设置,故实际上调用的是TMXMapInfo的startElement方法。
所以最后还是回到了TMXMapInfo类里面:
// the XML parser calls here with all the elements
void TMXMapInfo::startElement(void *ctx, const char *name, const char **atts)
{
// ...
if (elementName == "map"){/*...*/}
else if (elementName == "tileset") {/*...*/}
else if (elementName == "tile"){/*...*/}
else if (elementName == "layer"){/*...*/}
else if (elementName == "objectgroup"){/*...*/}
else if (elementName == "image"){/*...*/}
else if (elementName == "data"){/*...*/}
else if (elementName == "object"){/*...*/}
else if (elementName == "property"){/*...*/}
else if (elementName == "polygon") {/*...*/}
else if (elementName == "polyline"){/*...*/}
}
由于代码太长,只提取了主要部分。看到一堆if里面的内容是不是很熟悉?回忆上一篇讲到的tmx文件的格式,可以知道是在这里,cocos2d对解析出来的数据进行了处理,挑其中一个,例如object的内容:
/*...*/
else if (elementName == "object")
{
TMXObjectGroup* objectGroup = tmxMapInfo->getObjectGroups().back();
// The value for "type" was blank or not a valid class name
// Create an instance of TMXObjectInfo to store the object and its properties
ValueMap dict;
// Parse everything automatically
const char* keys[] = {"name", "type", "width", "height", "gid"};
for (const auto& key : keys)
{
Value value = attributeDict[key];
dict[key] = value;
}
// But X and Y since they need special treatment
// X
int x = attributeDict["x"].asInt();
// Y
int y = attributeDict["y"].asInt();
Vec2 p(x + objectGroup->getPositionOffset().x, _mapSize.height * _tileSize.height - y - objectGroup->getPositionOffset().y - attributeDict["height"].asInt());
p = CC_POINT_PIXELS_TO_POINTS(p);
dict["x"] = Value(p.x);
dict["y"] = Value(p.y);
int width = attributeDict["width"].asInt();
int height = attributeDict["height"].asInt();
Size s(width, height);
s = CC_SIZE_PIXELS_TO_POINTS(s);
dict["width"] = Value(s.width);
dict["height"] = Value(s.height);
// Add the object to the objectGroup
objectGroup->getObjects().push_back(Value(dict));
// The parent element is now "object"
tmxMapInfo->setParentElement(TMXPropertyObject);
}
代码很好懂,就是当解析到object时,对应object所含有的属性,从attributeDict找到其值,例如name、type等等,然后丢到dict里面存储起来就可以了。
回到TMXTiledMap类中来,当TMXMapInfo解析完数据后,TMXTileMap就根据数据绘制地图,其buildWithMapInfo方法如下:
void TMXTiledMap::buildWithMapInfo(TMXMapInfo* mapInfo) {
_mapSize = mapInfo->getMapSize();
_tileSize = mapInfo->getTileSize();
_mapOrientation = mapInfo->getOrientation();
_objectGroups = mapInfo->getObjectGroups();
_properties = mapInfo->getProperties();
_tileProperties = mapInfo->getTileProperties();
int idx=0;
auto& layers = mapInfo->getLayers();
for(const auto &layerInfo : layers) {
if (layerInfo->_visible) {
TMXLayer *child = parseLayer(layerInfo, mapInfo);
if (child == nullptr) {
idx++;
continue;
}
addChild(child, idx, idx);
// update content size with the max size
const Size& childSize = child->getContentSize();
Size currentSize = this->getContentSize();
currentSize.width = std::max( currentSize.width, childSize.width );
currentSize.height = std::max( currentSize.height, childSize.height );
this->setContentSize(currentSize);
idx++;
}
}
}
除了地图基本信息,绘制地图主要的是各个Layer的内容,所以在这里可以看到Layer被依次获取,通过TMXLayer进行了加载,这里就不再详细叙述了。
总结
总结一下,cocos2d中加载tilemap的过程,简单来说,就是:
- 创建TMXTiledMap类时,其创建了一个TMXMapInfo类的实例。
- TMXMapInfo类初始化时,使用了tinyxml2对tmx文件进行了解析,并且对解析的数据进行了处理。
- TMXMapInfo初始化完毕后,对数据处理也完毕了,这时候TMXTiledMap类直接通过TMXMapInfo的数据创建地图即可。