乐趣区

小游戏开发之资源管理跨引擎

前言

资源管理是内存优化的一部分,对于大型游戏,资源管理不明确,很容易出现内存不足而闪退的情况。
说到资源也就涉及到了资源划分,这部分内容可以看另一篇文章《游戏开发之目录划分》。

资源管理器需要考虑的情况

  1. 加载完成的回调
  2. 加载失败后的尝试
  3. 多个相同请求的处理。
  4. 未加载成功之前已经删除。
  5. 资源的使用情况,记数。
  6. 跨引擎使用。

各个引擎需要提供的辅助类需要实现的接口

/**
 * 自定义的资源分类,对应各个引擎中相同的资源。*/
export enum ResType {
    Texture2D,
    SpriteFrame,
    SpriteAtlas,
    Prefab,
    Json,
    Scene,
    Material,
    AnimationClip,
    Mesh,
    Particle2D,// 粒子效果
    AudioClip,
}

export type ResCallback = (err: any, res: any) => void

/**
 * 是否使用引用记数 
 * 对于一些资源很少的小游戏不需要清理资源,所以可以设置为 false。*/
export let RECORD_RES_COUNT: boolean = true

/**
 * 各个引擎需要提供资源的辅助类需要实现的接口
 */
export default interface ResInterface {
    /**
     * 
     * @param url 加载资源
     * @param type 
     * @param callback 
     */
    loadRes(url: string, type: ResType, callback: ResCallback): void;

    /**
     * 清理资源
     * @param url 
     */
    release(url: string): void;

    /**
     * 获取资源
     * @param url 
     * @param ResType 自定义的资源类型 
     */
    getRes(url: string, type: ResType): any;

    /**
     * 获得资源的依赖资源
     * @param url 
     */
    getDependsRecursively(url: any): any;


}

资源类的封装和记数处理

import ResHelper from "../../engine/ResHelper";
import {ResType, RECORD_RES_COUNT} from "./ResInterface";

export default class ResItem {

    // 全局资源使用计数器。protected static resCountMap: {} = {};

    // 尝试加载次数
    private loadCount: number = 0;
    // 以来资源
    protected resources: {} = {};

    // 使用次数
    protected useCount: number = 0;

    // 资源 id
    private url: string;

    // 资源类型
    private type: ResType;

    // 加载是否结束
    protected loadFinish: boolean = false;

    // 资源本身
    private res: any;

    // 需要通知的函数
    private callbackList: Function[] = []


    constructor(url: string, type?: ResType) {
        this.url = url;
        this.type = type;
    }

    addCallback(func: Function) {this.callbackList.push(func)
    }

    // 是否加载完毕
    isDone() {return this.loadFinish;}

    getUrl() {return this.url;}

    getType() {return this.type;}

    getRes() {if (RECORD_RES_COUNT) {this.addCount();
        }
        if (!this.res) {this.res = ResHelper.instance().getRes(this.url, this.type)
        }
        return this.res;
    }

    /**
     * 加载完成调用
     * @param flag 
     */
    setLoadingFlag(flag: boolean) {
        this.loadFinish = flag;
        if (flag) {while (this.callbackList.length > 0) {let func = this.callbackList.shift();
                func(null, this)
            }
        }
    }
    /**
     * 由于引擎加载机制,加载完成就已经使用,*/
    cacheRes(res: any) {
        this.res = res;
        if (RECORD_RES_COUNT) {let depands = ResHelper.instance().getDependsRecursively(res)
            for (let key of depands) {this.resources[key] = true;
            }
            // 加载成功后直接加 1,以免被其他模块的记载器清理掉。this.addCount()}

    }

    // 获得加载次数
    getLoadCount() {return this.loadCount;}
    // 更新加载次数
    updateLoadCount() {this.loadCount++;}
    // 获得使用次数
    getUseCount() {return this.useCount;}

    releaseAll() {if (RECORD_RES_COUNT) {while (this.useCount > 0) {this.release();
            }
        }
    }
    release() {if (RECORD_RES_COUNT) {if (this.useCount > 0) {this.subCount();
                if (this.useCount == 0) {return true;} else {return false;}
            } else {return true;}
        }


    }


    subCount() {
        this.useCount --;
        let resources: string[] = Object.keys(this.resources);
        for (let index = 0; index < resources.length; index++) {const key = resources[index];
            if (ResItem.resCountMap[key] > 0) {ResItem.resCountMap[key]--;
                if (ResItem.resCountMap[key] == 0) {ResHelper.instance().release(key)
                    delete this.resources[key];
                    delete ResItem.resCountMap[key];
                }
            }
        }
    }

    addCount() {
        this.useCount++;
        let resources: string[] = Object.keys(this.resources);
        for (let index = 0; index < resources.length; index++) {const key = resources[index];
            ResItem.resCountMap[key]++;
        }
    }

    /**
     * 删除没有使用的资源
     */
    static removeUnUsedRes() {let resources: string[] = Object.keys(this.resCountMap);
        for (let index = 0; index < resources.length; index++) {const key = resources[index];
            const count = this.resCountMap[key];
            if (count === 1) {// cc.log("removeUnUsedRes uuid" + key + "count" + ResItem.resCountMap[key])
                ResHelper.instance().release(key)
                delete this.resCountMap[key];
            }
        }
    }
}

资源管理器

import ResItem from "./ResItem";
import ResInterface, {ResCallback, ResType} from "./ResInterface";
import ResHelper from "../../engine/ResHelper";

export default class ResLoader {

