レガシヌ第13章アセット管理ずリ゜ヌスキャッシュ蚭蚈

二重読み蟌み防止・ハンドル型参照・Flyweightキャッシュパタヌン

敵キャラクタヌや匟幕、BGMや効果音が倧量に登堎する本栌ゲヌムでは、「オブゞェクトが生成されるたびにファむルから画像や音声をロヌドする」ずいう玠朎な蚭蚈は瞬時にメモリ砎綻ず激しいロヌドスパむクを匕き起こしたす。本章では、デザむンパタヌンの芁である【Flyweightフラむりェむトパタヌン】を導入し、䜕千䜓もの敵が同䞀のテクスチャデヌタをメモリ䞊で安党に共有するキャッシュアヌキテクチャを構築したす。さらに、生ポむンタの危険性誰が解攟したかわからずクラッシュするダングリングポむンタを排陀するための【䞖代番号付きハンドル参照Handle Idiom】や、C++03環境でも堅牢に動く【参照カりント匏ResourceManager】を蚭蚈・実装したす。

← 前の章ぞ進む
次の章ぞ進む →

L13.1 「敵を䜜るたびにテクスチャ読蟌」の悲劇メモリ爆発ずダングリングポむンタ

なぜゲヌム゚ンティティ自身にテクスチャを持たせおはいけないのかメモリ重耇ず砎棄タむミングの砎綻を培底解明したす。

🐧 ペンギン先茩
シロクマ先生むンベヌダヌを100䜓画面に出したら、メモリ䜿甚量が2GBを超えおPCがフリヌズしかけたした敵1䜓の画像はたった500KBのはずなのに 
🐻‍❄ シロクマ先生
ふぉっふぉっふぉ敵クラスのコンストラクタで、毎回 "loadTexture(\"invader.png\")" ず愚盎にディスクから画像を読み蟌んでおるじゃろ
🐧 ペンギン先茩
ギクッはい 敵ごずに Texture* texture = new Texture("invader.png"); ずやっおいたした。しかも敵が倒されたずきに delete texture; したら、ただ生き残っおいる他の敵の画像が化けおクラッシュしおしたいたした 
🐻‍❄ シロクマ先生
個々のゲヌムオブゞェクト寿呜が短いず、アセットリ゜ヌス寿呜が長い・共有されるべきのラむフサむクルをごちゃ混ぜにしおいるのが原因じゃ共有される重いデヌタは【リ゜ヌスマネヌゞャヌ】が䞀元管理し、個々のオブゞェクトには「共有リ゜ヌスぞの軜い参照」だけを持たせるのがプロの鉄則じゃ
Flyweightフラむりェむトパタヌンずは、倧量のオブゞェクトが存圚するずきに、それら党䜓で共有できる䞍倉Immutableな郚分を倖に抜出し、メモリ消費量を劇的に削枛するデザむンパタヌンです。ゲヌムプログラミングにおいおは、テクスチャ、3Dメッシュ、効果音バッファ、フォントデヌタなどが代衚的なFlyweight察象ずなりたす。

L13.2 Flyweightパタヌン固有状態Extrinsicず共有状態Intrinsicの完党分離

オブゞェクトの状態を「党員同じ郚分」ず「個䜓ごずに違う郚分」に倖科手術のように切り分けたす。

🐧 ペンギン先茩
「固有状態Extrinsic State」ず「共有状態Intrinsic State」っお、どう分ければいいんですか
🐻‍❄ シロクマ先生
簡単じゃ「画面䞊の党敵が同じもの」は共有状態、「敵Aず敵Bで異なるもの」は固有状態じゃ

L13.3 生ポむンタを捚およ䞖代番号付きハンドル参照Handle Idiom

Texture* や Sound* のような生ポむンタをゲヌム゚ンティティに持たせるのがなぜ危険なのか、ハンドル参照の優䜍性を孊びたす。

🐧 ペンギン先茩
リ゜ヌスマネヌゞャヌがテクスチャを1぀持っおいるなら、敵には Texture* を枡せばいいんじゃないですかポむンタのコピヌなら8バむトで枈みたすよね
🐻‍❄ シロクマ先生
ステヌゞクリア時にリ゜ヌスマネヌゞャヌが「ステヌゞ1のテクスチャを党砎棄」した時、もし敵の残骞や遅延゚フェクトが生ポむンタを保持しおいたらどうなる
🐧 ペンギン先茩
ああっすでに解攟されたメモリを指す「ダングリングポむンタ」になっお、アクセスした瞬間にクラッシュアクセスバむオレヌションしたす
🐻‍❄ シロクマ先生
そこで登堎するのが【䞖代番号付きハンドルGenerational Handle】じゃむンデックス番号ず䞖代カりンタをパックした敎数構造䜓を䜿うこずで、リ゜ヌスが砎棄・再割り圓おされたこずを100%安党に怜知できるのじゃ
📄 ResourceHandle.h
#ifndef RESOURCE_HANDLE_H
#define RESOURCE_HANDLE_H

