スポンサーリンク
UnityUnityメモ

Unityでメモリリークを防ぐ!C#のDisposeパターンとイベント管理の極意

Unity

はじめに

Unityでゲームを作っていると、いつのまにか動作が重くなったり、シーンを何度も切り替えるうちにメモリがどんどん膨らんでしまうことってありませんか?
その原因のひとつがメモリリークです。メモリリークは気づかないうちにゲーム全体のパフォーマンスを落とし、最悪の場合はアプリがクラッシュしてしまうこともあります。

特にC#で実装しているときに忘れがちなのがDisposeパターンイベントの解除処理。これを正しく行わないと、リソースやオブジェクトが解放されずに残ってしまうんです。
「Destroyしたのに残ってる!?」なんて現象に悩んだ経験がある方も多いはず。

この記事では、C#のDisposeパターンを中心に、Unityでのメモリリークを防ぐための考え方や実践的なテクニックをまとめました。
また、オブジェクトプールイベント管理ツールなど、Asset Storeで使える便利なアセットもご紹介します。
初心者から中級・上級の方まで「安定したゲーム開発の土台作り」に役立つ内容になっていますので、ぜひ最後まで読んでみてくださいね✨




C#におけるDisposeの正しい実装

なぜDisposeが必要なの?

C#ではガベージコレクション(GC)が自動でメモリ管理をしてくれますが、万能ではありません。
特にファイルストリームデータベース接続など、OSや外部ライブラリが管理しているリソースはGCでは解放されません。
そのため、明示的にリソースを解放する仕組みとしてIDisposableインターフェースが用意されているんです。

Disposeを正しく実装しておけば、不要なリソースを早めに解放でき、メモリリークやリソース不足を防げます。
逆にこれを怠ると「使ってないはずのリソースが残り続ける…」という不具合につながります。

Disposeメソッドが満たすべき条件

公式ドキュメントや実務経験から、Disposeは次の条件を守ることが推奨されています。

  • 複数回呼ばれても安全に動作すること
  • 保持しているリソースを正しく解放すること
  • 基底クラスのDisposeを呼ぶこと(継承対応)
  • GC.SuppressFinalize(this)でファイナライザの実行を抑制すること
  • 重大な例外を除き、例外をスローしないこと

アンマネージドリソースを持つ場合(Disposeパターン)

アンマネージドリソース(例えば IntPtr など)を持つ場合、単にDisposeを呼ぶだけでは不十分です。
ガベージコレクタが走る前にリソースを解放するため、Disposeパターンを実装するのがベストです。


public class MyClass : IDisposable
{
    private bool _disposed = false;
    private IntPtr _handle; // アンマネージドリソース
    private Stream _stream; // マネージドリソース

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (_disposed) return;

        if (disposing)
        {
            // マネージドリソースを解放
            _stream?.Dispose();
        }

        // アンマネージドリソースを解放
        if (_handle != IntPtr.Zero)
        {
            // 解放処理
            _handle = IntPtr.Zero;
        }

        _disposed = true;
    }

    ~MyClass()
    {
        Dispose(false);
    }
}

この実装では、マネージド/アンマネージドの両方を正しく解放できます。
さらにSafeHandleを使えばより安全に扱えるため、できる限りそちらの利用も検討すると良いですよ。

マネージドリソースだけを持つ場合

もしアンマネージドリソースを扱わないなら、実装はもっとシンプルになります。
ファイナライザ(デストラクタ)は不要で、Dispose() メソッドだけ実装すればOKです。


public class MyManagedClass : IDisposable
{
    private bool _disposed = false;
    private Stream _stream;

    public void Dispose()
    {
        if (_disposed) return;

        _stream?.Dispose();
        _disposed = true;
    }
}

このように「必要に応じてDisposeの複雑さを使い分ける」のがポイントです。
ゲームの規模が大きくなるほどリソース管理はシビアになるので、今のうちに習慣づけておきましょう😊

おすすめ学習書籍

DisposeパターンやC#のベストプラクティスをもっと深く知りたい方には、こちらの本がとても役立ちます。
実際の現場で「なるほど!」となる知識が詰まっています。


Unityにおけるメモリリークとその対策

Leaked Managed Shell (LMS)とは?

Unityでありがちなメモリリークのひとつに、Leaked Managed Shell (LMS)と呼ばれる現象があります。
これは、ゲームオブジェクトをDestroy()しても内部で参照が残り続け、「ゴミ」が溜まっていくようにメモリを圧迫する問題です。
一見すると小さなリークでも、テクスチャやバイト配列など大きなデータを抱えていると深刻なパフォーマンス低下につながります。

特にUnityEngine.Objectを継承した型を持つスクリプトや、プレハブ階層に含まれたオブジェクトの破棄処理で発生しやすいので注意が必要です。

