小白也能写框架之【五、资源管理器】

接上篇:
小白也能写框架之【四、带加密的数据管理器】

一、直接上菜

1、第一个文件:
assets\Core\Scripts\Managers\ResMgr.ts

import { Asset, AssetManager, assetManager } from "cc";

import { logMgr } from "./LogMgr";

import { bundleMgr } from "./BundleMgr";

import { Parser } from "../Utils/Parser";

/**

 * 资源管理器

 * 提供资源加载、释放功能。

 */

class ResMgr {

    /** 私有构造函数,确保外部无法直接通过new创建实例 */

    private constructor() {}

    /** 单例实例 */

    public static readonly instance: ResMgr = new ResMgr();

    /**

     * 加载资源

     * @param resPath 资源路径

     * @param progressFun 进度回调函数

     * @param completeFun 完成回调函数

     * @returns Promise<T> 加载完成后的Promise

     */

    public async loadRes<T extends Asset>(

        resPath: string,

        progressFun?: (completedCount: number, totalCount: number, item: any) => void,

        completeFun?: (err: Error | null, asset: T) => void

    ): Promise<T> {

        try {

            const { bundleName, path } = Parser.path(resPath);

            const bundle = await bundleMgr.getBundle(bundleName);

            return await this.loadAsset<T>(bundle, path, progressFun, completeFun);

        } catch (error) {

            logMgr.err(`加载资源失败: ${resPath}`, error.message);

            throw error;

        }

    }

    /**

     * 加载目录下的所有资源

     * @param resPath 资源路径

     * @param progressFun 进度回调函数

     * @param completeFun 完成回调函数

     * @returns Promise<Array<Asset>> 加载完成后的Promise

     */

    public async loadResDir(

        resPath: string,

        progressFun?: (completedCount: number, totalCount: number, item: any) => void,

        completeFun?: (err: Error | null, assets: Array<Asset>) => void

    ): Promise<Array<Asset>> {

        try {

            const { bundleName, path } = Parser.path(resPath);

            const bundle = await bundleMgr.getBundle(bundleName);

            return await this.loadAssetDir(bundle, path, progressFun, completeFun);

        } catch (error) {

            logMgr.err(`加载目录失败: ${resPath}`, error.message);

            throw error;

        }

    }

    /**

     * 释放指定分包单个资源

     * @param resPath 资源路径

     */

    public releaseRes(resPath: string): void {

        const { bundleName, path } = Parser.path(resPath);

        const bundle = assetManager.getBundle(bundleName);

        if (bundle) {

            bundle.release(path);

        } else {

            logMgr.err(`分包 ${bundleName} 未找到,无法释放资源 ${path}。`);

        }

    }

    /**

     * 释放指定分包全部资源

     * @param bundleName 分包名称

     */

    public releaseBundle(bundleName: string): void {

        const bundle = assetManager.getBundle(bundleName);

        if (bundle) {

            bundle.releaseAll();

            assetManager.removeBundle(bundle);

        } else {

            logMgr.err(`分包 ${bundleName} 未找到,无法移除。`);

        }

    }

    /** 移除所有分包 */

    public releaseAll(): void {

        assetManager.releaseAll();

    }

    /**

     * 加载单个资源的辅助方法

     * @param bundle 资源所在的分包

     * @param path 资源路径

     * @param progressFun 进度回调函数

     * @param completeFun 完成回调函数

     * @returns Promise<T> 加载完成后的Promise

     */

    private loadAsset<T extends Asset>(

        bundle: AssetManager.Bundle,

        path: string,

        progressFun?: (completedCount: number, totalCount: number, item: any) => void,

        completeFun?: (err: Error | null, asset: T) => void

    ): Promise<T> {

        return new Promise<T>((resolve, reject) => {

            bundle.load(

                path,

                (completedCount, totalCount, item) => progressFun?.(completedCount, totalCount, item),

                (err, asset) => {

                    completeFun?.(err, asset as T);

                    if (err) {

                        logMgr.err(`从分包加载资源 ${path} 失败`, err.message);

                        reject(err);

                    } else {

                        resolve(asset as T);

                    }

                }

            );

        });

    }

