Cocos Creator实现Google Play obb 分包

游戏开发 专栏收录该内容
73 篇文章 2 订阅

背景

Google Play 对 APK 大小限制是 100 M,但是游戏稍微重度一点,资源就会很多,包体很容易就超过了这个限制;Google Play 提供了 obb 分包方案,来解决包体问题。

OBB 是 Opaque Binary Blob 的缩写,是一种类型 zip 文件格式,作为安卓应用的扩展数据包。
参考:安卓开发指南百度百科

上面打不开的话,试试这个:https://developer.android.google.cn/google/play/expansion-files

思路

游戏多数资源都不需要在启动游戏时,就加载到内存,通常都是在游戏运行期间,需要时再动态去加载;因此可以:

  1. 将动态的资源分离出来,打包到 obb 文件中;
  2. 将 obb 文件和 APK 包一起上传到 Google Play;
  3. 玩家在下载 APK 时,会同时将 obb 文件下载到手机的 /Android/obb/包名/obb文件 路径;
  4. 首次启动游戏后,解压 obb 文件 到指定目录;
  5. 将 此目录 添加游戏资源的搜索路径中。

根据上面的思路,这里写了一个 demo,供大家参数,cocos creator 版本 2.2.0,过程如下:

一 新建 Demo

  1. 新建一个 HelloWorld 工程
  2. 新建 resources 目录,将图片 icon.png 加入到 resources,作为需要动态加载的资源
  3. 然后再 ui 上新建一个 Sprite,去掉纹理,在代码中动态加载 obb 中的资源

编辑器中设置如图:
在这里插入图片描述
4. 打开 HelloWorld 脚本,新建一个 Sprite 变量,关联到刚刚新建的精灵
5. 在 onLoad 方法动动态加载图片

onLoad: function () {
        this.label.string = this.text;
       // 动态加载资源代码
        cc.loader.loadRes("icon", cc.SpriteFrame, ((err, spf)=>{
            if (err) {
                console.error(err.message || err);
                return;
            }
            this.sp.spriteFrame = spf;
        }));
    },

这时候可以用网页运行一下:
在这里插入图片描述
这一步就是我们平时正常的项目开发流程,前期是不用管obb分包和打包的,只管开发功能就行。等功能开发好之后,才需要进一步的对接渠道,打包,分包等。

二 制作 obb 分包

  1. 构建 android 工程, 将上一步的 demo 构建 android 工程
    在这里插入图片描述

  2. 找到要分包的资源,打开新构建出的 android 工程,刚刚需要动态加载的资源。
    在这里插入图片描述

  3. 分离 obb 分包资源,新建一个目录 ‘raw-assets/0c/’,将资源剪切过来,注意是剪切,原来的资源不要了。
    在这里插入图片描述

  4. 制作 obb 分包,用压缩软件将 raw-assets 压缩成 zip 文件,然后改名为 main.1.org.cocos2d.helloworld.obb,命名规则是 [main/patch].[versionCode].[packageName].obb,到这一步 obb 分包就制作完成了。
    在这里插入图片描述

  5. 打包 APK,将分离 obb 文件之后工程,打包成 apk。此时如果运行 apk,图片资源是加载不出来的。

在这里插入图片描述

找资源,分离资源,打包重命名,过程都很繁琐,建议通过脚本自动化完成

三 上传下载 obb 包

经过上一步构建和分离资源得到了一个 obb 文件,如果是正式上线,需要将分离资源后的工程打包成 apk,然后将 obb 文件 和 apk 一起,提交到 Google Play,玩家下载 apk 时,就会将 obb文件一起下载到手机中的 /Android/obb/包名中。为了测试,直接把生成的 obb 直接拷贝到这个目录中:

在这里插入图片描述
拷贝的方式,不同的模拟器或是手机,方式不一样,请自行搜索。

四 解压 obb 包

obb 文件已经下载到手机之后,游戏首次启动需要先将 obb 文件解压到指定目录。为了能找到资源,还需要将解压的目录,加入到搜索路径中。

  1. 添加搜索路径,修改 HelloWorld.js ,在加载资源之前,将搜索路径加上。
