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

背景

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

热更

Cocos加载流程

思考

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

ini 复制代码
    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)
rust 复制代码
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)的实现
scss 复制代码
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()调用)
scss 复制代码
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实现
c 复制代码
std::string FileUtils::getStringFromFile(const std::string& filename)
{
    std::string s;
    getContents(filename, &s);
    return s;
}
  1. 找到FileUtils的getContents实现
ini 复制代码
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实现
c 复制代码
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::vectorstd::string& searchPaths)
ini 复制代码
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的方法声明也验证了我们的猜想
php 复制代码
/**
     *  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设置文件目录,看游戏是否可以加载成功。
ini 复制代码
- (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. 成功加载游戏。原生游戏热更完成。

游戏资源引用方式

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

以下为iOS游戏资源层级

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

以下为config.txt的内容:

json 复制代码
{
    "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"
        }
    }
}

游戏资源沙盒存放

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

预加载

以下仅为iOS的方案

步骤

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

  1. 在preLoadJYGame方法内部去load对应的原生游戏(在这里需要注意,原生的cocosview需要添加到view上,并且CocosAppManager.shareInstance().loadGame("ludo");需要异步执行)
ini 复制代码
        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路径拼接参数,原生加载需要通过方法告知)
相关推荐
若水无华18 小时前
fiddler 配置ios手机代理调试
ios·智能手机·fiddler
Aress"19 小时前
【ios越狱包安装失败?uniapp导出ipa文件如何安装到苹果手机】苹果IOS直接安装IPA文件
ios·uni-app·ipa安装
Jouzzy1 天前
【iOS安全】Dopamine越狱 iPhone X iOS 16.6 (20G75) | 解决Jailbreak failed with error
安全·ios·iphone
瓜子三百克1 天前
采用sherpa-onnx 实现 ios语音唤起的调研
macos·ios·cocoa
左钦杨1 天前
IOS CSS3 right transformX 动画卡顿 回弹
前端·ios·css3
努力成为包租婆1 天前
SDK does not contain ‘libarclite‘ at the path
ios
安和昂2 天前
【iOS】Tagged Pointer
macos·ios·cocoa
I烟雨云渊T2 天前
iOS 阅后即焚功能的实现
macos·ios·cocoa
struggle20252 天前
适用于 iOS 的 开源Ultralytics YOLO:应用程序和 Swift 软件包,用于在您自己的 iOS 应用程序中运行 YOLO
yolo·ios·开源·app·swift
Unlimitedz2 天前
iOS视频编码详细步骤(视频编码器,基于 VideoToolbox,支持硬件编码 H264/H265)
ios·音视频