    /**

     * 加载目录下所有资源的辅助方法

     * @param bundle 资源所在的分包

     * @param path 目录路径

     * @param progressFun 进度回调函数

     * @param completeFun 完成回调函数

     * @returns Promise<Array<Asset>> 加载完成后的Promise

     */

    private loadAssetDir(

        bundle: AssetManager.Bundle,

        path: string,

        progressFun?: (completedCount: number, totalCount: number, item: any) => void,

        completeFun?: (err: Error | null, assets: Array<Asset>) => void

    ): Promise<Array<Asset>> {

        return new Promise<Array<Asset>>((resolve, reject) => {

            bundle.loadDir(

                path,

                (completedCount, totalCount, item) => progressFun?.(completedCount, totalCount, item),

                (err, assets) => {

                    completeFun?.(err, assets);

                    if (err) {

                        logMgr.err(`从分包加载目录 ${path} 失败`, err.message);

                        reject(err);

                    } else {

                        resolve(assets);

                    }

                }

            );

        });

    }

}

/** 资源管理器实例 */

export const resMgr = ResMgr.instance;

2、第二个文件
assets\Core\Scripts\Utils\Parser.ts

/**

 * 解析器

 * 提供路径解析功能。

 */

export class Parser {

    /**

     * 解析资源路径并将其分解为包名和路径

     *

     * @param resPath - 要解析的资源路径

     * @returns 返回包含 bundleName 和 path 的对象

     */

    public static path(resPath: string): { bundleName: string; path: string } {

        const [bundleName, ...pathParts] = resPath.split('/');

        return {

            bundleName,

            path: pathParts.join('/')

        };

    }

}

二、全局映射

assets\Core\Scripts\Core.ts

三、使用示例

四、测试效果

五、注意事项
记得资源路径使用正斜杠 /
从cocosIDE复制出来的是反斜杠 \

后面实际开发会开源一个一键获取资源路径的插件,并且自动在对应分包生成资源索引,这样就不用自己手动复制资源路径了

1赞

哦对了,还得在cocosIDE中设置目录为分包目录,如下图:

资源管理模块感觉还不够,提几个意见,仅供参考:
1、资源应该是放在特定格式目录下的,这样能形成共识。
2、基于共识,就可以通过脚本自动生成预制体和音频等的配置文件。
3、资源可以增加引用计数级别的管理
4、可以增加资源收集器对象,用来收集资源并加载。

我之前也弄过计数器,后来给去掉了,为什么呢?
因为我是为大厅+子游戏的模式设计的框架
我在加载子游戏分包时,一起加载分包资源,子游戏关闭时,直接释放子游戏整个分包的资源
所以计数器对我几乎没用,资源缓存对我也用处不大。

我每个模块都是独立的分包,每个模块的资源肯定也是互不依赖的设计。

每个人对资源存放的路径有自己的风格,比如我:
分包名\Res\Images\xxx.png
分包名\Res\Font\xxx.font

所以这里也没对资源的路径做约束

不过你提提到的通过脚本自动生成预制体和音频等的配置文件,这点我也赞同

我一般都是根据项目需求,以【框架+DIY模块】的方式进行游戏开发,这样符合我

你像需要用的资源文件,我时通过一个插件脚本自动收集到对应项目分包目录下,见:
【免费开源小插件】分包资源路径获取,懒得传商店 - Creator 3.x - Cocos中文社区

后面我会录制一些实战开发的系列教程,和小白一起学习熟悉cocos和打造自己的框架

可以的,有特定的应用场景。

刚看了第一行
image
哪有这样创建单例的;单例有个好处是需要用到的时候才创建对象,你这里一开始就创建对象了。

饿汉式和饱汉式区别,,问题不大,

好用得讷~

主要是懒,哈哈