Top > プログラミング関連 > ファイルの重複ロードを防ぐ

ファイルの重複ロードを防ぐ

2005/4/22 記述
2005/6/18 ファイル名について修正
2005/8/3 std::hash_mapについて掲載
2006/2/17 NYSL 0.9982を適用

もしSTLportを利用できるなら、標準C++ではありませんが、ハッシュコンテナを使った管理を行う方が手軽で確実です。詳細ページはこちら

今回のコードはDirectX8のテクスチャ管理用に特化したものをダウンロードできます。が、このコードはこれを記述した時点ではまだテスト段階で、実際のコードに組み込んで使用してはいません。参考にする際には、未知のバグが潜んでいる可能性があることに注意してください。

ダウンロード(7.18KB)

概要

プログラミングをするとき、外部からデータを読み込んで利用したいときがあります。ゲームを作る上ではマップファイルを読み込むときなどです。2D RPGなどでは、マップにはあらかじめ用意されたパーツの配置を決めるだけのことが多いと思います。そのような場合、この記事はほとんど関係ありません。

今私が書こうとしているのは、3Dメッシュとそれに指定されているテクスチャをまとめて管理するクラスを書いているときに気づいた問題と、その解決法です。

私の書いたメッシュ保持クラスは、テクスチャデータをメッシュごとに保持していました。しかし、3Dゲームでは同一のテクスチャを複数のメッシュに指定する場合が良くあります(木目のメッシュなど)。このとき、データを個別に読み込む仕様のコードは、同一のデータを複数個メモリ上に保持することになり、メモリの無駄になります。

この問題を無視せずに解決するには、複数の方法が考えられます。

  1. 01.あらかじめ用意されたテクスチャのみを指定できるようにする
  2. 02.使用するすべてのテクスチャを別にリストアップしておき、その中からメッシュに使うテクスチャを選び出す
  3. 03.すべてのテクスチャを納めた1枚の大きなテクスチャの必要な部分だけを切り出してくる
  4. 04.テクスチャを一括して管理するクラスにロードを任せる。このクラスは同じテクスチャを重複してロードするように指示されても、既に読み込まれているテクスチャを返す。

01.は先に述べた2D RPGの例と同じ手法です。しかしこの方法では自由度が低すぎるので、02.がありますが、これはリストアップを自動化するとしても、やはり準備が面倒です。

03.はよく使われる手法ですが、開発者だけでなく一般ユーザーにもマップを作成できるようなゲームでは不便ですし、また1枚に詰め込むためにレイアウトの手間も生じます。

以上の理由に加え、個人的な趣味も含めて、ここでは04.の方法をとることにします。

実装の方針

さて、管理クラスには同じファイルを指定した場合には、ちゃんと同じファイルであると認識してもらう必要があるわけですが、一体どうやって識別すべきでしょうか?

たいていの OS のファイルシステムにはファイルを識別する機構(ファイルディスクリプタなど)があり、Windows ではファイルUID という 64Bit の値で管理されていたはずです(あまり自信はありません)。が、私はそれを取得する方法を見つけられなかったので、ファイル名で管理することにしました。ただし、ファイル名は相対パスではなく絶対パスである必要があります。

また、ファイル名とテクスチャデータを関連付けて管理する必要があるので、ここでは STL の map コンテナを使います(mapの使い方は「STLのページ」参照)。map のキーにファイル名を指定するわけです。

しかし、そのままキーにファイル名を指定すると、文字列は比較がやりにくいので、ロードが遅くなります。また、ファイル名はロードが終わってしまえばメモリを食うだけの冗長なデータになってしまいます。

そこで、ここではファイル名の 32Bit CRC をキーに使うことにします。いろいろ問題はあるかもしれませんが、ゲームで使うと割り切ってしまえばほぼ問題はないでしょう。

実際のコード

というわけで、以下が実際のコードです。ただし、テクスチャデータへのポインタの型を仮に LPTEXTURE とします。なので、そのままでは動きません。適当に書き換えてください。テクスチャ以外のデータに使う場合もちょっと変えるだけです。

CRC計算アルゴリズムには、奥村 晴彦 著「C言語による最新アルゴリズム事典」に掲載されていたcrc32t.cを使わせていただきました。

絶対パス取得コードはVisual C++6.0付属のMSDN ライブラリを参考にしました。

2005/6/18追加:なお、ファイル名はロングファイル名が与えられていると仮定します。また、ファイル名は大文字・小文字の違いがあるとCRCが変わってしまうので、すべて大文字(もしくは小文字)に統一した方がいいかもしれません(ここのコードでは無視しています)。