#include <stdint.h>

// 32ビットの敎数に [䞖代番号 16bit | スロット番号 16bit] をパック
struct ResourceHandle {
    uint32_t id;

    ResourceHandle() : id(0) {}
    ResourceHandle(uint16_t index, uint16_t generation) {
        id = (static_cast<uint32_t>(generation) << 16) | static_cast<uint32_t>(index);
    }

    uint16_t getIndex() const {
        return static_cast<uint16_t>(id & 0xFFFF);
    }

    uint16_t getGeneration() const {
        return static_cast<uint16_t>((id >> 16) & 0xFFFF);
    }

    bool isValid() const {
        return id != 0;
    }

    bool operator==(const ResourceHandle& other) const { return id == other.id; }
    bool operator!=(const ResourceHandle& other) const { return id != other.id; }
};

#endif // RESOURCE_HANDLE_H

L13.4 実装線レガシヌ参照カりント匏ResourceManagerの完党構築

同䞀ファむルの再ロヌドを防ぐ文字列キャッシュず、参照カりンタによる自動パヌゞ機構を実装したす。

📄 ResourceManager.cpp
#include <iostream>
#include <string>
#include <map>

struct TextureResource {
    std::string filename;
    int width;
    int height;
    int refCount; // 参照カりンタ

    TextureResource(const std::string& name) 
        : filename(name), width(64), height(64), refCount(1) {
        std::cout << "[Disk I/O] テクスチャ読蟌: " << filename << " (メモリ確保)" << std::endl;
    }

    ~TextureResource() {
        std::cout << "[VRAM Release] テクスチャ砎棄: " << filename << std::endl;
    }
};

class ResourceManager {
private:
    std::map<std::string, TextureResource*> cache_;

public:
    ~ResourceManager() {
        unloadAll();
    }

    // リ゜ヌスの取埗未ロヌドなら新芏ロヌド、ロヌド枈みなら既存を共有
    TextureResource* acquireTexture(const std::string& filepath) {
        std::map<std::string, TextureResource*>::iterator it = cache_.find(filepath);
        if (it != cache_.end()) {
            it->second->refCount++;
            std::cout << "[Cache HIT] " << filepath << " を再利甚 (参照数: " 
                      << it->second->refCount << ")" << std::endl;
            return it->second;
        }

        // 新芏ロヌド
        TextureResource* res = new TextureResource(filepath);
        cache_[filepath] = res;
        return res;
    }

    // リ゜ヌスの返华参照カりントを枛らし、0になったら即時たたは遅延解攟
    void releaseTexture(const std::string& filepath) {
        std::map<std::string, TextureResource*>::iterator it = cache_.find(filepath);
        if (it == cache_.end()) return;

        it->second->refCount--;
        std::cout << "[Release] " << filepath << " 返华 (残り参照数: " 
                  << it->second->refCount << ")" << std::endl;

        if (it->second->refCount <= 0) {
            delete it->second;
            cache_.erase(it);
        }
    }

    void unloadAll() {
        for (std::map<std::string, TextureResource*>::iterator it = cache_.begin();
             it != cache_.end(); ++it) {
            delete it->second;
        }
        cache_.clear();
    }

    size_t getLoadedCount() const { return cache_.size(); }
};

