Unityを触り始めた頃は、つい Update() の中に「入力処理」「移動」「エフェクト」「UI更新」「リソース管理」など、ありとあらゆる処理を書いてしまいがちですよね。
動いているうちは問題ありませんが、プロジェクトが少し大きくなると、次のような問題が出てきます。

  • どこで何が起きているのか追いづらく、バグ調査がつらい
  • メモリリークやGC(ガベージコレクション)の原因が特定しづらい
  • シーンごと・プレハブごとに必要な処理を切り替えるのが面倒

とくに「メモリの増え方が怪しいけど、どこで増えているのか分からない」という悩みは、脱初心者の壁としてよくぶつかるポイントです。
そこでこの記事では、RAM使用量を常に監視して画面に表示する専用コンポーネントを作って、メモリ状況を簡単に可視化できるようにしてみましょう。

コンポーネント名は MemoryMonitor
役割は「メモリ監視と表示」だけに絞り、他の処理とはきれいに分離しておきます。

【Unity】メモリの増減をその場でチェック!「MemoryMonitor」コンポーネント

ここでは、以下のようなことができる MemoryMonitor を実装します。

  • 一定間隔で現在のメモリ使用量を取得
  • ゲーム画面の隅に「現在のRAM使用量」「ピーク値」「変化量」などを表示
  • しきい値を超えたときに色を変えて「怪しい増え方」を目視で確認

Unityエディタだけでなく、ビルド後の実行環境でも動くように、System.Diagnostics.Process を使ってプロセスのワーキングセット(実メモリ使用量)を取得する方法を採用しています。

フルコード:MemoryMonitor.cs


using System;
using System.Diagnostics;
using System.Text;
using UnityEngine;

/// <summary>
/// 実行中のプロセスが使用している物理メモリ量を監視して、画面に表示するコンポーネント。
/// - Update() ではなく、一定間隔でサンプリングして負荷を抑える
/// - メモリ使用量の現在値・ピーク値・前回からの増減を表示
/// - しきい値を超えたら色を変えて「怪しい増え方」を視覚的に検知
/// </summary>
public class MemoryMonitor : MonoBehaviour
{
    // ==== 表示位置・スタイル関連 ====

    [Header("表示設定")]
    [SerializeField]
    private bool _enableDisplay = true; // 画面表示を有効 / 無効

    [SerializeField]
    private int _fontSize = 14; // OnGUI のフォントサイズ

    [SerializeField]
    private Vector2 _offset = new Vector2(10f, 10f); // 画面左上からのオフセット

    [SerializeField]
    private Color _normalColor = Color.white; // 通常時の文字色

    [SerializeField]
    private Color _warningColor = Color.yellow; // 警告しきい値を超えたときの文字色

    [SerializeField]
    private Color _criticalColor = Color.red; // クリティカルしきい値を超えたときの文字色

    [Header("監視設定")]
    [SerializeField]
    [Tooltip("メモリ使用量をサンプリングする間隔(秒)")]
    private float _sampleIntervalSeconds = 0.5f;

    [SerializeField]
    [Tooltip("警告色に切り替えるしきい値(MB)")]
    private float _warningThresholdMB = 1500f;

    [SerializeField]
    [Tooltip("クリティカル色に切り替えるしきい値(MB)")]
    private float _criticalThresholdMB = 2000f;

    [SerializeField]
    [Tooltip("ログに書き出すかどうか(一定間隔で Debug.Log 出力)")]
    private bool _enableLogging = false;

    [SerializeField]
    [Tooltip("ログを書き出す間隔(秒)")]
    private float _logIntervalSeconds = 5f;

    // ==== 内部状態 ====

    private float _currentMemoryMB;  // 現在のメモリ使用量(MB)
    private float _peakMemoryMB;     // 実行中のピークメモリ使用量(MB)
    private float _previousMemoryMB; // 1つ前のサンプルのメモリ使用量(MB)

    private float _nextSampleTime;   // 次にサンプルを取る Time.time
    private float _nextLogTime;      // 次にログを出す Time.time

    private GUIStyle _guiStyle;      // OnGUI 用スタイル
    private StringBuilder _stringBuilder; // 文字列の生成コスト削減用

    private Process _currentProcess; // 自身のプロセス情報

    private void Awake()
    {
        // プロセス情報を取得
        _currentProcess = Process.GetCurrentProcess();

        // StringBuilder を使い回して GC 負荷を下げる
        _stringBuilder = new StringBuilder(256);

        // GUIStyle を一度だけ作って使い回す
        _guiStyle = new GUIStyle
        {
            fontSize = _fontSize,
            normal = { textColor = _normalColor }
        };

        // 初期サンプルを取得
        SampleMemoryUsage(force: true);

        _nextSampleTime = Time.time + _sampleIntervalSeconds;
        _nextLogTime = Time.time + _logIntervalSeconds;
    }

