Cocos原生游戏热更和预加载调研

背景

前期进行了对cocos原生游戏调研,对于加载原生游戏有一个限制,也就是原生游戏的资源包存放的路径需要固定(也就是按照cocos的默认路径,iOS需要存放在main bundle下,android需要存放在Asset下),这也就带来了一个问题:原生游戏无法进行下载更新,只能每次通过将游戏资源包更新发版的方式进行游戏更新,这无疑是业务方不可接受的。基于这种情况,因此对cocos原生游戏的热更调研显得尤为重要。而且业务方对于游戏的加载耗时也是尤为关注的,也顺带把原生游戏预加载一并调研了。

热更

Cocos加载流程

思考

首先基于上次调研的结果:游戏离线包资源放在默认路径下,使用原生加载游戏的话,需要调用cocos引擎加载两个文件(/jsb-adapter/jsb-builtin.js和/main.js),在iOS调用方式如下:

    se::ScriptEngine* se = se::ScriptEngine::getInstance();

    se->start();

    se::AutoHandleScope hs;

    std::string g = std::string([gameId UTF8String]);

    jsb_run_script(g+"/jsb-adapter/jsb-builtin.js");
    jsb_run_script(g+"/main.js");

可以看到我们传的是一个相对路径,那么cocos引擎内部,绝对是有对这个相对路径进行拼接处理的。因此,我们可以根据这个jsb_run_script(const std::string& filePath, se::Value* rval = nullptr);方法的实现一步一步找到拼接路径的地方。

文件修改路径

  1. jsb_run_script(const std::string& filePath, se::Value* rval = nullptr)
bool jsb_run_script(const std::string& filePath, se::Value* rval/* = nullptr */)
{
    se::AutoHandleScope hs;
    return se::ScriptEngine::getInstance()->runScript(filePath, rval);
}
  1. 找到se::ScriptEngine::getInstance()->runScript(filePath, rval)的实现
bool ScriptEngine::runScript(const std::string& path, Value* ret/* = nullptr */)
    {
        assert(!path.empty());
        assert(_fileOperationDelegate.isValid());

        std::string scriptBuffer = _fileOperationDelegate.onGetStringFromFile(path);

        if (!scriptBuffer.empty())
        {
            return evalString(scriptBuffer.c_str(), scriptBuffer.length(), ret, path.c_str());
        }

        SE_LOGE("ScriptEngine::runScript script %s, buffer is empty!\n", path.c_str());
        return false;
    }
  1. 路径拼接是在_fileOperationDelegate.onGetStringFromFile(path);的实现的,首先我们需要找到_fileOperationDelegate的赋值所在(搜索setFileOperationDelegate()调用)

[图片上传失败...(image-e4f618-1700644259534)]

void jsb_init_file_operation_delegate()
{
    static se::ScriptEngine::FileOperationDelegate delegate;
    if (!delegate.isValid())
    {
        ...

        delegate.onGetStringFromFile = [](const std::string& path) -> std::string{
            assert(!path.empty());

            std::string byteCodePath = removeFileExt(path) + BYTE_CODE_FILE_EXT;
            if (FileUtils::getInstance()->isFileExist(byteCodePath)) {
                Data fileData = FileUtils::getInstance()->getDataFromFile(byteCodePath);

                uint32_t dataLen;
                uint8_t* data = xxtea_decrypt((uint8_t*)fileData.getBytes(), (uint32_t)fileData.getSize(), (uint8_t*)xxteaKey.c_str(), (uint32_t)xxteaKey.size(), &dataLen);

                if (data == nullptr) {
                    SE_REPORT_ERROR("Can't decrypt code for %s", byteCodePath.c_str());
                    return "";
                }

                if (ZipUtils::isGZipBuffer(data,dataLen)) {
                    uint8_t* unpackedData;
                    ssize_t unpackedLen = ZipUtils::inflateMemory(data, dataLen,&unpackedData);
                    if (unpackedData == nullptr) {
                        SE_REPORT_ERROR("Can't decrypt code for %s", byteCodePath.c_str());
                        return "";
                    }

                    std::string ret(reinterpret_cast<const char*>(unpackedData), unpackedLen);
                    free(unpackedData);
                    free(data);

                    return ret;
                }
                else {
                    std::string ret(reinterpret_cast<const char*>(data), dataLen);
                    free(data);
                    return ret;
                }
            }

            if (FileUtils::getInstance()->isFileExist(path)) {
                return FileUtils::getInstance()->getStringFromFile(path);
            }
            else {
                SE_LOGE("ScriptEngine::onGetStringFromFile %s not found, possible missing file.\n", path.c_str());
            }
            return "";
        };

        delegate.onGetFullPath = [](const std::string& path) -> std::string{
            assert(!path.empty());
            std::string byteCodePath = removeFileExt(path) + BYTE_CODE_FILE_EXT;
            if (FileUtils::getInstance()->isFileExist(byteCodePath)) {
                return FileUtils::getInstance()->fullPathForFilename(byteCodePath);
            }
            return FileUtils::getInstance()->fullPathForFilename(path);
        };

        delegate.onCheckFileExist = [](const std::string& path) -> bool{
            assert(!path.empty());
            return FileUtils::getInstance()->isFileExist(path);
        };

        assert(delegate.isValid());
    }

    se::ScriptEngine::getInstance()->setFileOperationDelegate(delegate);
}
  1. 找到FileUtils::getInstance()->isFileExist(path);,也就是FileUtils管理文件路径

  2. 找到FileUtils的isFileExist实现

