cocos creator 项目总结三(资源管理方案)

    一、引言

        针对资源的管理,很多人项目可能都不会自己单独再封装一层管理。简单粗暴的使用引擎提供的接口直接releaseunUse。这样当然也是没问题,能很大程度上解决资源清理的问题。但是我经历过这么多项目以来,由衷的感觉一个好的资源管理会为你以后性能优化提供很大的帮助,特别是那些大型项目到后期因为场景多界面多导致资源管理复杂,而最终导致没管理好导致游戏内存效果高又没好的办法去降低(因为前期没规划好后期只能修修补补)。

 二、方案的抉择 

        在此项目之前我一直都是用的单例模式全局管理资源的方法,简单的通过引用计数来决定资源是否需要释放。我相信绝大多数的朋友的管理方法都是如此,但是我经历几个项目后发现这个方案有个麻烦的点就是,如果我的游戏模块化后如果我像针对一个模块所有的资源要统一释放引用那我再编写业务的时候就要比较小心的去维护这份引用数据,再在最后统一释放这份维护的引用资源。这样也能做到细致的资源管理但是对于我们业务的编写会增加一定的麻烦程度。不能让我们编写业务时将更多的心思放在具体业务上了。

        针对上面提出的问题,我的选择是,资源管理采用 单例+实例的方式管理,全局单例对象用于全局管理所有资源的引用计数,实例对象给每个模块持有,模块加载资源都统一通过实例对象去加载(实例对象加载资源会自动加入单例对象的管理中)。其实这里的单例描述也不是完全准确,只是为了让大家理解更加方便其实在实现上我就是给这个资源管理class 增加了static 的数据结构存储全局数据和static的方法提供全局性的接口。

        按上面方案实现了资源管理器后,我给 SceneMgr 分配一个ResLoader,  BaseView 分配一个 ResLoader,然后 UIMgr 恭喜 SceneMgr的 ResLoader,然后再场景切换时都重新new一个新的resloader 然后再切完场景后将之前场景的resloader releaseAll (这里之所以要切场景之类release主要是担心场景之间有公共资源依赖导致刚卸载的资源又得重新加载)。

 三、意外得惊喜

        因为我资源管理采用得 asset bundle 得方式,后面发现封装了 统一 得资源管理器后,针对引擎提供得asset bundle 加载可以指定版本得方式,我能动态通过服务器控制每个资源得asset bundle的md5的变化去控制游戏静默更新游戏资源。

四、上代码

import * as cc from "cc"

const { ccclass } = cc._decorator;

type Bundle = cc.AssetManager.Bundle;
type AssetType = typeof cc.Asset;

export class ResItem {
    private m_refCount = 0;
    private m_path : string;

    constructor( path : string ){
        this.m_path = path;
    }

    public getPath() : string{
        return this.m_path;
    }

    public getRefCount() : number{
        return this.m_refCount;
    }

    public addRef(){
        this.m_refCount++;
    }

    public decRef(){
        this.m_refCount--;
        if(this.m_refCount <= 0){
            this.destroy();
        }
    }

    protected destroy(){

    }
}

export class BundleAsset extends ResItem{
    private m_bundle : Bundle;

    constructor( nameOrUrl : string, bundle : Bundle){
        super(nameOrUrl);
        this.m_bundle = bundle;
    }

    public getBundle() : Bundle{
        return this.m_bundle;
    }

    protected destroy(){
        BaseLoader.removeBundle(this.getPath());
    }
}

export class NormalAsset extends ResItem {
    private m_asset : cc.Asset;
    private m_bundle : BundleAsset;

    constructor( path : string, asset : cc.Asset, bundle : BundleAsset){
        super(path);
        this.m_bundle = bundle;
        bundle.addRef();
        asset.addRef();
        this.m_asset = asset;
    }

    public getAsset() : cc.Asset{
        return this.m_asset;
    }

    protected destroy(){
        this.m_asset.decRef();
        this.m_bundle.decRef();
    }
}



export type OptionType = Record;
export type LoadBundleAssetCompleteFunc = (err: Error | null, bundle : BundleAsset | null) => void;
export type LoadBundleAssetProcessFunc = (percent : number) => void;

export type LoadBundleArrayAssetCompleteFunc = (err: Error | null, bundle : Map | null) => void;
export type LoadBundleArrayAssetProcessFunc = (percent : number) => void;

export type LoadAssetProcessFunc = (percent : number) => void;
export type LoadAssetCompleteFunc = (error: Error | null, assets: cc.Asset | cc.Asset[] | null | any) => void;
export type PreloadAssetCompleteFunc = (error : Error | null, items : cc.AssetManager.RequestItem[] | null | any)=>void;
export type LoadBundleDoneCallback = (error : Error | null, resPath : string, bundle : BundleAsset | null )=>void;


