Cocos游戏开发|使用zip压缩减少web请求,加速资源加载

点击上方码不了一点+关注和★ 星标


1 引言

Cocos Creator 3.8 有提供 zip 格式的 bundle,但不支持 web 平台。今天就给大家分享一下如何使用 Zip 加速 Cocos Creator 在 Web 平台的资源加载。

前段时间使用 Cocos Creator 3.8 做一个云展厅项目,要求在 Web 平台上线(微信 H5&浏览器)。

这个云展厅项目使用 gltf 模型,gltf 模型中有拆分很多 Mesh 和材质。

而在 Cocos 中 gltf 会被拆分解析为 Cocos 资产,发布 Web 后加载一个这种大 gltf 就可能有几百个 request,明明网速飞起,但是加载还是很慢,因为此时项目中加载速度的瓶颈已经不是网速,而是 request 的数量太多了。

2 产生的原因&如何解决

为什么有这么多 request

  • 新建一个项目,将下图这个gltf(编辑注:想要这个资源的朋友请查看 码不了一点 公众号)直接放在resources文件夹下,方便在demo进行预加载,这个gltf中共28材质,32个Mesh和一些骨骼&贴图

cb0ffe930d9bcf21aa9a9e82335ce87a.jpeg

  • 创建Start场景用于预加载资源,和Game场景用于展示模型

24546447699cb97341a155de8edca460.png

  • 创建Start脚本,对资源做一个简单的预加载,加载完成后进入Game场景

import {_decorator, Component, director, Label, settings, ProgressBar, resources, assetManager, Settings} from 'cc';

const {ccclass, property} = _decorator;

@ccclass('Start')
export class Start extends Component {

    @property(ProgressBar)
    progressBar: ProgressBar;

    @property(Label)
    barLab: Label = null;

    async start() {
    
        // 直接加载resources根目录
        await this.preload([
            {
                path: "/",
                type: "dir",
            },
        ]);

        director.loadScene("Game");

    }

    /**
     * 预加载资源
     */
    preload = (pkg) => {
        return new Promise<void>((resolve, reject) => {
            const pathArr = [];

            pkg.forEach((asset) => {
                if (typeof asset == "string") {
                    return pathArr.push(asset);
                }
                switch (asset.type) {
                    case "dir":
                        resources.getDirWithPath(asset.path).forEach((v) => pathArr.push(v.path));
                        break;
                        
                    default:
                        pathArr.push(asset.path);
                }
            });

            resources.load(
                pathArr,
                (finish: number, total: number, _) => {
                    const pro = finish / total;
                    if (pro < this.progressBar.progress) {
                        return;
                    }
                    this.progressBar.progress = pro;
                    this.barLab.string = `正在加载中   ${(pro * 100).toFixed(0)}%`;
                },
                async (error) => {
                    if (error) console.error(error);
                    resolve();
                }
            );
        });
    }
}
  • 运行游戏,在本地web环境查看Network,request数量为379,和这个gltf相关的就有216个,打包发布至Web环境,选择合并所有Json,加载该gltf总共用了35次request。

e6dee45bfd52cda2133d53e24137e97a.jpeg

本地 Web 预览

8141b005c32f5364c9f6b1184296debb.jpeg

打包合并后
  • 明明只有一个gltf却用了35次request来加载。

  • 原因在于Cocos将gltf资源转换成了Cocos资产,将Mesh,材质等拆解了出来,每个资源除了资源本身外还会有一个记录属性依赖的Json文件

6de69bb535944b5e861a8d3381a46a39.png

如何解决

  • 将整个bundle打包,比如打包成zip文件,进入游戏先加载需要的bundle的zip文件一次下载并且解压,之后需要资源直接从解压完的文件里取。

3 Zip 和 JsZip 的使用

Zip

不必多言,想必大家都知道

JsZip 的使用

不必重复了,直接上npm平台参考文档吧

jszip[1]https://www.npmjs.com/package/jszip

文档看起来肯定很抽象,不如直接跟着下面的步骤实操。

