CocosCreator 一般的なフレームワーク設計リソース管理

CocosCreator 一般的なフレームワーク設計リソース管理

Cocos Creator を使用して大規模なゲームを作成する場合、リソース管理は解決しなければならない問題です。ゲームが進むにつれて、現在使用しているリソースがごくわずかで、cc.loader.release を使用して以前にロードしたリソースを解放している場合でも、ゲームのメモリ使用量は増えるばかりで、減ることはありません。ただし、以前に使用したリソースのほとんどはメモリ内に残ります。なぜこのようなことが起こるのでしょうか?

Cocos Creator のリソース管理に関する問題

リソース管理は、主にリソースの読み込み、リソースの検索 (使用)、リソースの解放という 3 つの問題を解決します。ここで議論する主な問題は、リソースの解放の問題です。この問題は非常に単純に見え、Cocos2d-x では非常に単純ですが、リソースが解放できるかどうかを追跡するのが難しいため、js では複雑になります。

Cocos2d-x では、参照カウントを使用します。参照カウントが 0 の場合、参照カウントを維持するだけで済みます。また、Cocos2d-x のリソース管理は比較的分散化されています。エンジン レベルでは、特定のリソースを管理するために、TextureCache や AudioManager などのシングルトンのみが提供されます。ほとんどのリソースは自分で管理する必要があります。cocos Creator では、リソースは cc.loader によって一元管理され、プレハブが広く使用されています。プレハブとさまざまなリソース間の複雑な参照関係により、リソース管理の難易度が増します。

リソースの依存関係

リソース A はリソース B、C、D に依存し、リソース D はリソース E に依存します。これは、非常に一般的なリソース依存関係の状況ですcc.loader.loadRes("A")を使用してリソース A をロードすると、B ~ E がすべてロードされますが、 cc.loader.release("A")を呼び出すと、リソース A のみが解放されます。

ロードされたすべてのリソースは cc.loader の _cache に格納されますが、cc.loader.release はリソースの依存関係を考慮せずに渡されたリソースのみを解放します。

cc.loader の背後にあるリソース読み込みプロセスに興味がある場合は、次のサイトを参照してください: https://www.cnblogs.com/ybgame/p/10576884.html

依存するリソースをまとめて解放したい場合、Cocos Creator はcc.loader.getDependsRecursively;という扱いにくいメソッドを提供します。このメソッドは、指定されたリソースが依存するすべてのリソースを再帰的に取得し、それらを配列に入れて返した後、その配列を cc.loader.release に渡します。cc.loader はそれらを走査し、1 つずつ解放します。

このメソッドはリソースを解放できますが、解放すべきでないリソースを解放してしまう可能性があります。D に依存するリソース F がある場合、F リソースは正常に動作しなくなります。 cocos クリエイター エンジンはリソースの依存関係を適切に管理していなかったため、D をリリースしたときに F がまだ私たちに依存していることを知りませんでした。 F 依存関係がない場合でも、D を解放できるかどうかはわかりません。たとえば、cc.loader を呼び出して D をロードし、次に A をロードします。この時点で、D はロードされており、A を直接使用できます。しかし、A がリリースされるときに D もリリースされる場合、これは期待どおりではありません。私たちが期待しているのは、D を明示的にリリースしない場合、D が他のリソースのリリースとともに自動的にリリースされるべきではないということです。

Chrome の開発者モードを開いて、コンソール パネルに入力するだけでテストできます。古いバージョンの cocos Creator の場合は、cc.textureCache にすべてのテクスチャをダンプできます。新しいバージョンでは、textureCache が削除されますが、cc.loader._cache と入力するとすべてのリソースを表示できます。リソースが多すぎて、数量だけが気になる場合は、Object.keys(cc.loader._cache).length と入力して、リソースの合計数を表示できます。リソースをロードする前、ロードした後、解放した後にそれぞれ 1 回ダンプして、cc.loader のキャッシュ ステータスを比較できます。もちろん、イメージのみをダンプしたり、前回のダンプと前回のダンプの差分をダンプするなど、便利なメソッドを記述することもできます。