let AssetTypeMap : any = {
    "mp3"    : cc.AudioClip,
    "prefab" : cc.Prefab,
    "scene"  : cc.Scene,
    "proto"  : cc.TextAsset,
    "png"    : cc.SpriteFrame,
    "jpg"    : cc.SpriteFrame,
}

function removeSuffix( path : string) : string{
    let idx = path.lastIndexOf(".");
    if(idx != -1){
        return path.substring(0, idx);
    }

    return path;
}

function getSuffix( path : string) : string{
    let idx = path.lastIndexOf(".");
    if(idx != -1){
        return path.substr(idx+1);
    }

    return path;
}

@ccclass("BaseLoader")
export class BaseLoader{
    protected static m_loadedBundle : Map = new Map;
    protected static m_bundleVersions : Map = null!;


    public static getBundleVersions( bundleName : string) : string | undefined{
        if(this.m_bundleVersions == null) return undefined;
        
        return this.m_bundleVersions.get(bundleName);
    }

    //删除bundle
    public static removeBundle( nameOrUrl : string ){
        let asset = this.m_loadedBundle.get(nameOrUrl);
        if(asset){
            this.m_loadedBundle.delete(nameOrUrl);
            if(nameOrUrl != "resources")
                cc.assetManager.removeBundle(asset.getBundle());
        }
    }

    public static loadBundleArray( names : string[], onComplete : LoadBundleArrayAssetCompleteFunc, onProgress ?: LoadBundleAssetProcessFunc){
        let size = names.length;
        let count = size;
        let isDone = false;

        let bundles : Map = new Map();
        let check_done = ( err : Error | null, url : string, bundle : BundleAsset | null)=>{
            if(isDone) return;
            if(err == null && bundle != null){
                count --;
                if(count <= 0){
                    isDone = true;
                    bundles.set(url, bundle);
                    onComplete(null, bundles );
                }
            }else{
                isDone = true;
                onComplete(err, null);
            }
        }

        let filePercents : Map = new Map();
        let onePercent = 1/size;
        let updatePorcess = ( bundleUrl : string, percent : number)=>{
            if(onProgress != null){
                filePercents.set(bundleUrl, percent);
                let allpercent = 0;
                filePercents.forEach(( p : number)=>{
                    allpercent += onePercent*p;
                })
                onProgress(allpercent);
            }
        }

        for(let i = 0; i < size; i++){
            let bundleUrl = names[i]
            filePercents.set(bundleUrl, 0);
            this.loadBundle(bundleUrl, ( err : Error | null, bundle)=>{
                check_done(err, bundleUrl, bundle);
            }, (percent : number)=>{
                updatePorcess(bundleUrl, percent);
            })
        }
    }

    //加载bundle
    public static loadBundle( nameOrUrl : string, onComplete : LoadBundleAssetCompleteFunc, onprogress ?: LoadBundleAssetProcessFunc){
        let asset = this.m_loadedBundle.get(nameOrUrl);
        if(asset){
            onprogress && onprogress(1);
            onComplete(null, asset);
        }else{
            if(nameOrUrl == "resources"){
                let asset = new BundleAsset(nameOrUrl, cc.resources);
                this.m_loadedBundle.set(nameOrUrl, asset);
                onprogress && onprogress(1);
                onComplete(null, asset);
            }else{
                let options : any = {}
                if(onprogress){
                    options.onFileProgress = (loaded: number, total: number)=>{
                        onprogress(loaded/total);
                    }
                }
                let version = this.getBundleVersions(nameOrUrl)
                if(version){
                    options.version = version;
                    cc.assetManager.loadBundle(nameOrUrl, options, (err: Error | null, data: Bundle)=>{
                        if(err == null){
                            let asset = new BundleAsset(nameOrUrl, data);
                            this.m_loadedBundle.set(nameOrUrl, asset);
                            onComplete(null, asset);
                        }else{
                            onComplete(err, null);
                        }
                    });
                }else{
                    cc.assetManager.loadBundle(nameOrUrl, options, (err: Error | null, data: Bundle)=>{
                        if(err == null){
                            let asset = new BundleAsset(nameOrUrl, data);
                            this.m_loadedBundle.set(nameOrUrl, asset);
                            onComplete(null, asset);
                        }else{
                            onComplete(err, null);
                        }
                    });
                }
                
            }
        }
    }

    protected m_loadedAssets : Map = new Map;
    protected m_stackLoadedAssets : Array> = new Array;

