【Cocos Creator】アタッチするだけ!MemoryMonitor (メモリ監視)の実装方法【TypeScript】

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

【Cocos Creator 3.8】MemoryMonitor の実装:アタッチするだけでゲーム実行中のメモリ使用量を監視・可視化する汎用スクリプト

このガイドでは、任意のノードにアタッチするだけで「現在のメモリ使用量」を定期的に取得し、画面上に数値として表示する MemoryMonitor コンポーネントを実装します。

ブラウザ・ネイティブ環境ともに「OS 全体の正確な RAM 使用量」を直接取得することはできませんが、以下のような値を監視することで、メモリリークの兆候を把握できます。

  • Web 環境: performance.memory(Chrome などの一部ブラウザで利用可能)
  • その他環境: 監視不能な場合は「不明」と表示し、ログで理由を通知

MemoryMonitor は、TextLabel を自動生成して画面に表示するため、他ノードへの参照は一切不要です。スクリプトを 1 ノードにアタッチするだけで動作します。


コンポーネントの設計方針

1. 監視対象と制約の整理

  • JavaScript / Web 環境では、ブラウザが提供する performance.memory(非標準 API)から概算のメモリ使用量を取得可能。
  • ネイティブビルドや一部ブラウザでは、メモリ情報 API が利用できないため、取得不能時は
    • 画面表示: 「N/A」などのテキスト
    • ログ: 利用できない旨を warn で出力
  • ゲーム内からは「Cocos の内部メモリ」だけでなく「プロセス全体の OS RAM 使用量」を直接取得する手段はないため、「あくまでヒューリスティックな監視ツール」であることを前提にする。

2. 外部依存をなくすための設計

  • GameManager やシングルトンなど、外部スクリプトへの依存は一切持たない。
  • メモリ表示用のラベルノードを、自分自身の子ノードとして自動生成する。
    • 自前で Label コンポーネントを追加するので、ユーザ側で Text ノードを用意する必要はない。
    • ただし、フォントや色などはインスペクタで調整可能にする。
  • 必要コンポーネント:
    • Labelcc.Label)… メモリ使用量のテキスト表示用
  • 実行時に Label が取得できなければ、自動で追加を試みる。取得・追加に失敗した場合はエラーログを出す。

3. インスペクタで設定可能なプロパティ設計

MemoryMonitor に用意するプロパティと役割は以下の通りです。

  • enabledMonitor: boolean
    • 監視の ON/OFF 切り替え。
    • true のときのみ update 内でメモリを取得・表示。
  • updateInterval: number
    • メモリ情報の更新間隔(秒)。
    • 例: 0.5 なら 0.5 秒ごとに表示更新。
    • 極端に小さくするとパフォーマンスに影響する可能性があるため、最低 0.1 秒などにクランプ。
  • showInBytes: boolean
    • true: Byte / KB / MB / GB などの単位付きで人間に読みやすく表示。
    • false: 生のバイト数(数値のみ)を表示。
  • decimalPlaces: number
    • showInBytestrue のとき、小数点以下の桁数。
    • 例: 1 → 123.4 MB のように表示。
  • warningThresholdMB: number
    • 警告閾値(MB)。
    • この値を超えた場合、ラベルの色を警告色に変更し、コンソールに warn を出す。
    • 0 以下なら「閾値なし」として扱う。
  • labelColorNormal: Color
    • 通常時(閾値未満)のラベル色。
    • 例: 白色。
  • labelColorWarning: Color
    • 閾値を超えたときのラベル色。
    • 例: 赤色。
  • fontSize: number
    • ラベルのフォントサイズ。
  • position: Vec3
    • このコンポーネントがアタッチされたノードから見た相対座標。
    • 画面のどこにメモリ表示を出すかを制御。
  • anchorToUITransform: boolean
    • このノードが UITransform を持つ UI ノードの場合に、アンカー基準で位置調整するかどうか。
    • UI シーンでのレイアウト時に便利。
  • prefixText: string
    • 表示テキストの先頭に付けるラベル。
    • 例: "Memory: "Memory: 123.4 MB
  • showUnavailableReason: boolean
    • メモリ情報が取得できない環境で、「Unavailable」などの理由テキストを表示するかどうか。

TypeScriptコードの実装

以下が完成した MemoryMonitor.ts の全コードです。


import { _decorator, Component, Label, Node, Color, Vec3, UITransform, sys } from 'cc';
const { ccclass, property } = _decorator;

@ccclass('MemoryMonitor')
export class MemoryMonitor extends Component {

    @property({
        tooltip: 'メモリ監視を有効にするかどうか。false の場合、ラベルは表示されますが値は更新されません。'
    })
    public enabledMonitor: boolean = true;

    @property({
        tooltip: 'メモリ情報の更新間隔(秒)。0.1 以上を推奨。'
    })
    public updateInterval: number = 0.5;

