美文网首页
Cocos原生游戏热更和预加载调研

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

作者: fighter0501 | 来源:发表于2023-11-21 17:12 被阅读0次

    背景

    前期进行了对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路径拼接参数,原生加载需要通过方法告知)

    相关文章

      网友评论

          本文标题:Cocos原生游戏热更和预加载调研

          本文链接:https://www.haomeiwen.com/subject/yqzbwdtx.html