    public releaseAsset( path : string){
        let asset = this.m_loadedAssets.get(path);
        if(asset){
            asset.decRef();
            if(asset.getRefCount() <= 0)
            {
                this.m_loadedAssets.delete(path);
            }
        }
    }

    public pushStackAssets(){
        this.m_stackLoadedAssets.push(this.m_loadedAssets);
        this.m_loadedAssets = new Map();
    }

    public popReleaseStackAssets(){
        if(this.m_stackLoadedAssets.length < 1)return;

        let assets = this.m_stackLoadedAssets.pop();
        assets?.forEach((asset : NormalAsset)=>{
            asset.getAsset().decRef();
        })
        assets?.clear();
    }

    public releaseAll(){
        this.m_loadedAssets.forEach(( asset : NormalAsset)=>{
            asset.getAsset().decRef();
        })
        this.m_loadedAssets.clear();
    }

    public loadPrefab( path : string, onComplete : LoadAssetCompleteFunc, onprogress ?: LoadBundleAssetProcessFunc){
        this.load(path, cc.Prefab, onComplete, onprogress);
    }

    public loadAudioClip(path : string, onComplete: LoadAssetCompleteFunc, onprogress ?: LoadBundleAssetProcessFunc){
        this.load(path, cc.AudioClip, onComplete, onprogress);
    }

    public loadTexture2D( path : string, onComplete : LoadAssetCompleteFunc, onprogress ?: LoadBundleAssetProcessFunc){
        this.load(path, cc.Texture2D, onComplete, onprogress);
    }

    public loadSpriteFrame( path : string, onComplete : LoadAssetCompleteFunc, onprogress ?: LoadBundleAssetProcessFunc){
        path = removeSuffix(path);
        if(path[path.length-1] != "/"){
            path += "/";
        }
        path += "spriteFrame"
        this.load(path, cc.SpriteFrame, onComplete, onprogress);
    }

    public loadSpriteAtlas( path : string, onComplete : LoadAssetCompleteFunc, onprogress ?: LoadBundleAssetProcessFunc){
        this.load(path, cc.SpriteAtlas, onComplete, onprogress);
    }

    public loadTextAsset( path : string, onComplete : LoadAssetCompleteFunc, onprogress ?: LoadBundleAssetProcessFunc){
        this.load(path, cc.TextAsset, onComplete, onprogress);
    }

    public LoadJsonAsset( path : string, onComplete : LoadAssetCompleteFunc, onProgress : LoadAssetProcessFunc | null = null){
        this.load(path, cc.JsonAsset, onComplete, onProgress);
    }

    public LoadAsset( path : string, onComplete : LoadAssetCompleteFunc, onProgress : LoadAssetProcessFunc | null = null){
        this.load(path, cc.Asset, onComplete, onProgress);
    }

    private parsePath( path : string ) : any{
        let arr = path.split("#");
        let bundleName = "resources";
        let resName = "";
        let assetType : AssetType | null = null;

        if( arr.length == 1){
            bundleName = "resources";
            resName = arr[0];
        }else if(arr.length  == 2){
            bundleName = arr[0];
            resName = arr[1];
        }else if(arr.length > 2){
            console.error("errror path get get BundleName"+ path);
            return null;
        }

        let itemArr = resName.split(".")
        if(itemArr.length == 2 && AssetTypeMap[itemArr[1]]){
            resName = itemArr[0];
            assetType = AssetTypeMap[itemArr[1]] as AssetType;
        }else{
            console.error("error path to get Asset Type" + path);
            return null;
        }

        return { bundleName : bundleName, resName : resName, assetType : assetType }
    }

    public loadArray( paths : string[], onComplete : LoadAssetCompleteFunc, onProgress : LoadAssetProcessFunc | null = null){
        let totoal = paths.length;
        let finished = 0;
        let assets : cc.Asset[] = [];
        let isError = false;

        paths.forEach(( path : string )=>{
            if(isError)return;

            let assetType = null;
            let resName = path;
            let arr = path.split(".")
            if(arr.length == 2 && AssetTypeMap[arr[1]]){
                resName = arr[0];
                assetType = AssetTypeMap[arr[1]] as AssetType;
            }
            this.load(resName, assetType, ( err : Error | null, data : cc.Asset)=>{
                if(err == null && data){
                    assets.push(data);
                    finished++;
                    if(finished < totoal){
                        onProgress && onProgress(finished/totoal);
                    }else{
                        onComplete( null, assets);
                    }
                }else{
                    isError = true;
                    onComplete(err, null);
                    return;
                }
            })
        })
    }