    private helper: ResInterface = null;

    constructor() {this.helper = ResHelper.instance();
    }

    protected resCache = {}
    /**
     * 清理单个资源
     * @param url 
     * @param type 
     */
    releaseRes(url: string, type: ResType) {let ts = this.getKey(url, type);
        let item = this.resCache[ts];
        if (item) {if (item.release()) {this.resCache[ts] = null;
            }
        }
    }

    /**
    * 删除所有资源
    */
    release() {console.log('ResLoader release ==================')
        let resources: string[] = Object.keys(this.resCache);
        for (let index = 0; index < resources.length; index++) {const key = resources[index];
            const element: ResItem = this.resCache[key];
            if (element) {element.releaseAll();
                this.resCache[key] = null;
            } else {// console.warn("ResLoader release url  =  is error",key)
            }
        }

    }

    private getKey(url: string, type: ResType) {
        let key = url + type;
        return key;
    }
    /**
     * 同时加载多个资源。* @param list 需要加载的资源列表
     * @param type 需要加载的资源类型,要求所有资源统一类型
     * @param func 加载后的回调
     * @param loader 资源加载管理器,默认是全局管理器。*/
    loadArray(list: Array<string>, type: ResType, func: (err: string, process: number) => void) {
        let resCount = 0;
        for (let index = 0; index < list.length; index++) {const element = list[index];
            this.loadRes(element, type, (err) => {
                // 不论是否都加载成功都返回。if (err) {console.log(err);
                    func(err, resCount / list.length);
                    return;
                }
                resCount++;
                func(err, resCount / list.length);
            });
        }
    }


    getItem(url: string, type: ResType) {let ts = this.getKey(url, type)
        if (this.resCache[ts]) {return this.resCache[ts]
        } else {let item = new ResItem(url, type);
            this.resCache[ts] = item;
        }

    }
    /**
     * 加载单个文件
     * @param url 
     * @param type 
     * @param callback 
     */
    loadRes(url: string, type: ResType, callback: (err: string, res: ResItem) => void) {let ts = this.getKey(url, type);
        let item: ResItem = this.resCache[ts]
        // cc.log("loadRes url",url,'ts',ts);
        if (item && item.isDone()) {callback(null, item);
            return;
        } else {if (item) {item.addCallback(callback)
                return;
            } else {item = new ResItem(url, type);
                this.resCache[ts] = item;
            }

        }


        let func: ResCallback = (err: any, res: any) => {item.updateLoadCount();
            if (err) {if (item.getLoadCount() <= 3) {console.warn("item.getLoadCount()  ===========", item.getLoadCount())
                    this.helper.loadRes(url, type, func);
                } else {console.warn("res load fail url is" + url);
                    this.resCache[ts] = null;
                    callback(err, null);
                }
            } else {item.cacheRes(res);
                if (this.resCache[ts]) {item.setLoadingFlag(true)
                    callback(err, item);
                } else {
                    // 处理加载完之前已经删除的资源
                    item.subCount();}


            }
        }
        this.helper.loadRes(url, type, func);
    }



    /**
     * 获取资源的唯一方式 
     * @param url 
     * @param type 
     */
    getRes(url: string, type: ResType) {let ts = this.getKey(url, type)
        let item = this.resCache[ts];
        if (item) {return item.getRes();
        } else {let res = this.helper.getRes(url, type);
            if (res) { // 如果其他管理器已经加载了资源,直接使用。console.log('其他加载器已经加载了次资源', url)
                let item = new ResItem(url, type);
                item.cacheRes(item)
                this.resCache[ts] = item
                return item.getRes();} else {console.warn('getRes url', url, 'ts', ts)
            }

        }
        return null;
    }

}