std::string FileUtils::getStringFromFile(const std::string& filename)
{
    std::string s;
    getContents(filename, &s);
    return s;
}
  1. 找到FileUtils的getContents实现
FileUtils::Status FileUtils::getContents(const std::string& filename, ResizableBuffer* buffer)
{
    if (filename.empty())
        return Status::NotExists;

    auto fs = FileUtils::getInstance();

    std::string fullPath = fs->fullPathForFilename(filename);
    if (fullPath.empty())
        return Status::NotExists;

    FILE *fp = fopen(fs->getSuitableFOpen(fullPath).c_str(), "rb");
    if (!fp)
        return Status::OpenFailed;

#if defined(_MSC_VER)
    auto descriptor = _fileno(fp);
#else
    auto descriptor = fileno(fp);
#endif
    struct stat statBuf;
    if (fstat(descriptor, &statBuf) == -1) {
        fclose(fp);
        return Status::ReadFailed;
    }
    size_t size = statBuf.st_size;

    buffer->resize(size);
    size_t readsize = fread(buffer->buffer(), 1, size, fp);
    fclose(fp);

    if (readsize < size) {
        buffer->resize(readsize);
        return Status::ReadFailed;
    }

    return Status::OK;
}
  1. 找到FileUtils的fullPathForFilename实现
std::string FileUtils::fullPathForFilename(const std::string &filename) const
{
    if (filename.empty())
    {
        return "";
    }

    if (isAbsolutePath(filename))
    {
        return normalizePath(filename);
    }

    // Already Cached ?
    auto cacheIter = _fullPathCache.find(filename);
    if(cacheIter != _fullPathCache.end())
    {
        return cacheIter->second;
    }

    // Get the new file name.
    const std::string newFilename( getNewFilename(filename) );

    std::string fullpath;

    for (const auto& searchIt : _searchPathArray)
    {
        for (const auto& resolutionIt : _searchResolutionsOrderArray)
        {
            fullpath = this->getPathForFilename(newFilename, resolutionIt, searchIt);

            if (!fullpath.empty())
            {
                // Using the filename passed in as key.
                _fullPathCache.insert(std::make_pair(filename, fullpath));
                return fullpath;
            }
        }
    }

    if(isPopupNotify()){
        CCLOG("fullPathForFilename: No file found at %s. Possible missing file.", filename.c_str());
    }

    // The file wasn't found, return empty string.
    return "";
}
  1. 发现路径拼接其实是拿_searchPathArray的内容进行拼接的。

  2. 找到对_searchPathArray入栈的地方(全局搜索_searchPathArray),最终找到void FileUtils::setSearchPaths(const std::vector<std::string>& searchPaths)

void FileUtils::setSearchPaths(const std::vector<std::string>& searchPaths)
{
    bool existDefaultRootPath = false;
    _originalSearchPaths = searchPaths;

    _fullPathCache.clear();
    _searchPathArray.clear();

    for (const auto& path : _originalSearchPaths)
    {
        std::string prefix;
        std::string fullPath;

        if (!isAbsolutePath(path))
        { // Not an absolute path
            prefix = _defaultResRootPath;
        }
        fullPath = prefix + path;
        if (!path.empty() && path[path.length()-1] != '/')
        {
            fullPath += "/";
        }
        if (!existDefaultRootPath && path == _defaultResRootPath)
        {
            existDefaultRootPath = true;
        }
        _searchPathArray.push_back(fullPath);
    }

    if (!existDefaultRootPath)
    {
        //CCLOG("Default root path doesn't exist, adding it.");
        _searchPathArray.push_back(_defaultResRootPath);
    }
}
  1. 同时,setSearchPaths的方法声明也验证了我们的猜想