    private load(path : string, type : AssetType | null, onComplete : LoadAssetCompleteFunc, onProgress : LoadAssetProcessFunc | null = null){
        path = removeSuffix(path);
        let asset = this.m_loadedAssets.get(path);
        if(asset){
            asset.addRef();
            onComplete(null, asset.getAsset());
        }else{
            let bundleBase = 0.5;
            this.checkLoadBundle(path, ( err : Error | null, resPath : string, bundle : BundleAsset | null)=>{
                if( err || bundle == null ){
                    onComplete( err || new Error("load bundle" + path +" failed"), null);
                }else{
                    if(type == null){
                        bundle.getBundle().load(resPath, (finished: number, total: number, item: cc.AssetManager.RequestItem)=>{
                            if( onProgress )
                                onProgress(bundleBase+finished/total);
                        },(err, obj : cc.Asset)=>{
                            if( err == null && obj){
                                let asset = new NormalAsset(path, obj, bundle);
                                asset.addRef();
                                this.m_loadedAssets.set(path, asset);
                                onComplete(null, asset.getAsset());
                            }else{
                                onComplete(err || new Error("load asset" + path+ "failed"), null);
                            }
                        })
                    }else{
                        bundle.getBundle().load(resPath, type, (finished: number, total: number, item: cc.AssetManager.RequestItem)=>{
                            if( onProgress )
                                onProgress(bundleBase+finished/total);
                        },(err, obj : cc.Asset)=>{
                            if( err == null && obj){
                                let asset = new NormalAsset(path, obj, bundle);
                                asset.addRef();
                                this.m_loadedAssets.set(path, asset);
                                onComplete(null, asset.getAsset());
                            }else{
                                onComplete(err || new Error("load asset" + path+ "failed"), null);
                            }
                        })
                    }
                }
            }, ( percent : number)=>{
                onProgress && onProgress(percent*bundleBase);
            })
        }
    }

    private checkLoadBundle(pathName : string, onComplete : LoadBundleDoneCallback, onprogress ?: LoadBundleAssetProcessFunc){
        let arr = pathName.split("#")
        // console.log("checkLoadBundle", pathName, arr);
        
        let bundleName = "";
        let path = "";

        if(arr.length == 1){ //代表内嵌bundle的资源
            bundleName = "resources";
            path = arr[0];
        }else if(arr.length == 2){
            bundleName = arr[0];
            path = arr[1]
        }else{
            console.error("errror pathName:"+pathName);
            return;
        }

        BaseLoader.loadBundle(bundleName, ( err: Error | null, bundle : BundleAsset | null)=>{
            if( err || bundle == null){
                onComplete( err || new Error("load bundle failed"), path, null);
            }else{
                onComplete( err, path, bundle);
            }
        }, onprogress)
    }

    public loadScene(path : string, onComplete : LoadAssetCompleteFunc, onProgress : LoadAssetProcessFunc | null = null){
        this.checkLoadBundle(path, (error : Error | null, resPath : string, bundle : BundleAsset | null )=>{
            if(error == null && bundle){
                bundle.getBundle().loadScene(resPath, ( finish : number, totoal : number)=>{
                    onProgress && onProgress(finish/totoal);
                }, (err : Error | null, obj : cc.SceneAsset)=>{
                    if(err == null && obj){
                        let asset = new NormalAsset(path, obj, bundle);
                        asset.addRef();
                        this.m_loadedAssets.set(path, asset);
                    }
                    onComplete(err, obj);
                });
            }else{
                onComplete(error, bundle);
            }
        });
    }
}
import * as cc from "cc"
import { HttpUtils } from "../utils/util/HttpUtils"
import { BaseLoader } from "../../../world/BaseLoader";
const { ccclass } = cc._decorator;

@ccclass("ResLoader")
export class ResLoader extends BaseLoader{
    //需要使用 bundle 的 version 更新策略则需要使用此接口
    public static loadRemoteBundleVersions( versionFileUrl : string, doneCallback : ()=>void ){
        if(this.m_bundleVersions != null) return doneCallback();

        HttpUtils.httpGet(versionFileUrl, ( txt : string )=>{
            this.m_bundleVersions = new Map();
            let data = txt == "" ? {} : JSON.parse(txt);
            for(let key in data){
                this.m_bundleVersions.set(key, data[key]);
            }
            doneCallback();
        })
    }
}

这里之所以将其拆成两个 class,是因为我的world模块是个比较纯净模块,不希望依赖太多的脚本,所以我将BaseResloader 放到了 world 模块用于启动加载游戏使用。

你可能感兴趣的:(cocos,creator,creator)