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