4 探索 Cocos 加载资源的奥秘

  • 查看Network,可以发现Cocos下载资源都会通过一个download-file.ts文件,移动鼠标到download-file.ts上就可以看到他的调用栈,其中主要是download-file.tsdownloader.ts也就是资源下载管线的一部分。那么我们直接打开源码进入到这里

db63717aab2ddb54660e75b55f393d69.png

ec3a3444780174f2c12f958a269fac66.png

  • 在代码中我们可以看到,大部分文件的下载都是通过downloadFile方法进行下载的,这个方法就是刚才的download-file.ts中的方法,该方法使用XMLHttpRequest下载文件

0c272388996c5c4571230fb540ea95f4.png

9c9c1a5b279166f4dc0d176d395b93ec.png

  • 既然我们已经知道在Cocos中,大部分资源的下载都依赖于XMLHttpRequest,那么我们可以想办法拦截它,重定向到我们解包的zip包就可以避免发起它真实的网络请求从而消耗大量时间了。

5 如何加载自己的 Zip 包

装载自己的 zip

  • 浅写一个 ZipLoader 并作为单例使用

  • 偷个懒,这里直接使用 Cocos 内置 API 加载远程文件吧,注意这个 API 已经弃用,未来可能删除

  • 我们直接不管容错,把 demo 跑通再说

  • 并使用 Promise 配合外部 async/await 来简化控制流。

import {assetManager} from "cc";
import JSZIP from "jszip";

export default class ZipLoader {

    static _ins: ZipLoader;
    static get ins() {
        if (!this._ins) {
            this._ins = new ZipLoader();
        }
        return this._ins;
    }

    /**
     * 下载单个zip文件为buffer
     * 为什么这里带上后缀名后面会讲到,是为了方面自动化
     * @param path 文件路径
     * @returns zip的buffer
     */
    downloadZip(path: string) {
        return new Promise((resolve) => {
            assetManager.downloader.downloadFile(
                path + '.zip',
                {xhrResponseType: "arraybuffer"},
                null,
                (err, data) => {
                    resolve(data);
                }
            );
        });
    }

    /**
     * 解析加载Zip文件
     * @param path 文件路径
     */
    async loadZip(path: string) {
        // 这里没用npm包的形式而是采用umd形式的js包
        const jsZip = window["JSZip"]();

        // 下载
        const zipBuffer = await this.downloadZip(path);

        // 解压
        const zipFile = await jsZip.loadAsync(zipBuffer);
    }
}
  • 在之前的Start.ts中添加代码

  • 注意以下几点

  • 作者这里有自动化压缩上传插件,会自动修改server字段,server就是项目发布的根目录带协议和域名,例如https://xxx.com/cc_project/version/

  • 作者这里会将需要zip加载的包注入到window上

  • 注入的js类似window["zipBundle"] = ["internal", "main", "resources"];

  • 作者这里所有bundle全都在远程所以只加载remote中的文件就行了且zip文件和bundle文件夹在同一目录下

/* ... */

@ccclass('Start')
export class Start extends Component {

    /* ... */
    
    async start() {

        // 作者这里有自动化压缩上传插件,会自动修改server字段
        // 并且会将需要zip加载的包注入到window上
        // 注入的js类似与下面这行
        // window["zipBundle"] = ["internal", "main", "resources"];
        const remoteUrl = settings.querySettings(Settings.Category.ASSETS, "server");
        const zipBundle = window["zipBundle"] || [];

        // 作者这里所有bundle全都是远程bundle所以只加载remote中的文件就行了
        // 且zip文件和bundle文件夹在同一目录下
        const loadZipPs = zipBundle.map((name: string) => {
            return ZipLoader.ins.loadZip(`${remoteUrl}remote/${name}`);
        });
        
        // 先等zip加载完
        await Promise.all(loadZipPs);

        // 直接加载resources根目录
        await this.preload([
            {
                path: "/",
                type: "dir",
            },
        ]);

        director.loadScene("Game");

    }
    
    /* ... */
    
}