リソースの使用

リソース依存性の問題に加えて、リソース使用の問題も解決する必要があります。前者は cc.loader 内のリソース編成の問題であり、後者はアプリケーション層ロジックにおけるリソース使用の問題です。たとえば、インターフェースが閉じられたときにリソースを解放する必要がある場合、閉じられていない別のインターフェースがリソースを使用しているかどうかなど、解放するかどうかの問題にも直面します。リソースが他の場所で使用されている場合は、解放しないでください。

レスローダー

ここで、cc.loader が解決できなかった問題を解決するために ResLoader を設計しました。鍵となるのは、リソースごとに CacheInfo を作成し、リソースの依存関係と使用状況の情報を記録して、リソースを解放できるかどうかを判断することです。cc.loader.loadRes() の代わりに ResLoader.getInstance().loadRes() を使用し、cc.loader.releaseRes() の代わりに ResLoader.getInstance().releaseRes() を使用します。

依存関係については、ResLoader はリソースがロードされると自動的にマッピングを確立し、リソースが解放されると自動的にマッピングをキャンセルします。また、マップされていないリソースを解放できるかどうかを検出し、解放ロジックに従います。

使用状況については、リソースがどこで使用されているか、およびリソースが他の場所でも使用されているかどうかを区別するための使用パラメータが提供されます。リソースが他のリソースに依存しておらず、他のロジックによって使用されていない場合、リソースを解放できます。

/**
 * リソース読み込みクラス* 1. 読み込み完了後に参照関係を自動的に記録し、DependKeys に従って逆依存関係を記録します。* 2. リソースの使用をサポートします。たとえば、特定の開かれた UI がリソース A を使用していて、リソース B が他の場所で解放された場合、リソース B はリソース A を参照します。リソース A を参照する他のリソースがない場合、リソース A の解放がトリガーされます。
 * 3. 依存リソースを安全に解放する機能(リソースが複数のリソースによって同時に参照され、他のすべてのリソースが解放された場合にのみリソースが解放される)
 * 
 * 2018-7-17 by Bao Ye*/

// リソース読み込み処理コールバック export type ProcessCallback = (completedCount: number, totalCount: number, item: any) => void;
// リソース読み込み完了コールバック export type CompletedCallback = (error: Error, resource: any) => void;

// 構造体インターフェースCacheInfoの参照と使用{
    参照: Set<文字列>、
    使用法: Set<文字列>
}

// LoadRes メソッドパラメータ構造インターフェース LoadResArgs {
    url: 文字列、
    タイプ?: typeof cc.Asset、
    onCompleted?: CompletedCallback、
    onProgess?: プロセスコールバック、
    使用?: 文字列、
}

// ReleaseRes メソッドパラメータ構造インターフェース ReleaseResArgs {
    url: 文字列、
    タイプ?: typeof cc.Asset、
    使用?: 文字列、
}

// 互換性処理 let isChildClassOf = cc.js["isChildClassOf"]
if (!isChildClassOf) {
    子クラスを cc に代入します。
}