ゲーム設計レベルでの対策

  • オブジェクトプールを使う:敵やアイテムを生成・破棄するたびにGCを走らせるのではなく、プールして再利用するのが鉄則です。
  • シーンのリロード:特定のタイミングで同じシーンを再度読み込み、残りカスも含めてメモリを一掃する方法も有効です。
  • DontDestroyOnLoadの活用:BGMやゲーム全体で共有するオブジェクトはシーンをまたいで維持し、不要な再生成を避けましょう。

実装にあたっては、自前でプール処理を作るよりアセットストアのツールを導入する方が効率的です。
おすすめは以下のアセットです👇

ヘルパークラスやツールでの対策

さらに便利なのが、オブジェクト破棄やリーク検出を支援するヘルパークラス・エディター拡張を使う方法です。
例えば「ManagedShell」を利用すれば、オブジェクトを破棄するときに名前変更や非アクティブ化、タグ・レイヤーのリセットまで自動で処理してくれるため、テスト環境でも安心です。

また「LeakedManagedShellDetector」のような検出ツールを使えば、
DisposeやOnDestroyを実装していないクラスをリストアップしてくれるので、メモリリークの温床を事前に把握できます。

シーンごとのリセット戦略

エンドレスラン系のゲームやサバイバルゲームのように長時間動作するシーンでは、
一定のタイミングでシーンをリロードして「メモリのクリーンアップ」を図るのも現実的な戦略です。

ただし、スコアやプレイヤーの進行状況はリセットされてしまうため、データの受け渡し処理も合わせて実装しておきましょう。




イベント管理のベストプラクティス

「解除し忘れ」が一番のリーク要因

C#のevent(デリゲート)やUnityEventにリスナー登録したまま解除を忘れると、
参照が残りつづけてGCで回収されない=メモリリークの原因になります。
特にstaticイベント長寿命シングルトンに短命なオブジェクト(敵・弾・UIなど)が購読したままだと危険度MAXです。

基本ルール(これだけは守る)

  • 短命オブジェクト:OnEnableで購読 → OnDisableで解除
  • 長命クラス(非MonoBehaviour・サービス層):IDisposableで解除
  • staticイベントに登録したら、必ず「同じスコープ」で確実に解除
  • UnityEventはAddListener/RemoveListenerを対にする