onLoad: function () {
        // 将解压目录添加到资源搜索路径中
        if (window.jsb) {
            let obbDir = ((jsb.fileUtils ? jsb.fileUtils.getWritablePath() : "/") + "obb")
            jsb.fileUtils.addSearchPath(obbDir, true);
            console.log(obbDir);
        }

        this.label.string = this.text;
        
        cc.loader.loadRes("icon", cc.SpriteFrame, ((err, spf)=>{
            if (err) {
                console.error(err.message || err);
                return;
            }
            this.sp.spriteFrame = spf;
        }));
    },
  1. 解压 obb 资源,在 APPActivity 的 onCreate 方法中,添加如下代码解压资源:
        String obbFilepath = Utils.getInstance().getObbFilepath();
        String outPath = "/data/data/org.cocos2d.helloworld/files/obb/res";
        Utils.unzipFile(obbFilepath, outPath);
  1. 实现解压方法,在制作 obb 的过程可以知道 obb 实际上就是 zip 文件,因此只需要 zip 文件解压就可以了,这里提供一个 作为参数:
	// 获取 obb 文件路径
    public String getObbFilepath() {
        try {
            Cocos2dxActivity context = (Cocos2dxActivity) AppActivity.getContext();
            String packageName = context.getPackageName();
            int versionCode = context.getPackageManager().getPackageInfo(packageName, 0).versionCode;
            return context.getObbDir().getPath()
                    + File.separator
                    + "main."
                    + versionCode
                    + "."
                    + packageName
                    + ".obb";
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
            Log.e(TAG, "getFilepath fail");
            return "";
        }
    }

	// 解压 zip 文件
    public static boolean unzipFile(String zipFilepath, String unzipPath) {
        try {
            File zipFile = new File(zipFilepath);
            if (!zipFile.exists()) {
                Log.e(TAG, "no zip file " + zipFilepath);
                return false;
            }

            File outDir = new File(unzipPath);
            if (!outDir.exists()) {
                if (!outDir.mkdirs()) {
                    Log.e(TAG, "create unzip dir fail");
                    return false;
                }
            }

            ZipInputStream zipStream = new ZipInputStream(new FileInputStream(zipFile));
            ZipEntry zipEntry;
            String entryName;
            while ((zipEntry = zipStream.getNextEntry()) != null) {
                entryName = zipEntry.getName();
                if (zipEntry.isDirectory()) {
                    entryName = entryName.substring(0, entryName.length() - 1);
                    File subDir = new File(unzipPath + File.separator + entryName);
                    if (!subDir.exists() && !subDir.mkdirs()) {
                        Log.e(TAG, "create unzip sub dir fail " + subDir.getName());
                        return false;
                    }
                } else {
                    File subFile = new File(unzipPath + File.separator + entryName);

                    String parentPath = subFile.getParent();
                    if (parentPath == null) {
                        Log.e(TAG, "get sub file parent dir fail" + subFile.getName());
                        return false;
                    }
                    File parentDir = new File(parentPath);
                    if (!parentDir.exists() || !parentDir.isDirectory()) {
                        if (!parentDir.mkdirs()) {
                            Log.e(TAG, "create sub file parent dir fail" + subFile.getName());
                            return false;
                        }
                    }

                    if (!subFile.createNewFile()) {
                        Log.e(TAG, "create sub file fail" + subFile.getName());
                        return false;
                    }

                    FileOutputStream out = new FileOutputStream(subFile);
                    int len;
                    byte[] buffer = new byte[1024];
                    while ((len = zipStream.read(buffer)) != -1) {
                        out.write(buffer, 0, len);
                        out.flush();
                    }
                    out.close();
                }
            }
            zipStream.close();
            return true;
        } catch (Exception e) {
            Log.e(TAG, "unzipFile fail exception " + zipFilepath + " " + unzipPath);
            e.printStackTrace();
            return false;
        }
    }
  1. 重新打开 apk,然后运行可以看到,现在图片资源显示出来了。
    在这里插入图片描述
    打开我们的解压目录,可以看到解压之后的资源
    在这里插入图片描述

*** 若是压缩包内容很大,可以考虑用C++实现解压 ***