デフォルトクラスResLoaderをエクスポートします。

    プライベート _resMap: Map<string, CacheInfo> = 新しい Map<string, CacheInfo>();
    プライベート静的_resLoader: ResLoader = null;
    パブリック静的getInstance(): ResLoader {
        if (!this._resLoader) {
            this._resLoader = 新しい ResLoader();
        }
        this._resLoader を返します。
    }

    パブリック静的破棄(): void {
        if (this._resLoader) {
            this._resLoader = null;
        }
    }

    プライベートコンストラクタ() {

    }

    /**
     * cc.loaderからリソースアイテムを取得する
     * @param url クエリURL
     * @param type クエリするリソースタイプ*/
    プライベート_getResItem(url: 文字列、タイプ: typeof cc.Asset): 任意{
        ccloader: any = cc.loader; とします。
        item = ccloader._cache[url]とします。
        if (!item) {
            uuid = ccloader._getResUuid(url, type, false);
            (UUID)の場合{
                ref = ccloader._getReferenceKey(uuid); とします。
                アイテム = ccloader._cache[ref];
            }
        }
        返品商品;
    }

    /**
     * loadRes メソッドのパラメータ前処理 */
    プライベート_makeLoadResArgs(): LoadResArgs {
        引数の長さが 1 の場合、引数の型は [0] != "文字列") {
            console.error(`_makeLoadResArgs エラー ${arguments}`);
            null を返します。
        }
        ret: LoadResArgs = { url: arguments[0] };
        (i = 1 とします; i < 引数の長さ; ++i) {
            i == 1 の場合、isChildClassOf(arguments[i], cc.RawAsset) {
                // 最初のパラメータ型かどうかを判定する
                ret.type = 引数[i];
            } そうでない場合、(i == arguments.length - 1 && typeof arguments[i] == "string") {
                // 最後のパラメータ使用かどうかを判定する
                ret.use = 引数[i];
            } そうでない場合 (typeof arguments[i] == "function") {
                // それ以外の場合は関数です if (arguments.length > i + 1 && typeof arguments[i + 1] == "function") {
                    ret.onProgess = 引数[i];
                } それ以外 {
                    ret.onCompleted = 引数[i];
                }
            }
        }
        ret を返します。
    }

    /**
     * releaseRes メソッドのパラメータ前処理 */
    プライベート_makeReleaseResArgs(): ReleaseResArgs {
        引数の長さが 1 の場合、引数の型は [0] != "文字列") {
            console.error(`_makeReleaseResArgs エラー ${arguments}`);
            null を返します。
        }
        ret: ReleaseResArgs = { url: arguments[0] };
        (i = 1 とします; i < 引数の長さ; ++i) {
            if (typeof 引数[i] == "文字列") {
                ret.use = 引数[i];
            } それ以外 {
                ret.type = 引数[i];
            }
        }
        ret を返します。
    }

    /**
     * キーを使用してリソースを生成する
     * @param シーン、UI、プールなど、使用する場所
     * @param who ユーザー (Login、UIHelp など)
     * @param why 使用理由、カスタム...
     */
    パブリック静的 makeUseKey(where: 文字列、who: 文字列 = "none", why: 文字列 = ""): 文字列 {
        `use_${where}_by_${who}_for_${why}` を返します。
    }

    /**
     * リソースキャッシュ情報を取得する * @param key 取得するリソースURL
     */
    パブリックgetCacheInfo(キー: 文字列): CacheInfo {
        if (!this._resMap.has(key)) {
            this._resMap.set(キー、{
                参照: 新しい Set<string>()、
                使用法: new Set<string>()
            });
        }
        this._resMap.get(key) を返します。
    }

    /**
     * リソースの読み込みを開始 * @param url リソースのURL
     * @param type リソースタイプ、デフォルトは null
     * @param onProgess 読み込み進捗状況コールバック* @param onCompleted 読み込み完了コールバック* @param use makeUseKey メソッドに従って生成されたリソース使用キー*/
    パブリック loadRes(url: 文字列、use?: 文字列);
    パブリック loadRes(url: string, onCompleted: CompletedCallback, use?: string);
    パブリック loadRes(url: 文字列、onProgess: ProcessCallback、onCompleted: CompletedCallback、use?: 文字列);
    パブリック loadRes(url: 文字列、タイプ: typeof cc.Asset、使用方法: 文字列);
    パブリック loadRes(url: string, type: typeof cc.Asset, onCompleted: CompletedCallback, use?: string);
    パブリック loadRes(url: string、type: typeof cc.Asset、onProgess: ProcessCallback、onCompleted: CompletedCallback、use?: string);
    パブリックloadRes() {
        resArgs: LoadResArgs = this._makeLoadResArgs.apply(this, arguments); とします。
        console.time("loadRes|"+resArgs.url);
        let finishCallback = (error: Error, resource: any) => {
            // 逆関連付け参照 (参照されているすべてのリソースに、このリソースが参照していることを示すマークを付ける)
            addDependKey = (item, refKey) を追加します => {
                if (item && item.dependKeys && Array.isArray(item.dependKeys)) {
                    for (let depKey of item.dependKeys) {
                        //リソースが自分によって参照されていることを記録します this.getCacheInfo(depKey).refs.add(refKey);
                        // cc.log(`${depKey} は ${refKey} によって参照されます`);
                        ccloader: any = cc.loader; とします。
                        depItem = ccloader._cache[depKey]とします。
                        依存キーを追加します(depItem、refKey)
                    }
                }
            }

            item = this._getResItem(resArgs.url, resArgs.type); とします。
            if (アイテム && item.url) {
                DependKey を追加します(アイテム、アイテム.url);
            } それ以外 {
                cc.warn(`addDependKey 項目エラー1! for ${resArgs.url}`);
            }

            // 自分自身への参照を追加する if (item) {
                info = this.getCacheInfo(item.url); とします。
                info.refs.add(item.url);
                // リソース使用量を更新する if (resArgs.use) {
                    info.uses.add(resArgs.use);
                }
            }

            // 実行完了コールバック if (resArgs.onCompleted) {
                resArgs.onCompleted(エラー、リソース);
            }
            console.timeEnd("loadRes|"+resArgs.url);
        };

        // リソースがロードされたかどうかを予測します。let res = cc.loader.getRes(resArgs.url, resArgs.type);
        もし(res){
            終了コールバック(null、res);
        } それ以外 {
            cc.loader.loadRes(resArgs.url、resArgs.type、resArgs.onProgess、finishCallback);
        }
    }

    /**
     * リソースを解放する * @param url 解放するURL
     * @param type リソースタイプ* @param use 解放するリソース使用キー。makeUseKey メソッドに従って生成されます*/
    パブリック releaseRes(url: 文字列、use?: 文字列);
    パブリック releaseRes(url: 文字列、タイプ: typeof cc.Asset、使用方法: 文字列)
    パブリックreleaseRes() {
        /**リソースを一時的に解放しない*/
        // 戻る;

        resArgs: ReleaseResArgs = this._makeReleaseResArgs.apply(this, arguments); とします。
        item = this._getResItem(resArgs.url, resArgs.type); とします。
        if (!item) {
            console.warn(`releaseRes 項目が null ${resArgs.url} ${resArgs.type} です`);
            戻る;
        }
        cc.log("resloader リリースアイテム");
        // cc.log(引数);
        cacheInfo を this.getCacheInfo(item.url); に設定します。
        (resArgs.use)の場合{
            cacheInfo.uses.delete(resArgs.use)
        }
        this._release(アイテム、アイテム.url);
    }

    // リソースを解放する private _release(item, itemUrl) {
        if (!item) {
            戻る;
        }
        cacheInfo を this.getCacheInfo(item.url); に設定します。
        // 自身への参照を削除します。cacheInfo.refs.delete(itemUrl);

        (cacheInfo.uses.size == 0 && cacheInfo.refs.size == 0)の場合{
            // 逆参照 let delDependKey = (item, refKey) => {
                if (item && item.dependKeys && Array.isArray(item.dependKeys)) {
                    for (let depKey of item.dependKeys) {
                        ccloader: any = cc.loader; とします。
                        depItem = ccloader._cache[depKey]とします。
                        this._release(depItem、refKey);
                    }
                }
            }
            delDependKey(アイテム、アイテムUrl);
            //UUIDがない場合はURLを直接解放します
            if (item.uuid) {
                cc.loader.release(item.uuid);
                cc.log("resloader が uuid でアイテムを解放:" + item.url);
            } それ以外 {
                cc.loader.release(アイテムのURL);
                cc.log("resloader は URL でアイテムをリリースしました:" + item.url);
            }
        }
    }

    /**
     * リソースを解放できるかどうかを決定する * @param url リソースのURL
     * @param type リソースタイプ* @param use 解放するリソース使用キー。makeUseKey メソッドに従って生成されます*/
    パブリック checkReleaseUse(url: string, use?: string): boolean;
    パブリック checkReleaseUse(url: 文字列、タイプ: typeof cc.Asset、使用?: 文字列): ブール値
    パブリック checkReleaseUse() {
        resArgs: ReleaseResArgs = this._makeReleaseResArgs.apply(this, arguments); とします。
        item = this._getResItem(resArgs.url, resArgs.type); とします。
        if (!item) {
            console.log(`解放できません、アイテムが null ${resArgs.url} ${resArgs.type}`);
            true を返します。
        }

        cacheInfo を this.getCacheInfo(item.url); に設定します。
        checkUse を false にします。
        checkRef = false とします。

        resArgs.use と cacheInfo.uses.size > 0 の場合 {
            cacheInfo.uses.size == 1 の場合、cacheInfo.uses.has(resArgs.use) になります。
                チェック使用 = true;
            } それ以外 {
                チェック使用 = false;
            }
        } それ以外 {
            チェック使用 = true;
        }

        ((cacheInfo.refs.size == 1 && cacheInfo.refs.has(item.url))|| cacheInfo.refs.size == 0)の場合{
            チェックRef = true;
        } それ以外 {
            チェックRef = false;
        }

        checkUse && checkRef; を返します。
    }
}