安全な購読/解除(C# event版)


// 発行側(Publisher)
public static class GameSignals
{
    public static event Action PlayerDied;

    public static void RaisePlayerDied() => PlayerDied?.Invoke();
}

// 受信側(MonoBehaviour・短命オブジェクト)
public class PlayerHud : MonoBehaviour
{
    void OnEnable()
    {
        GameSignals.PlayerDied += OnPlayerDied;
    }

    void OnDisable()
    {
        GameSignals.PlayerDied -= OnPlayerDied; // ← 必ず対にする
    }

    void OnPlayerDied()
    {
        // 表示更新など
    }
}

UnityEventの正しい使い方


using UnityEngine;
using UnityEngine.Events;

public class Health : MonoBehaviour
{
    public UnityEvent OnDied;

    public void Kill()
    {
        OnDied?.Invoke();
    }
}

public class DeathEffect : MonoBehaviour
{
    [SerializeField] private Health health;

    void OnEnable()
    {
        health.OnDied.AddListener(PlayEffect);
    }

    void OnDisable()
    {
        health.OnDied.RemoveListener(PlayEffect); // ← Addと必ずワンセット
    }

    void PlayEffect()
    {
        // エフェクト再生
    }
}

IDisposableでイベント解除を「自動化」する

非MonoBehaviourのサービスやマネージャーのように明確に寿命を管理できるクラスは、IDisposableでイベント解除まで面倒を見ましょう。


public sealed class ScoreService : IDisposable
{
    private bool _disposed;

    public ScoreService()
    {
        GameSignals.PlayerDied += OnPlayerDied;
    }

    public void Dispose()
    {
        if (_disposed) return;
        GameSignals.PlayerDied -= OnPlayerDied; // ← 解除を確実に
        _disposed = true;
    }

    private void OnPlayerDied() { /* 集計処理など */ }
}

解除忘れをさらに防ぐ小技(サブスクをIDisposable化)

購読時に「解除を返す」ユーティリティを作ると、usingDispose()で確実に解除できます。


public static class EventSub
{
    public static IDisposable Subscribe(ref Action ev, Action handler)
    {
        ev += handler;
        return new Disposer(() => ev -= handler);
    }

    private sealed class Disposer : IDisposable
    {
        private Action _dispose; public Disposer(Action d) => _dispose = d;
        public void Dispose() { _dispose?.Invoke(); _dispose = null; }
    }
}

// 使い方(非MB)
public sealed class EnemyCounter : IDisposable
{
    private readonly IDisposable _sub;
    public EnemyCounter()
    {
        _sub = EventSub.Subscribe(ref GameSignals.PlayerDied, OnPlayerDied);
    }
    public void Dispose() => _sub.Dispose(); // ← これで解除OK
    void OnPlayerDied(){ /* ... */ }
}

設計メモ:誰が解除するの?早見ルール

  • UI/敵/弾など短命:MonoBehaviourのOnEnable/OnDisable
  • シーン跨ぎのマネージャ:Dispose() or OnDestroy()(シーン切替時に破棄されるならOnDestroyでもOK)
  • static発行源:購読先の寿命に合わせて購読側が必ず解除

イベント設計をシンプルにしたいなら

プロジェクトが大きくなると、イベントの出入り口管理が複雑になりがち。
そんなときはイベントハブ系アセットで「発行・購読の見える化/型安全化」を図ると安心です👇

デバッグ時のチェックポイント

  • 同じハンドラを二重登録していないか(ログ仕込むと発見しやすい)
  • OnDisableで必ず解除されているか(ブレークポイント/ログ)
  • staticイベントに短命オブジェクトがぶら下がっていないか
  • UnityEventのRemoveListener忘れがないか



メモリリーク検出・デバッグ支援ツール

Unity公式「Memory Profiler」

Unityには公式のMemory Profilerパッケージがあり、ヒープメモリの使用状況やオブジェクトの参照関係を分析できます。
メモリスナップショットを撮って比較できるので、「どのタイミングでどんなオブジェクトが残り続けているか」を確認するのに役立ちます。

Profiler Memory+ でさらに強化

公式Profilerだけでも十分ですが、さらに便利なのがProfiler Memory+というアセットです。
このツールを使えば、メモリ使用状況をリアルタイムで細かく追跡でき、リークの温床を素早く発見できます。
特にモバイル向けゲームやVR/ARのように限られたメモリ環境で動かす場合は、かなりの安心材料になります。

チェック時のコツ

  • シーン切替ごとにスナップショットを取って比較する
  • Destroyしたはずのオブジェクトが残っていないか確認する
  • イベント解除忘れや、Dispose未実装のクラスがないか疑う
  • GCの発生頻度が極端に多くないかを監視する

「Disposeを実装しているのに解放されてない…?」というときは、こうしたツールを使って参照チェーンを辿るのが解決への近道です。
コードの見直しだけでなく、ツールを活用して「見える化」するのがポイントですね。




まとめ

今回はC#のDisposeパターンUnityでのイベント管理・メモリリーク対策について解説しました。
押さえておきたいポイントを振り返ると──

  • Disposeはリソース解放の基本。アンマネージドリソースを扱う場合はDisposeパターンを実装する
  • イベントは登録と解除を必ずセットで管理。OnEnable/OnDisable、Disposeをうまく活用する
  • Unity特有のLMS(Leaked Managed Shell)はDestroyだけで油断しないこと
  • オブジェクトプールProfilerツールを活用して、無駄な生成・メモリ使用を防ぐ

メモリ管理は一見難しそうに見えますが、習慣づけることで自然に身についていきます。
ゲームを長時間遊んでも安定して動くようになると、プレイヤーの満足度もグッと上がりますよ✨


あわせて読みたい

メモリ管理やコードの設計に関心がある方におすすめの記事をまとめました👇


よくある質問(FAQ)

Q
Disposeを呼び忘れるとどうなりますか?
A

Disposeを呼ばないと、ファイルハンドルやソケット、データベース接続などが解放されずに残り続けます。
小規模なプロジェクトならすぐには問題が見えなくても、長時間のプレイや複雑なシーンではメモリリークや処理落ちにつながります。

Q
using構文を使えばDisposeを意識しなくても大丈夫?
A

using構文を使えば、ブロックを抜けるタイミングでDisposeが自動的に呼ばれるため便利です。
ただし、アンマネージドリソースを扱うクラスでは正しいDisposeパターンを実装する必要があります。
「usingを使えば絶対安全」というわけではないので注意してください。

Q
Unityのガベージコレクション(GC)に任せてもいいのでは?
A

GCはマネージドリソース(C#のオブジェクト)を自動で解放してくれますが、
アンマネージドリソースイベント登録の解除忘れまでは対応できません。
そのため、GC任せにするのではなく、Disposeや明示的な解除処理を組み合わせることが必須です。

※当サイトはアフィリエイト広告を利用しています。リンクを経由して商品を購入された場合、当サイトに報酬が発生することがあります。

※本記事に記載しているAmazon商品情報(価格、在庫状況、割引、配送条件など)は、執筆時点のAmazon.co.jp上の情報に基づいています。
最新の価格・在庫・配送条件などの詳細は、Amazonの商品ページをご確認ください。

スポンサーリンク