    private void Update()
    {
        // 一定間隔でメモリをサンプリング
        if (Time.time >= _nextSampleTime)
        {
            SampleMemoryUsage();
            _nextSampleTime = Time.time + _sampleIntervalSeconds;
        }

        // 必要ならログ出力
        if (_enableLogging && Time.time >= _nextLogTime)
        {
            LogMemoryUsage();
            _nextLogTime = Time.time + _logIntervalSeconds;
        }
    }

    /// <summary>
    /// メモリ使用量をサンプリングして内部状態を更新する。
    /// </summary>
    /// <param name="force">true の場合、前回値に関わらず即座に更新する</param>
    private void SampleMemoryUsage(bool force = false)
    {
        if (_currentProcess == null)
        {
            // 万が一取得に失敗した場合はリトライ
            _currentProcess = Process.GetCurrentProcess();
            if (_currentProcess == null)
            {
                return;
            }
        }

        // WorkingSet64 は現在の物理メモリ使用量(バイト)
        long bytes = _currentProcess.WorkingSet64;

        float mb = bytes / (1024f * 1024f);

        if (!force)
        {
            _previousMemoryMB = _currentMemoryMB;
        }

        _currentMemoryMB = mb;

        // ピーク値更新
        if (_currentMemoryMB > _peakMemoryMB)
        {
            _peakMemoryMB = _currentMemoryMB;
        }
    }

    /// <summary>
    /// 現在のメモリ使用状況をログに出力する。
    /// </summary>
    private void LogMemoryUsage()
    {
        float delta = _currentMemoryMB - _previousMemoryMB;

        string sign = delta >= 0 ? "+" : "-";
        float absDelta = Mathf.Abs(delta);

        Debug.Log(
            $"[MemoryMonitor] Current: {_currentMemoryMB:F1} MB, " +
            $"Peak: {_peakMemoryMB:F1} MB, " +
            $"Delta: {sign}{absDelta:F1} MB"
        );
    }

    private void OnGUI()
    {
        if (!_enableDisplay)
        {
            return;
        }

        // フォントサイズがインスペクタで変更された場合に追従
        if (_guiStyle.fontSize != _fontSize)
        {
            _guiStyle.fontSize = _fontSize;
        }

        // しきい値に応じて文字色を変更
        Color colorToUse = _normalColor;
        if (_currentMemoryMB >= _criticalThresholdMB)
        {
            colorToUse = _criticalColor;
        }
        else if (_currentMemoryMB >= _warningThresholdMB)
        {
            colorToUse = _warningColor;
        }

        _guiStyle.normal.textColor = colorToUse;

        // 表示用文字列を組み立て(StringBuilder で GC 削減)
        _stringBuilder.Length = 0;

        float delta = _currentMemoryMB - _previousMemoryMB;
        string sign = delta >= 0 ? "+" : "-";
        float absDelta = Mathf.Abs(delta);

        _stringBuilder
            .Append("Memory Monitor\n")
            .AppendFormat("Current : {0:F1} MB\n", _currentMemoryMB)
            .AppendFormat("Peak    : {0:F1} MB\n", _peakMemoryMB)
            .AppendFormat("Delta   : {0}{1:F1} MB\n", sign, absDelta)
            .AppendFormat("Warning : {0:F0} MB\n", _warningThresholdMB)
            .AppendFormat("Critical: {0:F0} MB", _criticalThresholdMB);

        string text = _stringBuilder.ToString();

        // 画面左上基準で描画
        float x = _offset.x;
        float y = _offset.y;

        // テキストのサイズを自動計測して背景用の矩形を作る
        Vector2 size = _guiStyle.CalcSize(new GUIContent(text));
        Rect rect = new Rect(x, y, size.x + 8f, size.y + 8f);

        // 半透明の背景
        Color prevColor = GUI.color;
        GUI.color = new Color(0f, 0f, 0f, 0.6f);
        GUI.Box(rect, GUIContent.none);
        GUI.color = prevColor;

        // テキスト本体
        Rect labelRect = new Rect(x + 4f, y + 4f, size.x, size.y);
        GUI.Label(labelRect, text, _guiStyle);
    }

    /// <summary>
    /// 外部から現在のメモリ情報を取得したい場合のアクセサ。
    /// 例えば他のデバッグUIコンポーネントから参照することを想定。
    /// </summary>
    public (float currentMB, float peakMB, float deltaMB) GetMemoryInfo()
    {
        float delta = _currentMemoryMB - _previousMemoryMB;
        return (_currentMemoryMB, _peakMemoryMB, delta);
    }
}

使い方の手順

ここからは、実際に MemoryMonitor コンポーネントをプロジェクトに組み込む手順を見ていきましょう。例として「プレイヤーが弾を撃ちまくるシューティングゲーム」や「敵を大量にスポーンするタワーディフェンス」を想定すると分かりやすいです。

手順① スクリプトを用意する

  1. Unity の Project ウィンドウで、Scripts フォルダ(なければ作成)を選択。
  2. 右クリック → Create → C# Script を選び、名前を MemoryMonitor にする。
  3. 自動生成された中身をすべて削除し、上記のフルコードをそのままコピペして保存。