    @property({
        tooltip: '人間に読みやすい単位(KB / MB / GB)で表示するかどうか。false の場合は生のバイト数を表示します。'
    })
    public showInBytes: boolean = true;

    @property({
        tooltip: 'showInBytes が true のときの小数点以下の桁数。'
    })
    public decimalPlaces: number = 1;

    @property({
        tooltip: '警告閾値(MB)。0 以下の場合は閾値チェックを行いません。'
    })
    public warningThresholdMB: number = 0;

    @property({
        tooltip: '通常時のラベル色。'
    })
    public labelColorNormal: Color = new Color(255, 255, 255, 255);

    @property({
        tooltip: '閾値を超えたときのラベル色。'
    })
    public labelColorWarning: Color = new Color(255, 80, 80, 255);

    @property({
        tooltip: 'ラベルのフォントサイズ。'
    })
    public fontSize: number = 20;

    @property({
        tooltip: 'このノードから見たラベルのローカル座標。'
    })
    public position: Vec3 = new Vec3(0, 0, 0);

    @property({
        tooltip: 'UITransform を持つノードの場合、アンカー基準の位置として扱うかどうか。'
    })
    public anchorToUITransform: boolean = true;

    @property({
        tooltip: '表示テキストの先頭に付けるプレフィックス。例: "Memory: "'
    })
    public prefixText: string = 'Memory: ';

    @property({
        tooltip: 'メモリ情報が取得できない環境で、その旨のテキストを表示するかどうか。'
    })
    public showUnavailableReason: boolean = true;

    // 内部用: 表示に使用する Label コンポーネント
    private _label: Label | null = null;
    private _elapsed: number = 0;
    private _lastWarnedOverThreshold: boolean = false;
    private _memoryApiAvailable: boolean | null = null;
    private _unavailableMessage: string = 'N/A';

    onLoad () {
        // 更新間隔の最低値を制限
        if (this.updateInterval <= 0) {
            this.updateInterval = 0.5;
        }

        // 小数桁数の制限
        if (this.decimalPlaces < 0) {
            this.decimalPlaces = 0;
        }

        // ラベルの取得または自動生成
        this._ensureLabel();
        this._checkMemoryApiAvailability();
        this._updateUnavailableMessage();

        // 初期表示
        this._updateLabelText(0, true);
    }

    start () {
        // 特に処理は不要だが、将来拡張に備えてメソッドを定義
    }

    update (dt: number) {
        if (!this.enabledMonitor) {
            return;
        }

        this._elapsed += dt;
        if (this._elapsed < this.updateInterval) {
            return;
        }
        this._elapsed = 0;

        // メモリ情報の取得
        const memoryInfo = this._getMemoryUsageBytes();
        if (memoryInfo === null) {
            // 取得できない場合
            this._updateLabelText(0, false);
            return;
        }

        const usedBytes = memoryInfo.usedJSHeapSize;
        this._updateLabelText(usedBytes, true);
        this._checkWarningThreshold(usedBytes);
    }

    /**
     * ラベルコンポーネントを確実に用意する。
     * 既に子ノードに Label があればそれを利用し、なければ自動生成する。
     */
    private _ensureLabel () {
        // 1. 自ノードに Label があるか確認
        let label = this.getComponent(Label);
        if (!label) {
            // 2. 子ノードから Label を探す
            const children = this.node.children;
            for (let i = 0; i < children.length; i++) {
                const childLabel = children[i].getComponent(Label);
                if (childLabel) {
                    label = childLabel;
                    break;
                }
            }
        }

        // 3. 見つからなければ自動生成
        if (!label) {
            const childNode = new Node('MemoryMonitorLabel');
            this.node.addChild(childNode);
            label = childNode.addComponent(Label);
            if (!label) {
                console.error('[MemoryMonitor] Label コンポーネントの追加に失敗しました。');
                return;
            }
        }

        this._label = label;
        this._applyLabelStyle();
        this._applyLabelPosition();
    }

    /**
     * Label のスタイル(色・フォントサイズ)を適用。
     */
    private _applyLabelStyle () {
        if (!this._label) {
            console.error('[MemoryMonitor] Label コンポーネントが見つかりません。');
            return;
        }
        this._label.fontSize = this.fontSize;
        this._label.color = this.labelColorNormal;
    }

    /**
     * Label の位置をプロパティに基づいて設定。
     */
    private _applyLabelPosition () {
        if (!this._label || !this._label.node) {
            return;
        }

        const labelNode = this._label.node;

        if (this.anchorToUITransform) {
            const uiTrans = this.node.getComponent(UITransform);
            if (uiTrans) {
                // UITransform がある場合は、そのローカル座標系で position を適用
                labelNode.setPosition(this.position);
                return;
            }
        }

        // 通常のローカル座標として適用
        labelNode.setPosition(this.position);
    }

