前言
资源管理是内存优化的一部分,对于大型游戏,资源管理不明确,很容易出现内存不足而闪退的情况。
说到资源也就涉及到了资源划分,这部分内容可以看另一篇文章《游戏开发之目录划分》。
资源管理器需要考虑的情况
- 加载完成的回调
- 加载失败后的尝试
- 多个相同请求的处理。
- 未加载成功之前已经删除。
- 资源的使用情况,记数。
- 跨引擎使用。
各个引擎需要提供的辅助类需要实现的接口
/** * 自定义的资源分类,对应各个引擎中相同的资源。 */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) }}
如何使用
- 我一般会先定义一个模块类,管理资源
//模块idexport 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; }}
- 然后使用模块管理器管理模块
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() }}
- 使用
结语
欢迎扫码关注公众号《微笑游戏》,浏览更多内容。
欢迎扫码关注公众号《微笑游戏》,浏览更多内容。