不自定义引擎,拦截 Cocos 加载

查阅了 Cocos 的文档没有很好的批量实现这个需求的方式,又因为 Cocos 引擎更新比较频繁,我个人又喜欢多用新引擎新功能,所以我选择不自定义引擎,直接采用拦截 Cocos 加载的方法实现将加载资源替换到自己的zip包。

通过阅读源码我们已经知道除了图片资源,其他资源都是通过 XMLHttpRequest 来加载的,那么很简单,我们直接拦截 XMLHttpRequest 就行了。

那么你问我怎么才能拦截一个浏览器 Native 对象,这可是 Js,Js 无所不能!

拦截open和send

  • 不必多说,按下面这种方法就可拦截一个XMLHttpRequest来做一些操作

// 拦截open
const oldOpen = XMLHttpRequest.prototype.open;
// @ts-ignore
XMLHttpRequest.prototype.open = function (method, url, async, user, password) {
  return oldOpen.apply(this, arguments);
}

// 拦截send
const oldSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = async function (data) {
  return oldSend.apply(this, arguments);
}
  • 添加解析zip的代码,将zip中的代码解析到完整的路径上

/* ... */;

const ZipCache = new Map<string, any>();

export default class ZipLoader {

    /* ... */

    constructor() {
        this.init();
    }

    /* ... */

    /**
     * 解析加载Zip文件
     * @param path 文件路径
     */
    async loadZip(path: string) {

        const jsZip = JSZIP();

        const zipBuffer = await this.downloadZip(path);

        const zipFile = await jsZip.loadAsync(zipBuffer);
        // 解析zip文件,将路径,bundle名,文件名拼起来,直接存在一个map里吧
        zipFile.forEach((v, t) => {
            if (t.dir) return;
            ZipCache.set(path + "/" + v, t);
        });
    }
    
    init() {
        // 拦截open
        const oldOpen = XMLHttpRequest.prototype.open;
        // @ts-ignore
        XMLHttpRequest.prototype.open = function (method, url, async, user, password) {
            return oldOpen.apply(this, arguments);
        }

        // 拦截send
        const oldSend = XMLHttpRequest.prototype.send;
        XMLHttpRequest.prototype.send = async function (data) {
            return oldSend.apply(this, arguments);
        }
    }
  • 在拦截的open和send中取消网络请求,直接定向到我们缓存在zip资源,由于我们不能直接修改xhr的response,因为他是只读属性,所以我们要借助Object.getOwnPropertyDescriptorObject.defineProperty,话不多说,直接看代码把

  • 在测试过程中发现Cocos可能会请求多次同一个json,且可能修改解析后的对象,所以我暂时给json类型的资源加了一个id,可以让他每次都重新获取zip中的内容并解析

/* ... */

const ZipCache = new Map<string, any>();
const ResCache = new Map<string, any>();
let jsonId = 0;  // 兼容json

export default class ZipLoader {

    /* ... */

    constructor() {
        this.init();
    }

    /* ... */