手順② シーンに監視用オブジェクトを追加する

  1. Hierarchy で右クリック → Create Empty を選択し、名前を MemoryMonitor に変更。
  2. その GameObject に MemoryMonitor コンポーネントをアタッチ。
  3. インスペクタで、必要に応じて以下を調整します。
    • Sample Interval Seconds:0.5~1.0 秒程度がおすすめ
    • Warning / Critical Threshold MB:ターゲットプラットフォームに合わせて設定(例:PC なら Warning 1500, Critical 2000)
    • Enable Logging:メモリ推移をコンソールで追いたい場合は ON

手順③ 実行してメモリの増え方を観察する

ゲームを再生すると、画面左上に以下のような情報が表示されます。

  • Current:現在の RAM 使用量(MB)
  • Peak:これまでの最大値
  • Delta:前回サンプルからの増減
  • Warning / Critical:しきい値の設定値

例えば、次のようなケースで使うと効果的です。

  • プレイヤーの弾やエフェクトを連射しまくる:撃ち続けても Current / Peak が安定しているか確認
  • 敵を大量にスポーンして倒す:敵の生成・破棄でメモリが増え続けていないかチェック
  • シーン遷移を何度も繰り返す:遷移ごとに Peak がじわじわ上がり続けていないか観察

もし Delta が常にプラスで増え続ける、あるいは シーン遷移ごとに Peak がどんどん更新される ようであれば、何かが解放されていない可能性があります。
そういうときに、まずは MemoryMonitor を眺めながら、どの操作で増えるのかを切り分けていくと調査がかなり楽になります。

手順④ ビルド後の挙動もチェックする

エディタ上とビルド後では、メモリの使われ方が変わることがあります。
PC ビルド(Windows / macOS)を作成して実行し、同じように MemoryMonitor の表示を見ながら挙動を確認してみましょう。

  • エディタでは問題ないのに、ビルド後だけメモリが増え続ける
  • 特定のシーンだけ、異常に Critical しきい値を超えてしまう

といった違いが見えたら、そのシーンや機能周辺のリソース管理を重点的に見直すきっかけになります。

メリットと応用

MemoryMonitor のような「専用の監視コンポーネント」を用意しておくと、次のようなメリットがあります。

  • ゲームロジックとデバッグロジックが分離できる
    プレイヤーや敵のスクリプトに「メモリ監視コード」を書き込まず、いつでも付け外し可能な独立コンポーネントとして扱えます。
  • プレハブやシーンをまたいだ共通ツールとして使える
    監視用 GameObject を 1 つ用意しておけば、どのシーンでも同じ表示・同じしきい値で確認できます。
  • レベルデザイン時の「重さ」をすぐ確認できる
    敵の最大同時出現数やエフェクト量を調整しながら、メモリがどこまで増えるかをリアルタイムで見られるので、チューニングの指標になります。
  • GC(ガベージコレクション)発生の前兆をつかみやすい
    メモリ使用量が一定以上に膨らんでから一気に下がるようなパターンが見えたら、「ここで GC が走っているな」といった推測もしやすくなります。

特に「全部をひとつの巨大スクリプトに押し込まない」という方針で開発していると、
「ゲーム本体」コンポーネントはゲームロジックだけに集中し、「監視」や「デバッグ」は専用コンポーネントに分割する という構成がとても相性が良いです。

改造案:一定以上増えたときに自動で GC.Collect() を試す

あくまでデバッグ用途に限った話になりますが、「メモリが急増したときに一度 GC を試してみたい」というケースもあります。
以下は、MemoryMonitor に追加できる簡単な改造例です(本番ゲームでは安易に GC を呼ばないように注意してください)。


    /// <summary>
    /// メモリが指定量以上増えたら、一度 GC.Collect() を呼んでみるデバッグ用ヘルパー。
    /// 実運用ではパフォーマンス悪化の原因になる可能性があるため、開発中のみ使用推奨。
    /// </summary>
    /// <param name="deltaThresholdMB">前回サンプルからの増加量しきい値(MB)</param>
    public void CollectGCIfDeltaExceeded(float deltaThresholdMB)
    {
        float delta = _currentMemoryMB - _previousMemoryMB;
        if (delta >= deltaThresholdMB)
        {
            Debug.Log($"[MemoryMonitor] Delta {delta:F1} MB exceeded threshold {deltaThresholdMB:F1} MB. Calling GC.Collect().");
            GC.Collect();
        }
    }

例えば、Update() か別のデバッグコンポーネントから


memoryMonitor.CollectGCIfDeltaExceeded(200f);

のように呼べば、「前回より 200MB 以上増えたときだけ GC を試す」といった挙動を追加できます。
もちろん、これはあくまで「挙動の観察用」であり、本番環境での常用はおすすめしません。

このように、メモリ監視をひとつの小さなコンポーネントとして切り出しておくと、
必要に応じてログ機能を足したり、他のデバッグ UI と連携させたりといった拡張がしやすくなります。
ぜひ自分のプロジェクト用にカスタマイズして、快適なメモリ監視環境を作ってみてください。