ResLoaderの使用

ResLoader の使い方は非常に簡単です。簡単な例を示します。ダンプ ボタンをクリックすると、現在のリソースの合計数が表示されます。cc.load と cc.release をクリックして、一度ダンプします。最初は 36 個のリソースがあり、ロード後は 40 個のリソース、解放後は 39 個のリソースがあることがわかります。解放されるリソースは 1 つだけです。

ResLoader を使用してテストすると、解放後には 34 個のリソースしかないことがわかります。これは、以前にロードされたシーンのリソースもテスト リソースに依存しているため、これらのリソースも解放されるためです。ResLoader を使用してリソースをロードおよびアンロードする限り、リソース漏洩の問題は発生しません。

サンプルコード:

@ccクラス
デフォルトクラスNetExampleをエクスポートし、cc.Componentを拡張します。
    @property(cc.Node)
    アタッチノード: cc.Node = null;
    @property(cc.ラベル)
    dumpLabel: cc.Label = null;

    onLoadRes() {
        cc.loader.loadRes("Prefab/HelloWorld", cc.Prefab, (エラー: エラー、プレファブ: cc.Prefab) => {
            もしエラーなら
                cc.instantiate(prefab).parent = this.attachNode;
            }
        });
    }

    onUnloadRes() {
        this.attachNode.removeAllChildren(true);
        cc.loader.releaseRes("プレハブ/HelloWorld");
    }

    onMyLoadRes() {
        ResLoader.getInstance().loadRes("Prefab/HelloWorld", cc.Prefab, (エラー: エラー、プレファブ: cc.Prefab) => {
            もしエラーなら
                cc.instantiate(prefab).parent = this.attachNode;
            }
        });
    }

    オンマイアンロードRes() {
        this.attachNode.removeAllChildren(true);
        ResLoader.getInstance().releaseRes("Prefab/HelloWorld");
    }

    オンダンプ() {
        Loader:any = cc.loader; とします。
        this.dumpLabel.string = `現在のリソースの合計数: ${Object.keys(Loader._cache).length}`;
    }
}