// 补充CPP版本

    FileUtils *_fileUtils;

    struct AsyncData
    {
        std::string zipFile;
        std::string outPath;
        bool succeed;
    };
    
    std::string basename(const std::string& path) const
    {
        size_t found = path.find_last_of("/\\");
        return std::string::npos != found ? path.substr(0, found) : path;
    }

    bool unzip(AsyncData *data)
    {
#define BUFFER_SIZE    8192
#define MAX_FILENAME   512
        const std::string rootPath = basename(data->outPath) + "/";
        const std::string &zip = data->zipFile;

        // Open the zip file
        unzFile zipfile = unzOpen(FileUtils::getInstance()->getSuitableFOpen(zip).c_str());
        if (! zipfile)
        {
            CCLOG("unzipFile : can not open zip file %s\n", zip.c_str());
            return false;
        }

        // Get info about the zip file
        unz_global_info global_info;
        if (unzGetGlobalInfo(zipfile, &global_info) != UNZ_OK)
        {
            CCLOG("unzipFile : can not read file global info of %s\n", zip.c_str());
            unzClose(zipfile);
            return false;
        }

        // Buffer to hold data read from the zip file
        char readBuffer[BUFFER_SIZE];
        // Loop to extract all files.
        uLong i;
        for (i = 0; i < global_info.number_entry; ++i)
        {
            // Get info about current file.
            unz_file_info fileInfo;
            char fileName[MAX_FILENAME];
            if (unzGetCurrentFileInfo(zipfile,
                                      &fileInfo,
                                      fileName,
                                      MAX_FILENAME,
                                      NULL,
                                      0,
                                      NULL,
                                      0) != UNZ_OK)
            {
                CCLOG("unzipFile : can not read compressed file info\n");
                unzClose(zipfile);
                return false;
            }
            const std::string fullPath = rootPath + fileName;

            // Check if this entry is a directory or a file.
            const size_t filenameLength = strlen(fileName);
            if (fileName[filenameLength-1] == '/')
            {
                //There are not directory entry in some case.
                //So we need to create directory when decompressing file entry
                if ( !_fileUtils->createDirectory(basename(fullPath)) )
                {
                    // Failed to create directory
                    CCLOG("unzipFile : can not create directory %s\n", fullPath.c_str());
                    unzClose(zipfile);
                    return false;
                }
            }
            else
            {
                // Create all directories in advance to avoid issue
                std::string dir = basename(fullPath);
                if (!_fileUtils->isDirectoryExist(dir)) {
                    if (!_fileUtils->createDirectory(dir)) {
                        // Failed to create directory
                        CCLOG("unzipFile : can not create directory %s\n", fullPath.c_str());
                        unzClose(zipfile);
                        return false;
                    }
                }
                // Entry is a file, so extract it.
                // Open current file.
                if (unzOpenCurrentFile(zipfile) != UNZ_OK)
                {
                    CCLOG("unzipFile : can not extract file %s\n", fileName);
                    unzClose(zipfile);
                    return false;
                }

                // Create a file to store current file.
                FILE *out = fopen(FileUtils::getInstance()->getSuitableFOpen(fullPath).c_str(), "wb");
                if (!out)
                {
                    CCLOG("unzipFile : can not create decompress destination file %s (errno: %d)\n", fullPath.c_str(), errno);
                    unzCloseCurrentFile(zipfile);
                    unzClose(zipfile);
                    return false;
                }

                // Write current file content to destinate file.
                int error = UNZ_OK;
                do
                {
                    error = unzReadCurrentFile(zipfile, readBuffer, BUFFER_SIZE);
                    if (error < 0)
                    {
                        CCLOG("unzipFile : can not read zip file %s, error code is %d\n", fileName, error);
                        fclose(out);
                        unzCloseCurrentFile(zipfile);
                        unzClose(zipfile);
                        return false;
                    }

                    if (error > 0)
                    {
                        fwrite(readBuffer, error, 1, out);
                    }
                } while(error > 0);

                fclose(out);
            }

            unzCloseCurrentFile(zipfile);

            // Goto next entry listed in the zip file.
            if ((i+1) < global_info.number_entry)
            {
                if (unzGoToNextFile(zipfile) != UNZ_OK)
                {
                    CCLOG("unzipFile : can not read next file for decompressing\n");
                    unzClose(zipfile);
                    return false;
                }
            }
        }

        unzClose(zipfile);

后记

obb 分包制作流程就是这样的,但是有几点内容还需要进一步的完成:

  1. obb 资源分离,需要一个配置文件,来指定哪些资源需要移到 obb 文件中;另外还需要一个自动化脚本,将配置文件中指定的资源,移动到 obb 文件对应的目录,然后压缩重命名;
  2. 解压 obb 文件,按照 demo 的方法,每次都会解压,需要一个标记,只有首次安装,或者是解压目录不存在时才需要解压
  3. 热更新,如何用热更新下来的资,覆盖掉obb 文件的资源?其实解压obb文件的,就是为了方便后续做热更新,不然其实可以不用解压,可以有方法直接读取 obb 文件中的内容。

最后,我是寒风,欢迎加入加入Q群(830756115)讨论。
在这里插入图片描述

  • 0
    点赞
  • 2
    评论
  • 4
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

相关推荐
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值