    init() {

        const accessor = Object.getOwnPropertyDescriptor(XMLHttpRequest.prototype, 'response');
        Object.defineProperty(XMLHttpRequest.prototype, 'response', {
            get: function () {
                if (this.zipCacheUrl) {
                    return ResCache.get(this.zipCacheUrl);
                }
                return accessor.get.call(this);
            },
            set: function (str) {
                // console.log('set responseText: %s', str);
                // return accessor.set.call(this, str);
            },
            configurable: true
        });

        // 拦截open
        const oldOpen = XMLHttpRequest.prototype.open;
        // @ts-ignore
        XMLHttpRequest.prototype.open = function (method, url, async, user, password) {
            // 有这个资源就记录下来
            if (ZipCache.has(url as string)) {
                this.zipCacheUrl = url;
            }
            return oldOpen.apply(this, arguments);
        }

        // 拦截send
        const oldSend = XMLHttpRequest.prototype.send;
        XMLHttpRequest.prototype.send = async function (data) {
            if (this.zipCacheUrl) {
                // 有缓存就不解析了
                if (!ResCache.has(this.zipCacheUrl)) {

                    const cache = ZipCache.get(this.zipCacheUrl);

                    if (this.responseType === "json") {
                        // 兼容json
                        this.zipCacheUrl += jsonId++;
                        const text = await cache.async("text");
                        ResCache.set(this.zipCacheUrl, JSON.parse(text));
                    } else {
                        // 直接拿cocos设置的responseType给zip解析
                        const res = await cache.async(this.responseType);
                        ResCache.set(this.zipCacheUrl, res);
                    }
                }
                
                // 解析完了直接调用onload,并且不再发起真实的网络请求
                this.onload();
                return;
            }

            return oldSend.apply(this, arguments);
        }
    }
}
  • 打包项目手动压缩bundle文件夹上传进行测试,可以看到我们下载了三个zip资源,大量的json和bin文件夹都没有下载,和我们测试gltf相关的文件,仅有两张贴图而已,而且能正常进入Game场景,说明我们刚才写的代码是有效的,加载该gltf文件的request次数从35次降到了3次

3ad1d796189389ecfde6da08fa91d699.png

cffd61156286c91246dd2735083dca9b.png

6 发布自动化

  • 编写 Cocos 插件打包自动压缩 bundle 为 zip

  • 这个就比较简单了,新建一个构建插件