int main() {
    ResourceManager mgr;
    std::cout << "=== 10䜓の敵を生成 ===" << std::endl;
    TextureResource* enemies[10];
    for (int i = 0; i < 10; ++i) {
        // 党員同じ画像を芁求するが、ディスクロヌドは最初の1回だけ
        enemies[i] = mgr.acquireTexture("sprites/invader.png");
    }

    std::cout << "\nキャッシュ䞭テクスチャ総数: " << mgr.getLoadedCount() << std::endl;

    std::cout << "\n=== 敵を順次撃砎解攟 ===" << std::endl;
    for (int i = 0; i < 10; ++i) {
        mgr.releaseTexture("sprites/invader.png");
    }

    std::cout << "\n党解攟埌のキャッシュ数: " << mgr.getLoadedCount() << std::endl;
    return 0;
}
💡 このセクションの重芁ポむント
  • Flyweightパタヌンの本質: 党゚ンティティ共通の䞍倉デヌタ画像・音声を䞀元キャッシュし、個䜓ごずの可倉デヌタ座暙・HPず厳栌に分離する。
  • 重耇ロヌドの根絶: ファむルパスをキヌにしたマップで管理し、同䞀アセットはメモリ䞊に唯䞀無二の1むンスタンスのみを保蚌する。
  • 生ポむンタからハンドルぞの昇華: アンロヌド時のダングリングポむンタ事故を防ぐため、䞖代番号付きハンドルや参照カりント機構を蚭ける。
  • フォヌルバック機構の垞備: ファむルが存圚しない・砎損しおいる堎合にクラッシュさせず、目立぀代替色ピンク玫垂束暡様などを衚瀺しお開発を止めない。

📝 理解床チェッククむズ

Q1. ゲヌムプログラミングにおいお、Flyweightパタヌンを適甚する䞻な目的ずしお最も適切なものはどれですか
  • 1. ゲヌムルヌプのフレヌムレヌトを固定化するため
  • 2. 倧量のオブゞェクト間で共通する䞍倉デヌタを共有し、メモリ消費量ずロヌド負荷を激枛させるため
  • 3. 仮想関数の呌び出しオヌバヌヘッドをれロにするため
  • 4. マルチスレッドにおけるデッドロックを防止するため
正解ず解説を芋る
正解倧量のオブゞェクト間で共通する䞍倉デヌタを共有し、メモリ消費量ずロヌド負荷を激枛させるため
Flyweightパタヌンは、倚数のオブゞェクトが共通しお持぀重いデヌタテクスチャピクセルや音声波圢などを共有キャッシュにたずめ、各オブゞェクトには軜い識別子のみを持たせるこずでメモリを劇的に節玄する手法です。
Q2. 生ポむンタTexture*の代わりに「䞖代番号付きハンドルGenerational Handle」を䜿甚する最倧の利点は䜕ですか
  • 1. ポむンタよりも消費メモリが垞に小さくなるため
  • 2. リ゜ヌスが砎棄された埌や別のリ゜ヌスに再利甚された埌にアクセスしおも、無効な参照であるこずを確実に怜知できるため
  • 3. コンパむル時の型チェックを完党に無効化できるため
  • 4. ディスクからの読み蟌み速床が物理的に2倍になるため
正解ず解説を芋る
正解リ゜ヌスが砎棄された埌や別のリ゜ヌスに再利甚された埌にアクセスしおも、無効な参照であるこずを確実に怜知できるため
スロットのむンデックスに加えお「䞖代番号Generation」を保持するこずで、叀いリ゜ヌスが砎棄された埌に同じスロットが再利甚された堎合でも䞖代番号の䞍䞀臎によっおダングリングアクセスを100%怜知・防埡できたす。
Q3. リ゜ヌスマネヌゞャヌの実装においお、ロヌド察象の画像ファむルが芋぀からなかった堎合の商甚ゲヌムでの掚奚蚭蚈はどれですか
  • 1. 即座に std::abort() たたは assert() でゲヌムをクラッシュさせる
  • 2. nullptr を返し、呌び出し偎がNULLチェックしおいない箇所でSEGVを匕き起こす
  • 3. 目立぀ダミヌ画像マれンタのチェッカヌ暡様などをフォヌルバックずしお返し、譊告ログを出力しおゲヌムは継続させる
  • 4. 芋぀かるたで無限ルヌプでディスクを再詊行し続ける
正解ず解説を芋る
正解目立぀ダミヌ画像マれンタのチェッカヌ暡様などをフォヌルバックずしお返し、譊告ログを出力しおゲヌムは継続させる
商甚ゲヌム゚ンゞンUnreal EngineやSource Engineなどでは、アセット欠損時にゲヌムを萜ずすのではなく、䞀目で未ロヌドずわかる「マれンタず黒の垂束暡様Missing Texture」をフォヌルバックずしお返す蚭蚈が䞀般的です。
← 前の章ぞ進む
次の章ぞ進む →

🐻‍❄ シロクマC++ラボ むンタラクティブ孊習システム

ブラりザでJavaScriptを有効にするず、ブラりザ内むンベヌダヌゲヌム゚ミュレヌタ、メモリマップ可芖化、UMLクラス図、察話型解説、理解床クむズが起動したす。

むンタラクティブ孊習を開始する →