CocosCreator 资源辅助类

import ResInterface, {ResType, ResCallback} from "../cfw/res/ResInterface";

/**
 * 各个引擎提供的资源辅助类。需要实现 ResInterface 接口
 */
export default class ResHelper implements ResInterface {

    private static ins: ResInterface;

    static instance() {if (!this.ins) {this.ins = new ResHelper()
        }
        return this.ins;
    }

    /**
      * 加载资源
      * @param url 
      * @param type 
      * @param callback 
      */
    loadRes(url: string, type: ResType, callback: ResCallback): void {switch (type) {
            case ResType.Prefab:
                cc.loader.loadRes(url, cc.Prefab, callback)
                break;
            case ResType.Texture2D:
                cc.loader.loadRes(url, cc.Texture2D, callback)
                break;
            case ResType.SpriteFrame:
                cc.loader.loadRes(url, cc.SpriteFrame, callback)
                break;
            case ResType.Json:
                cc.loader.loadRes(url, cc.JsonAsset, callback)
                break;
            case ResType.SpriteAtlas:
                cc.loader.loadRes(url, cc.SpriteAtlas, callback)
                break;
            case ResType.Particle2D:
                cc.loader.loadRes(url, cc.ParticleAsset, callback)
                break;
            case ResType.AudioClip:
                cc.loader.loadRes(url, cc.AudioClip, callback)
                break;
        }
    }

    /**
     * 清理资源
     * @param url 
     */
    release(url: string): void {cc.loader.release(url);
    }

    getRes(url: string, type: ResType): any {switch (type) {
            case ResType.Prefab:
                return cc.loader.getRes(url, cc.Prefab);
            case ResType.Texture2D:
                return cc.loader.getRes(url, cc.Texture2D);
            case ResType.SpriteFrame:
                return cc.loader.getRes(url, cc.SpriteFrame);
            case ResType.Json:
                return cc.loader.getRes(url, cc.JsonAsset);
            case ResType.SpriteAtlas:
                return cc.loader.getRes(url, cc.SpriteAtlas);
            case ResType.Particle2D:
                return cc.loader.getRes(url, cc.ParticleAsset)
            case ResType.AudioClip:
                return cc.loader.getRes(url, cc.AudioClip)
            default:
                console.error('getRes error url is', url, 'type is', type)
                return null;
        }
    }

    getDependsRecursively(res: any): any {return cc.loader.getDependsRecursively(res)
    }
}

如何使用

  1. 我一般会先定义一个模块类,管理资源
// 模块 id
export enum ModuleID {
    LOGIN,
    LOADING,
    GAME,
    LOBBY,
    PUBLIC,
    MAX
}
import ResLoader from "../../cfw/res/ResLoader";
import AudioManager from "../../cfw/audio/AudioManager";

export default class Module {

    private loader: ResLoader;

    protected audio: AudioManager;

    protected name: string = ''
    constructor(moduleName: string) {
        this.name = moduleName;
        this.loader = new ResLoader()
        this.audio = new AudioManager(moduleName, this.loader)
    }

    getName() {return this.name;}

    getLoader() {return this.loader;}

    getAudio() {return this.audio;}
}
  1. 然后使用模块管理器管理模块
import {ModuleID} from "./Config";
import Module from "./Module";

export default class ModuleManager {private static mgrMap: Module[] = []

    private static moduleID: ModuleID = ModuleID.LOADING;

    static init(projectName: string) {for (let index = 0; index < ModuleID.MAX; index++) {this.mgrMap[index] = new Module(projectName + index);
        }
    }

    static getAudio(id: ModuleID = this.moduleID) {return this.mgrMap[id].getAudio()}

    static publicAudio() {return this.mgrMap[ModuleID.PUBLIC].getAudio()}

    static publicLoader() {return this.mgrMap[ModuleID.PUBLIC].getLoader()}

    static setModuleID(id: ModuleID) {this.moduleID = id;}
    static getLoader(id: ModuleID = this.moduleID) {return this.mgrMap[id].getLoader()}

}
  1. 使用

结语

欢迎扫码关注公众号《微笑游戏》,浏览更多内容。

欢迎扫码关注公众号《微笑游戏》,浏览更多内容。

退出移动版