上記の例では、ノードが最初に削除され、その後解放されていることがわかります。これが正しい使用方法です。削除せずに直接解放するとどうなるでしょうか? ?テクスチャが解放されるため、Cocos Creator は後続のレンダリングで引き続きエラーを報告します。

ResLoader は単なる基盤です。ResLoader を直接使用する場合、リソースの依存関係を気にする必要はありませんが、リソースの使用については気にする必要があります。実際の使用では、リソースのライフサイクルを次のようにする必要があります。

  • オブジェクトのライフサイクルに従い、オブジェクトが破棄されたときにリソースを解放します。
  • 特定のインターフェースのライフサイクルに従い、インターフェースが閉じられるとリソースが解放されます。
  • シーンのライフサイクルに従い、シーンが切り替わるときにリソースが解放されます。

オブジェクトにぶら下がっているコンポーネントを実装できます。オブジェクト内またはオブジェクトの他のコンポーネントにロジックを記述してリソースをロードする場合、このリソース管理コンポーネントを使用してリソースをロードし、このコンポーネントがリソースの解放を維持します。インターフェースやシーンも似ています。 。

プロジェクト コードは https://github.com/wyb10a10/cocos_creator_framework にあります。Scene ディレクトリの ResExample シーンを開くと表示できます。

上記は、CocosCreator の一般的なフレームワーク設計のリソース管理の詳細な内容です。CocosCreator フレームワーク設計のリソース管理の詳細については、123WORDPRESS.COM の他の関連記事に注目してください。