    /**
     * 実行環境でメモリ API が利用可能かどうかをチェック。
     */
    private _checkMemoryApiAvailability () {
        // すでにチェック済みならスキップ
        if (this._memoryApiAvailable !== null) {
            return;
        }

        // Web 環境で performance.memory が存在するか確認
        if (sys.isBrowser) {
            const perf: any = (window as any).performance;
            if (perf && perf.memory && typeof perf.memory.usedJSHeapSize === 'number') {
                this._memoryApiAvailable = true;
                return;
            }
        }

        this._memoryApiAvailable = false;
        console.warn('[MemoryMonitor] この環境ではメモリ情報 API が利用できません。表示は "N/A" になります。');
    }

    /**
     * メモリ情報が取得できない場合のメッセージを更新。
     */
    private _updateUnavailableMessage () {
        if (!this.showUnavailableReason) {
            this._unavailableMessage = '';
            return;
        }

        if (!sys.isBrowser) {
            this._unavailableMessage = 'Unavailable (non-browser platform)';
        } else {
            this._unavailableMessage = 'Unavailable (performance.memory not supported)';
        }
    }

    /**
     * メモリ使用量をバイト単位で取得。
     * 利用できない場合は null を返す。
     */
    private _getMemoryUsageBytes (): { usedJSHeapSize: number } | null {
        if (!this._memoryApiAvailable) {
            return null;
        }

        try {
            const perf: any = (window as any).performance;
            const mem = perf.memory;
            if (!mem || typeof mem.usedJSHeapSize !== 'number') {
                return null;
            }
            return { usedJSHeapSize: mem.usedJSHeapSize };
        } catch (e) {
            console.warn('[MemoryMonitor] メモリ取得中にエラーが発生しました:', e);
            return null;
        }
    }

    /**
     * ラベルのテキストを更新。
     * @param bytes メモリ使用量(バイト)
     * @param hasValidValue 有効な値かどうか
     */
    private _updateLabelText (bytes: number, hasValidValue: boolean) {
        if (!this._label) {
            return;
        }

        let text = this.prefixText;

        if (!hasValidValue || !this._memoryApiAvailable) {
            text += this._unavailableMessage || 'N/A';
            this._label.string = text;
            this._label.color = this.labelColorNormal;
            return;
        }

        if (this.showInBytes) {
            text += this._formatBytes(bytes, this.decimalPlaces);
        } else {
            text += bytes.toString() + ' bytes';
        }

        this._label.string = text;
    }

    /**
     * 閾値をチェックし、ラベル色とログを制御。
     */
    private _checkWarningThreshold (usedBytes: number) {
        if (!this._label) {
            return;
        }

        if (this.warningThresholdMB <= 0) {
            // 閾値チェックなし
            this._label.color = this.labelColorNormal;
            this._lastWarnedOverThreshold = false;
            return;
        }

        const usedMB = usedBytes / (1024 * 1024);
        if (usedMB >= this.warningThresholdMB) {
            this._label.color = this.labelColorWarning;
            if (!this._lastWarnedOverThreshold) {
                console.warn(`[MemoryMonitor] メモリ使用量が警告閾値を超えました: ${usedMB.toFixed(1)} MB (threshold: ${this.warningThresholdMB} MB)`);
                this._lastWarnedOverThreshold = true;
            }
        } else {
            this._label.color = this.labelColorNormal;
            this._lastWarnedOverThreshold = false;
        }
    }

    /**
     * バイト数を人間が読みやすい文字列にフォーマット。
     */
    private _formatBytes (bytes: number, decimals: number): string {
        if (bytes === 0) return '0 B';

        const k = 1024;
        const dm = decimals < 0 ? 0 : decimals;
        const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
        const i = Math.floor(Math.log(bytes) / Math.log(k));

        const value = parseFloat((bytes / Math.pow(k, i)).toFixed(dm));
        return `${value} ${sizes[i]}`;
    }
}

コードのポイント解説

  • onLoad
    • プロパティの値を安全な範囲に補正(updateIntervaldecimalPlaces)。
    • _ensureLabel() で Label を自動取得・生成し、スタイルと位置を適用。
    • _checkMemoryApiAvailability() で実行環境がメモリ API に対応しているか判定。
    • _updateUnavailableMessage() で取得不能時のメッセージを構築。
    • 初期テキストを _updateLabelText(0, true) で設定。
  • update(dt)
    • enabledMonitorfalse なら処理をスキップ。
    • updateInterval ごとにメモリ情報を取得。
    • 取得できた場合はバイト数をラベルに反映し、閾値チェック。
    • 取得できない場合は Unavailable 表示に切り替え。
  • _ensureLabel()
    • 自ノード → 子ノードと順に Label を探索。
    • 見つからなければ MemoryMonitorLabel という子ノードを作成し、Label を自動追加。
    • Label が取得できない場合は console.error を出して防御的に処理。
  • _getMemoryUsageBytes()
    • ブラウザ環境で performance.memory.usedJSHeapSize を取得。
    • 利用できない場合は null を返し、update 側で適切に処理。
  • _checkWarningThreshold()
    • MB に変換して warningThresholdMB と比較。
    • 閾値を超えたらラベル色を警告色にし、初回のみ console.warn を出力。