#include <windows.h>
#include <stdlib.h> //_fullpath()
#include <limits.h> //CHAR_BIT,UCHAR_MAX
#include <map>
#include 他にも必要なヘッダファイル

class CTextureManager{
protected:
    //ファイルパスの32bitCRCをキーにテクスチャを管理する
    std::map<unsigned int,LPTEXTURE> TextureManagerMap;
public:
    CTextureManager();
    virtual ~CTextureManager();
    virtual void Clear();    //管理しているすべてのテクスチャを放棄
    virtual bool Load(LPTEXTURE&,const char*); //テクスチャのロード
};

//*******************
//*    CRC計算部    *
//*******************

#define CRCPOLY 0x04C11DB7UL
    /* x^{32}+x^{26}+x^{23}+x^{22}+x^{16}+x^{12}+x^{11]+
       x^{10}+x^8+x^7+x^5+x^4+x^2+x^1+1 */

//CRCテーブル
static unsigned long crctable[UCHAR_MAX + 1];

//CRCテーブルの初期化;一度だけ呼べばよい
void MakeCRCTable()
{
    unsigned int i, j;
    unsigned long r;

    for(i = 0; i <= UCHAR_MAX; i++) {
        r = (unsigned long)i << (32 - CHAR_BIT);
        for(j = 0; j < CHAR_BIT; j++)
            if(r & 0x80000000UL) r = (r << 1) ^ CRCPOLY;
            else r <<= 1;
        crctable[i] = r & 0xFFFFFFFFUL;
    }
}

unsigned long CalcCRC32(int n, const BYTE c[])
{
    unsigned long r;

    r = 0xFFFFFFFFUL;
    while (--n >= 0)
        r = (r << CHAR_BIT) ^ crctable[(byte)(r >> (32 - CHAR_BIT)) ^ *c++];
    return ~r & 0xFFFFFFFFUL;
}



CTextureManager::CTextureManager()
{
    MakeCRCTable();
}


CTextureManager::~CTextureManager()
{
    Clear();
}


//テクスチャをロードする。
//テクスチャの読み込みに失敗した場合のみfalseを返す
//trueが返れば必ず有効なテクスチャが与えられると考えて良い
bool CTextureManager::Load(LPTEXTURE &Texture,const char *filename){
    //*********************
    //*   絶対パス取得    *
    //*********************
    //ファイル名が指定されていなければロードは失敗
    if(NULL==filename[0]){
        return false;
    }

    char Full[_MAX_PATH];    //絶対パス取得用バッファ
    if(NULL==_fullpath( Full, filename, _MAX_PATH)){
        //絶対パス取得失敗
        return false;
    }

    //*********************
    //*   重複チェック    *
    //*********************
    unsigned int key_crc=CalcCRC32(strlen(Full)*sizeof(char),(BYTE*)Full);

    std::pair<std::map<unsigned int,LPTEXTURE>::iterator,bool> p;

    p=TextureManagerMap.insert(std::pair<unsigned int,LPTEXTURE>(key_crc,NULL));

    //mapに挿入してみる。
    //失敗すれば、既に同じキーが登録されているということ。
    if(!p.second){
        //既に登録されているテクスチャのポインタを引数に設定して終了
        Texture=(*p.first).second;
        return true;
    }

    //挿入に成功すれば、新しくデータを読み込む必要がある
    //ここではLoadTextureが第一引数にファイル名、
    //第二引数にLPTEXTUREへのポインタをとり、ロードに失敗すればfalseを返す関数であるとする
    if(!LoadTexture(Full,&(*p.first).second)){
        //ファイルロードに失敗
        return false;
    }
    Texture=(*p.first).second;
    return true;
}

void CTextureManager::Clear()
{
    //アンロード
    std::map<unsigned int,LPTEXTURE>::iterator ite=TextureManagerMap.begin();
    std::map<unsigned int,LPTEXTURE>::iterator end=TextureManagerMap.end();

    for(;ite!=end;ite++){
        //ここではUnloadTextureは引数にLPTEXTUREをとり、
        //テクスチャをメモリからアンロードする関数であるとする
        UnloadTexture((*ite).second);
    }

    //クリア
    TextureManagerMap.clear();
}

コードについて

crc32t.h/crc32t.cpp以外のコードにはNYSL 0.9982を適用します。

戻る


[Claybird Logo]泥巣 by Claybird <claybird.without.wing@gmail.com>