木匣子

Web/Game/Programming/Life etc.

CCTMXTiledMap 与热更机制的冲突

本文涉及的 cocos2d-x 开发版本:cocos2d-x 3.2 / cocos2d-js 3.0 final

最近一个月在项目中新增加了类大富翁玩法,使用了 CCTMXTiledMap 来实现地图背景。由于热更资源的时候只提交了修改过的 tmx 地图数据文件,导致 CCTMXLayer 根据「相对路径」无法加载到地图图集(Tilesets)。解决方案有两种:

  1. 热更的时候连 tilesets 相关图片一起提交;
  2. 修复引擎底层加载图集的机制。

对于方法一,是比较好的临时补救方案,不需要重新提交软件包。通过现有热更机制更新脚本和资源即可修复。但不是个长久之计。

所幸这个问题是在内部测试的时候发现的,方案二的影响很少。所以就对 CCTMXTiledMap 底层的加载机制进行了一次跟踪。

Hot Update

热更的过程是将线上变更的文件下载到本地可写目录中(例如 iOS 中是 .app/Documents),然后将其优先级置于原始目录之上。最后重新启动游戏,即可加载到最新资源。没有更新到的文件会 fallback 到原始目录上,所以可以读到旧的资源。

FileUtils

在 Cocos2d-x 3.0 之后,文件的读取通过 FileUtils 进行。游戏中多使用相对路径索引资源(res/path/to/file),然后统一由 FileUtils 根据预置的根目录列表转换成绝对路径,以保证资源位置的正确性。

Tiled Map Editor

由于 Tiled Map Editor 允许资源以相对路径的方式进行资源管理,所以在早期的 Cocos2d-x 版本中 CCTMXTiledMap 需要自己处理相对路径。

Conclusion

显然 Cocos2d-x 的一些子系统并没有完全使用 FileUtils 来进行资源定位,而是自己私下处理一些「相对路径」生成绝对路径。而「绝对路径」传入 FileUtils 后不会再次经过处理,就导致了文章最开始出现的问题。

经过追踪,定位到 CCTMXXMLParser.cpp 作了如下处理:

void TMXMapInfo::internalInit(const std::string& tmxFileName, const std::string& resourcePath)
{
    if (tmxFileName.size() > 0)
    {
        _TMXFileName = FileUtils::getInstance()->fullPathForFilename(tmxFileName); 
    }

    // ...

这里获取了 tmx 文件的「绝对路径」,其后使用这一绝对路径与其它资源的「相对路径」拼出新的绝对路径来加载资源:

    // ...
    
    else if (elementName == "image")
    {
        TMXTilesetInfo* tileset = tmxMapInfo->getTilesets().back();
    
        // build full path
        std::string imagename = attributeDict["source"].asString();
    
        if (_TMXFileName.find_last_of("/") != string::npos)
        {
            string dir = _TMXFileName.substr(0, _TMXFileName.find_last_of("/") + 1);
            tileset->_sourceImage = dir + imagename;
        }
        else 
        {
            tileset->_sourceImage = _resources + (_resources.size() ? "/" : "") + imagename;
        }
    }

于是如果 tmx 文件在热更目录中,而又没有同时热更 tilesets 图集时,tileset->_sourceImage 指向的文件实际上就不存在,导致游戏闪退。

Solution

所以我想到的解决方法是,internalInit 方法中并不需要获取 tmx 的「绝对路径」,直接使用 tmx 最初的「相对路径」即可:

void TMXMapInfo::internalInit(const std::string& tmxFileName, const std::string& resourcePath)
{
    if (tmxFileName.size() > 0)
    {
        _TMXFileName = tmxFileName.substr();
    }

    // ...    

这样处理后的路径仍然是「相对路径」,使用这样的路径可以被 FileUtils 正确解析,问题解决。