以下もご興味があるかもしれません:
  • Unity3Dはカメラレンズの動きを実現し、角度を制限する
  • CocosCreatorで複数のタイマーを使用する方法の詳細な説明
  • CocosCreator 学習モジュールスクリプト
  • CocosCreator で物理エンジン ジョイントを使用する方法
  • CocosCreatorでJSZip圧縮を使用する方法
  • CocosCreator 入門チュートリアル: TS で初めてのゲームを作る
  • CocosCreator ソースコードの解釈: エンジンの起動とメインループ
  • CocosCreatorでリストを作成する方法
  • CocosCreator で http と WebSocket を使用する方法
  • CocosCreator の新しいリソース管理システムの分析
  • CocosCreator でカメラトラッキングに cc.follow を使用する方法

<<:  MySQLでテーブルデータを削除する方法

>>:  Docker-compose を使用して Django アプリケーションをオフラインでデプロイする方法

推薦する

WeChatアプレットはシンプルなチャットルームを実装します

この記事では、WeChatアプレットの具体的なコードを共有し、簡単なチャットルームを実装します。具体...

詳細なハードウェア情報を取得するための Linux のいくつかのコマンドの詳細な説明

Linux システム、特にサーバー システムでは、デバイスのハードウェア情報を表示する必要がよくあり...

MySQL での %% のようなファジークエリの実装

1、%: 0 個以上の任意の文字を表します。あらゆるタイプと長さの文字に一致します。場合によっては、...

UA による Web サイトのクロールを防ぐ Nginx のクローラー対策戦略

クローラー対策ポリシー ファイルを追加しました: vim /usr/www/server/nginx...

Linux ソフトウェアのインストール場所を確認する簡単な方法

1. ソフトウェアのインストールパスを確認します。 Linuxソフトウェアをインストールできる場所は...

HTML 名、ID、クラス (フォーマット/アプリケーション シナリオ/機能) などの違いの紹介。

ページには多くのコントロール (要素またはタグ) があります。これらのタグをより便利に操作するには、...

DOCTYPE 文書型宣言 (Web ページ愛好家必読)

DOCTYPE 宣言 作成するすべてのページの先頭に、ドキュメント宣言が必要です。はい、そうでしょう...

フロントエンドパフォーマンス最適化に関する補足記事

序文私は、Web サイトのフロントエンド パフォーマンス最適化のための JavaScript と C...

Linuxにpipパッケージをインストールする方法

1. システムの Python バージョンに応じて、pip インストール パッケージをダウンロードし...

ユーザーエクスペリエンスの76の経験ポイントの要約

ウェブサイト体験の分類1. 感覚体験:快適性を重視した視聴覚体験をユーザーに提供します。 2. イン...

iframe の src を about:blank に設定した後の詳細

iframe の src を 'about:blank' に設定した後、"...

Docker+DockerCompose を使用して Web アプリケーションをカプセル化する方法

目次テクノロジースタックバックエンドビルドAPIフロントエンドウェブ構築ゲートウェイ建設ゲートウェイ...

jQuery エディタ プラグイン tinyMCE の使い方

簡略化されたファイル サイズを変更し、サンプルをダウンロードします。ファイルをローカル コンピュータ...

Vue3.xはコンポーネント通信にmitt.jsを使用します

目次クイックスタート使い方基本原則Vue2.x はコンポーネント通信に EventBus を使用しま...

クリックイメージ反転効果を実現するJavaScript

最近、顔コレクションに関するプロジェクトに取り組んでいましたが、フロントエンドモジュールを書いている...