使用手順と動作確認

1. スクリプトファイルの作成

  1. Editor の Assets パネルで任意のフォルダ(例: assets/scripts/)を右クリックします。
  2. Create → TypeScript を選択し、ファイル名を MemoryMonitor.ts とします。
  3. 作成された MemoryMonitor.ts をダブルクリックして開き、既存コードをすべて削除して、上記のコードを貼り付けて保存します。

2. テスト用ノードの作成

MemoryMonitor はどのノードにもアタッチできますが、ここではシンプルにルート直下に 1 つノードを作ってテストします。

  1. Hierarchy パネルで、シーン(例: Canvas など)の下で右クリックし、Create → Empty Node を選択します。
  2. 作成されたノードの名前を MemoryMonitorNode など分かりやすい名前に変更します。

3. コンポーネントのアタッチ

  1. HierarchyMemoryMonitorNode を選択します。
  2. Inspector の下部にある Add Component ボタンをクリックします。
  3. Custom カテゴリ(または検索欄)から MemoryMonitor を選択して追加します。

この時点で、MemoryMonitorNode の子として MemoryMonitorLabel が自動で作成され、メモリ情報を表示する準備が整います。

4. プロパティの設定例

Inspector の MemoryMonitor コンポーネントに、以下のような値を設定してみてください。

  • Enabled Monitor: ON (true)
  • Update Interval: 0.5(0.5 秒ごとに更新)
  • Show In Bytes: ON (true)
  • Decimal Places: 1
  • Warning Threshold MB: 200(200MB を超えたら警告色とログ)
  • Label Color Normal: 白 (255,255,255,255)
  • Label Color Warning: 赤 (255,80,80,255)
  • Font Size: 20
  • Position: (0, 0, 0)(Canvas の左下などにしたい場合は座標を調整)
  • Anchor To UITransform: Canvas 配下の UI ノードに付ける場合は ON 推奨
  • Prefix Text: “Memory: “
  • Show Unavailable Reason: ON

5. 動作確認(エディタ内プレビュー)

  1. Editor 上部の Play ボタンを押してゲームを実行します。
  2. ゲーム画面内に Memory: 〇〇 MB のようなテキストが表示されていることを確認します。
  3. Scene 内でオブジェクトを増やしたり、別シーンに遷移するなどして負荷をかけると、数値が変動する様子が確認できます。
  4. 設定した Warning Threshold MB を超えると、ラベル色が警告色に変わり、コンソールに
    [MemoryMonitor] メモリ使用量が警告閾値を超えました: ...

    のようなログが出力されます。

6. メモリ情報が取得できない場合の挙動

  • ネイティブビルドや performance.memory に非対応なブラウザでは、ラベルには
    • 例: Memory: Unavailable (performance.memory not supported)

    のように表示されます。

  • コンソールには
    [MemoryMonitor] この環境ではメモリ情報 API が利用できません。表示は "N/A" になります。

    という警告が 1 回だけ出力されます。

  • Show Unavailable ReasonOFF にすると、値が取得できない場合は Memory: のみ(あるいは空文字)となり、理由テキストは表示されません。

まとめ

この MemoryMonitor コンポーネントは、

  • アタッチするだけでメモリ使用量の概算を可視化できる
  • UI ノード・通常ノードどちらにも対応し、ラベルノードを自動生成する
  • 閾値設定により、メモリリークの兆候を色とログで素早く検知できる
  • 外部の GameManager やシングルトンに依存せず、この 1 ファイルだけで完結する

といった特徴を持っています。

開発中はこのコンポーネントを常にシーンに置いておき、重いシーンへの遷移やリソースのロード/アンロード後にメモリ値の推移を観察することで、「どのシーンでメモリが増え続けているか」「解放処理が効いているか」などを素早く把握できます。

さらに応用として、

  • FPS や DrawCall 数など、他のパフォーマンス指標を併せて表示する HUD コンポーネントに統合する
  • 閾値を超えたときにスクリーンショットを撮る・ログファイルを出力するなど、デバッグ用途に特化した動作を追加する

といった拡張も容易です。

まずはこの MemoryMonitor をプロジェクトに組み込んで、日常的にメモリの挙動をチェックできる開発環境を整えてみてください。

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

URLをコピーしました!