  • 编写一个zip.ts,文件内容如下

import * as fs from "fs";
import JSZIP from "jszip";

//读取目录及文件
function readDir(zip, nowPath) {
    const files = fs.readdirSync(nowPath);
    files.forEach(function (fileName, index) {//遍历检测目录中的文件
        console.log(fileName, index);//打印当前读取的文件名
        const fillPath = nowPath + "/" + fileName;
        const file = fs.statSync(fillPath);//获取一个文件的属性
        if (file.isDirectory()) {//如果是目录的话,继续查询
            const dirlist = zip.folder(fileName);//压缩对象中生成该目录
            readDir(dirlist, fillPath);//重新检索目录文件
        } else {
            // 排除图片文件,下面会讲到
            if (fileName.endsWith(".png") || fileName.endsWith(".jpg")) {
                return;
            }
            zip.file(fileName, fs.readFileSync(fillPath));//压缩目录添加文件
        }
    });
}

//开始压缩文件
export function zipDir(name, dir, dist) {
    return new Promise<void>((resolve, reject) => {
        const zip = new JSZIP();
        readDir(zip, dir);
        zip.generateAsync({//设置压缩格式,开始打包
            type: "nodebuffer",//nodejs用
            compression: "DEFLATE",//压缩算法
            compressionOptions: {//压缩级别
                level: 9
            }
        }).then(function (content) {
            fs.writeFileSync(`${dist}/${name}.zip`, content, "utf-8");
            resolve();
        });
    });
}
  • 在hooks中onAfterBuild中编写压缩脚本的内容,压缩脚本的内容在其他操作(如压缩图片,混淆代码,修改js等)都做完之后,且在上传资源前,且只针对web模板大致内容如下

export const onAfterBuild: BuildHook.onAfterBuild = async function (options: ITaskOptions, result: IBuildResult) {
    
    // 非需要的模板不进行这个操作
    if (options.platform !== "web-mobile") return;

    // 修改脚本,混淆代码,压缩资源等
    / ... /
    if (fs.existsSync(result.dest + "/remote")) {
        await Promise.all(
            fs.readdirSync(result.dest + "/remote")
                .map((dirName) => {
                    return zipDir(dirName, result.dest + "/remote/" + dirName, result.dest + "/remote");
                })
        )
    }
    / ... /
    
    // 上传

};

7 做一个简单的优化

通过前面阅读源码和 Network 中看到,Cocos 加载图片的方式不是通过 XMLHttpRequest,而是通过创建 Image 对象的方式。

此片文章的内容暂时不研究如何将加载图片也替换到使用自己的 Zip,因为我自己也还没做。

所以我选择直接在打包 zip 的时候过滤 png/jpg 文件来降低 zip 包的大小,仅在 zip 中打包需要的文件即可。

8 关注我

欢迎大家关注我的公众号,只搞实用的。

参考资料

[1]

jszip: https://www.npmjs.com/package/jszip

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
HTML5游戏开发实战涉及多个关键方面,包括游戏设计、用户体验、性能优化以及利用HTML5的特性来实现各种游戏功能。以下是一些关于HTML5游戏开发实战的要点和建议: 游戏设计: 确定游戏类型和玩法,如动作、益智、角色扮演等。 设计游戏场景、角色、道具等元素,确保游戏具有吸引力和可玩性。 制定游戏规则和流程,确保游戏逻辑清晰、易于理解。 用户体验: 优化游戏界面布局,确保元素排列合理、易于操作。 注重游戏的音效和视觉效果,提高游戏的沉浸感。 设计合理的游戏难度和进度,让玩家能够逐步挑战自我。 性能优化: 利用Canvas或WebGL等技术实现高效的图形渲染。 优化游戏代码和资源加载减少游戏卡顿和延迟。 针对移动设备进行优化,确保游戏在不同设备和网络环境下都能流畅运行。 HTML5特性利用: 利用HTML5的音频和视频支持,实现游戏中的背景音乐和音效。 使用HTML5的触摸事件处理,支持移动设备的触摸操作。 借助Web Storage API实现游戏数据的本地存储,方便玩家随时继续游戏。 跨平台兼容性: 确保游戏在不同浏览器和操作系统上都能正常运行。 测试游戏在不同设备上的性能表现,进行必要的优化和调整。 社区与反馈: 建立游戏社区,与玩家保持互动,收集他们的反馈和建议。 根据玩家反馈进行游戏更新和改进,提高游戏质量和用户满意度。 学习与借鉴: 学习和借鉴其他成功的HTML5游戏案例,了解它们的设计理念和技术实现。 参加游戏开发社区和论坛,与其他开发者交流经验和技术心得。 总之,HTML5游戏开发实战需要综合考虑多个方面,包括游戏设计、用户体验、性能优化以及HTML5特性的利用等。通过不断学习和实践,你可以逐渐掌握HTML5游戏开发的技巧和精髓,创造出受欢迎的游戏作品。
cocos2d-x是一个开源的游戏开发框架,它支持使用zip压缩文件进行资源管理。在cocos2d-x中,我们可以使用cocos2d::FileUtils类提供的方法对zip文件进行操作。 首先,我们需要将资源文件打包成zip压缩文件。我们可以使用压缩软件将资源文件夹压缩成一个zip文件,也可以使用cocos2d-x提供的命令行工具如cocos命令行工具将资源文件夹打包成zip文件。 然后,我们可以使用cocos2d::FileUtils类提供的方法对zip压缩文件进行解压缩操作。我们可以调用cocos2d::FileUtils::getInstance()方法获取FileUtils类的实例,然后使用其提供的方法如isFileExist、getFileData等判断文件是否存在以及获取文件数据。 例如,如果我们想判断一个文件是否存在,可以使用以下代码: ``` cocos2d::FileUtils *fileUtils = cocos2d::FileUtils::getInstance(); std::string zipFilePath = fileUtils->fullPathForFilename("resources.zip"); std::string filePathInsideZip = "resources/image.png"; if (fileUtils->isFileExist(zipFilePath)) { cocos2d::Data data = fileUtils->getFileDataFromZip(zipFilePath, filePathInsideZip); if (!data.isNull()) { // 文件存在 } else { // 文件不存在 } } else { // zip文件不存在 } ``` 除了判断文件是否存在,我们还可以使用getFileDataFromZip方法获取zip文件中的文件数据,并进行进一步的处理。 总之,cocos2d-x提供了方便的方法来进行zip压缩文件的管理,我们可以使用cocos2d::FileUtils类提供的方法来判断zip文件是否存在、获取文件数据等。这样,我们可以更好地管理和利用资源文件。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值