/**
     *  Sets the array of search paths.
     *
     *  You can use this array to modify the search path of the resources.
     *  If you want to use "themes" or search resources in the "cache", you can do it easily by adding new entries in this array.
     *
     *  @note This method could access relative path and absolute path.
     *        If the relative path was passed to the vector, FileUtils will add the default resource directory before the relative path.
     *        For instance:
     *            On Android, the default resource root path is "@assets/".
     *            If "/mnt/sdcard/" and "resources-large" were set to the search paths vector,
     *            "resources-large" will be converted to "@assets/resources-large" since it was a relative path.
     *
     *  @param searchPaths The array contains search paths.
     *  @see fullPathForFilename(const char*)
     *  @since v2.1
     *  In js:var setSearchPaths(var jsval);
     *  @lua NA
     */
    virtual void setSearchPaths(const std::vector<std::string>& searchPaths);
  1. 在Demo里面验证,把原生游戏资源文件存放在沙盒里面,然后通过setSearchPaths设置文件目录,看游戏是否可以加载成功。
- (void)initCocosEngine {
    float scale = [[UIScreen mainScreen] scale];
    CGRect bounds = [[UIScreen mainScreen] bounds];

    NSString *docDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
    NSString *fileFolderPath = [docDir stringByAppendingFormat:@"/hhhh"];

    std::string g = std::string([fileFolderPath UTF8String]);

    std::vector<std::string> paths;//创建一个string型的容器
//    paths.push_back("hhhh");//往容器中添加图片目录所在的路径
    paths.push_back(g);//往容器中添加图片目录所在的路径
    cocos2d::FileUtils::getInstance()->setSearchPaths(paths);

    app = new CocosAppDelegate(bounds.size.width * scale, bounds.size.height * scale);

    app->setMultitouch(true);
    //run the cocos2d-x game scene
    app->start();

}
  1. 成功加载游戏。原生游戏热更完成。
image.png
image.png

游戏资源引用方式

之前由于调研认为游戏资源只能放在main bundle下,所以使用了pod组件导入游戏资源的方式。现在游戏资源可以放在任意路径,因此原生游戏资源的引用方式可以参照之前webview加载的方式。通过资源包让业务导入到工程中即可。

以下为iOS游戏资源层级

sealSource.bundle          //bundle资源包
    └── web                  //webview渲染资源包
        └── 5206662980335600255.zip
        └── 5237049012831387775.zip
    └── native               //原生渲染游戏资源包
        └── 5206662980335600255.zip
        └── 5237049012831387775.zip
    └── config.txt           //版本配置文件

以下为config.txt的内容:

{
    "web": {
        "5237049012831387775": {
            "version": 10203,
            "versionStr": "1.2.3"
        },
        "5206662980335600255": {
            "version": 10101,
            "versionStr": "1.1.1"
        }
    },
    "native": {
        "5237049012831387775": {
            "version": 10203,
            "versionStr": "1.2.3"
        },
        "5206662980335600255": {
            "version": 10101,
            "versionStr": "1.1.1"
        }
    }
}

游戏资源沙盒存放

gameSource              //游戏资源
    └── web                  //webview渲染资源包
        └── 5206662980335600255
            └── 10101
                └── 资源文件...
        └── 5237049012831387775
            └── 10203
                └── 资源文件...
    └── native               //原生渲染游戏资源包
        └── 5206662980335600255
            └── 10101
                └── 资源文件...
        └── 5237049012831387775
            └── 10203
                └── 资源文件...

预加载

以下仅为iOS的方案

步骤

可以复用之前webview预加载的方式去实现原生预加载

  1. 在preLoadJYGame方法内部去load对应的原生游戏(在这里需要注意,原生的cocosview需要添加到view上,并且CocosAppManager.shareInstance().loadGame("ludo");需要异步执行)
        let preView = UIView(frame: UIScreen.main.bounds)
        UIApplication.shared.windows.last?.addSubview(preView)
        preView.isHidden = true

        let gv: UIView = CocosAppManager.shareInstance().getCocosView()
        gv.frame = UIScreen.main.bounds
        preView.addSubview(gv)
        DispatchQueue.main.async {
            CocosAppManager.shareInstance().loadGame("ludo");
        }
  1. 需要和游戏协商,通过jsb协议方法告知游戏方,当前加载为预加载(之前webview加载是直接通过url路径拼接参数,原生加载需要通过方法告知)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容