天天看點

小遊戲開發之資源管理(跨引擎)

前言

資源管理是記憶體優化的一部分,對于大型遊戲,資源管理不明确,很容易出現記憶體不足而閃退的情況。

說到資源也就涉及到了資源劃分,這部分内容可以看另一篇文章《遊戲開發之目錄劃分》。

資料總管需要考慮的情況

  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)
    }
}
           

結語

歡迎掃碼關注公衆号《微笑遊戲》,浏覽更多内容。

歡迎掃碼關注公衆号《微笑遊戲》,浏覽更多内容。