ホーム < ゲームつくろー! < IKD備忘録

何か色々徒然と
ファイル読み込みをあれやこれや

(2011. 6. 4)


 ゲームのリソースは十中八九ファイル内に収められています。ですから、どこの場面を再生するのにもファイル読み込みが発生します。一番ベタな読み込みは例えば次のようなコードです:

Hoge hoge1, hoge2;

// シーンをロード
void loadScene() {
    {
        FILE *f = fopen("Hoge01.dat", "rb");
        fseek(f, 0, SEEK_END);
        unsigned sz = ftell(f);
        fseek(f, 0, SEEK_SET);
        char* data = new char[sz];
        fread(data, sz, 1, f);
       
        hoge1.create(data);

        delete[] data;
    }
    {
        FILE *f = fopen("Hoge02.dat", "rb");
        fseek(f, 0, SEEK_END);
        unsigned sz = ftell(f);
        fseek(f, 0, SEEK_SET);
        char* data = new char[sz];
        fread(data, sz, 1, f);
       
        hoge2.create(data);

        fclose(f);
        delete[] data;
    }
    ....
}

・・・んー(-_-;

 ファイルをオープンして、メモリに一度書き込み、それをHoge::createメソッドに渡す事でHogeオブジェクトが出来上がるという仕組みです。もちろんこれでもファイルにあるリソースから特定のオブジェクトは作れます。では、やばい所を探してみます。

 もしHoge01.datが非常に大きなファイルだったらどうなるか?ファイルオープン自体は巨大なファイルでも一瞬です。つまりfopen関数の呼び出しは問題なし。でも、freadでメモリに格納する所で非常な時間がかかります。hoge1を作るのにも時間がかかるでしょう。もしHoge01.datが大きくてhoge1生成までに2秒とか掛かったとすると、loadScene関数内で2秒以上プログラムカウンタが居座る事になります。その間他の更新作業や描画作業、ユーザのキー入力の受付け等々が一切行われません。プレイヤーからすると、ゲーム画面が完全に止まっている状態です。そういうファイルが10個位続いたとしたら、プレイヤーは20秒間止まった画面を見続けます。多分「落ちた」と思われるでしょう。

 止まったように見える理由はファイルの中身をいっぺんに読み込んでメモリに書きこもうとするためです。freadなどのファイル読み込み関数はファイルの先頭から1バイト単位で途中まで読み込む事ができます。どれだけ大きなファイルも小さくちぎって読みこめば、1回分は十分に速いです。そこで、ファイルを分割して読み込むことを考えます。

 loadScene関数を1回呼び出すとファイルをちょっと読むように変更してみます。取りあえずhoge1だけの場合は例えばこういう感じでしょうか:
 

Hoge hoge1;
unsigned fileSize1 = 0;
unsigned curRead_hoge1 = 0;
char *data1 = 0;
bool loading1 = false;
FILE *f1 = 0;

// シーンをロード
void loadScene1() {
    if (!loading1) {
        FILE *f1 = fopen("Hoge01.dat", "rb");
        fseek(f1, 0, SEEK_END);
        fileSize1 = ftell(f1);
        fseek(f1, 0, SEEK_SET);
        data1 = new char[fileSize1];
    }
    curRead_hoge1 += fread(&data1[curRead_hoge1], fileSize1 / 10, 1, f1);

    // ファイル読み終えた?
    if (curRead_hoge1 == fileSize1) {
        fclose(f1);
        f1 = 0;
        hoge1.create(data1);
        delete[] data1;
    }
}

最初にloadScene1が呼ばれると、loading1フラグはfalseなのでファイルオープンが走ります。ファイルサイズを調べて格納するのに必要なメモリを確保します。そのままファイルを読み込むのですが、fileSize1/10と全体の10分の1を読み込みます。curRead_hoge1には現在までの読み込みファイルサイズが格納されていきます。最初のloadScene1関数の呼び出しでは読み込みきれないので関数をそのまま抜けます。loadScene1を何度も呼び出すと、いずれ全ファイルを読み込み終えられます。この時curRead_hoge1がファイルサイズに達するので、Hogeオブジェクトにリソースデータ(data1)を渡し、確保したdata1メモリブロックを解放して終了します。

 簡易ではありますが、これで分割してファイルを読み込む事ができます。最初よりはマシになりました。

 ただ、実際に呼び出すとほころびが沢山あるのがすぐにわかります。実際にゲームループ内で呼んだと仮定したコードを書いてみます:

void update() {
    hoge1.update();
}

void draw() {
    hoge1.draw();
}

int main() {
    // ゲームループ
    while(1) {
        // ファイル読み込み
        loadScene();

        // 更新と描画
        update();
        draw();
    }
}

 Hogeオブジェクトが作成されていない時にも更新や描画がちゃんと回る(updateやdrawメソッド内で何もしなければ良い)とすると、ループ自体は回ります。loadScene関数を2回、3回・・・10回と呼び出すと、いずれファイルが全部読み込めます。その段階で更新と描画が始まりますが、問題はその次。11回目のループの時、再びloadScene関数が呼ばれるとどうなるでしょうか?ファイルポインタ(f1)は読み込み終えた段階でNULLにはなりますが、freadが走るのは明らかに「危険」です。これは「読み終えたらそもそもloadScene関数を呼びに行かない仕組み」が絶対に必要です。つまり「読み込みステート」がいるわけです。

 上のようなファイルの読み込みには、「まだ読み込んでいない」「現在読込中」「読み込み完了」という3態があります。これを列挙型で表現してみます:

// ファイル読み込みステート
enum FileLoadState {
    FileLoadState_Wait,    // まだ読み込んでいない
    FileLoadState_Loading, // 読込中
    FileLoadState_Finish,  // 読み込み終了
};


Hoge hoge1;
unsigned fileSize1 = 0;
unsigned curRead_hoge1 = 0;
char *data1 = 0;
//  bool loading1 = false;   // 読み込みフラグはいらなくなる
FILE *f1 = 0;
FileLoadState state1 = FileLoadState_Wait;

// シーンをロード
void loadScene1() {
    // すでに読み終わっている場合は終了
    if (state1 == FileLoadState_Finish)
        return;

    // 読み込み開始
    if (state1 == FileLoadState_Wait) {
        FILE *f1 = fopen("Hoge01.dat", "rb");
        fseek(f1, 0, SEEK_END);
        fileSize1 = ftell(f1);
        fseek(f1, 0, SEEK_SET);
        data1 = new char[fileSize1];
        state1 = FileLoadState_Loading;
    }

    curRead_hoge1 += fread(&data1[curRead_hoge1], fileSize1 / 10, 1, f1);

    // ファイル読み終えた?
    if (curRead_hoge1 == fileSize1) {
        fclose(f1);
        f1 = 0;
        hoge1.create(data1);
        delete[] data1;
        state1 = FileLoadState_Finish;
    }
}

 これで11回目以降の読み込みは阻止されますし、呼び出し側も現在読み込みがどういう状態になっているかを認知できます。

 では、ファイルが無かった場合は?上のコードはそのエラー処理が入っていないので、問答無用で落ちます。ファイルオープンに失敗したらエラー状態にして関数をすぐに終了するようにします:

// ファイル読み込みステート
enum FileLoadState {
    FileLoadState_Wait,    // まだ読み込んでいない
    FileLoadState_Loading, // 読込中
    FileLoadState_Finish,  // 読み込み終了
    FileLoadState_Error,   // 読み込みエラー
};

Hoge hoge1;
unsigned fileSize1 = 0;
unsigned curRead_hoge1 = 0;
char *data1 = 0;
//  bool loading1 = false;   // 読み込みフラグはいらなくなる
FILE *f1 = 0;
FileLoadState state1 = FileLoadState_Wait;

// シーンをロード
void loadScene1() {
    // すでに読み終わっているかエラー場合は終了
    if (state1 == FileLoadState_Finish || state1 == FileLoadState_Error)
        return;

    // 読み込み開始
    if (state1 == FileLoadState_Wait) {
        FILE *f1 = fopen("Hoge01.dat", "rb");
        if (f1 == 0) {
            state1 = FileLoadState_Error;
            return;
        }
        fseek(f1, 0, SEEK_END);
        fileSize1 = ftell(f1);
        fseek(f1, 0, SEEK_SET);
        data1 = new char[fileSize1];
        state1 = FileLoadState_Loading;
    }

    curRead_hoge1 += fread(&data1[curRead_hoge1], fileSize1 / 10, 1, f1);

    // ファイル読み終えた?
    if (curRead_hoge1 == fileSize1) {
        fclose(f1);
        f1 = 0;
        hoge1.create(data1);
        delete[] data1;
        state1 = FileLoadState_Finish;
    }
}

 これで読み込みに失敗しても落ちることは無くなります。


@ とりあえずクラス化

 関数としては動くものにはなりましたが、このままだとhoge1専用関数です。hoge2を読もうとした時に同じ関数を作るのは馬鹿げています。loadScene関数の引数に変数を渡すのは一つの解決策ですが、変数と機能が一体化していますからクラス化するのが自然の流れでしょう。そこで、仮にですがHogeLoaderクラスを作ります。上の関数をメンバメソッドにして、グローバル変数をメンバ変数にするだけですから簡単です:

class HogeLoader {
public:
    // ファイル読み込みステート
    enum FileLoadState {
        FileLoadState_Wait,    // まだ読み込んでいない
        FileLoadState_Loading, // 読込中
        FileLoadState_Finish,  // 読み込み終了
        FileLoadState_Error,   // 読み込みエラー
    };

    Hoge hoge;
    unsigned fileSize;
    unsigned curRead_hoge;
    char *data;
    FILE *f;
    FileLoadState state;
    std::string fileName;

public:
    HogeLoader(const std::string &fileName) : fileName(fileName), fileSize(), curRead_hoge(), data(), f(), state(FileLoadState_Wait) {}
    ~HogeLoader() {}

    // Hoge取得
    Hoge get() {
        return hoge;
    }

    // 読み込み状況取得
    FileLoadState getState() {
        return state;
    }

    // シーンをロード
    void loadScene() {
        // すでに読み終わっているかエラー場合は終了
        if (state == FileLoadState_Finish || state == FileLoadState_Error)
        return;

        // 読み込み開始
        if (state == FileLoadState_Wait) {
            FILE *f = fopen(fileName.c_str(), "rb");
            if (f == 0) {
                state = FileLoadState_Error;
                return;
            }
            fseek(f, 0, SEEK_END);
            fileSize = ftell(f);
            fseek(f, 0, SEEK_SET);
            data = new char[fileSize];
            state = FileLoadState_Loading;
        }

        curRead_hoge += fread(&data[curRead_hoge], fileSize / 10, 1, f);

        // ファイル読み終えた?
        if (curRead_hoge == fileSize) {
            fclose(f);
            f = 0;
            hoge.create(data);
            delete[] data;
            state = FileLoadState_Finish;
        }
    }
};

どうでもいい事ですが、こういう風にソースをバンバン貼り付けて改良を加えていく見せ方は紙面では無理ですね。いやはや、ネットで良かった(^-^)。

 クラス化すると、指定ファイル名で作れる複数のHogeを別々に分割読み込みできるようになります。上のクラスではゲッターが用意されているので、作成後にHogeオブジェクトを取得して使用します。

 では、今度はFooオブジェクトを同様に作る必要が出てきました。上のクラスでHogeがFooになれば良いのですが、残念ながらFooクラスは別のcreateメソッドだとします。メモリブロックを受け取る所までは一緒です。そこで、上のクラスからファイル読み込みの部分だけを分離したFileLoaderクラスを作ります:

class FileLoader {
public:
    // ファイル読み込みステート
    enum State {
        State_Wait,    // まだ読み込んでいない
        State_Loading, // 読込中
        State_Finish,  // 読み込み終了
        State_Error,   // 読み込みエラー
    };

    unsigned fileSize;
    unsigned curReadSize;
    char *data;
    FILE *f;
    State state;
    std::string fileName;

public:
    FileLoader(const std::string &fileName) : fileName(fileName), fileSize(), curReadSize(), data(), f(), state(State_Wait) {}
    ~FileLoader() {
        if (data)
            delete[] data;
    }

    // メモリブロック取得
    char *getBolck() {
        return data;
    }

    // ファイルロード
    State loadFile() {
        // すでに読み終わっているかエラー場合は終了
        if (state == State_Finish || state == State_Error)
        return state;

        // 読み込み開始
        if (state == State_Wait) {
            FILE *f = fopen(fileName.c_str(), "rb");
            if (f == 0) {
                state = State_Error;
                return;
            }
            fseek(f, 0, SEEK_END);
            fileSize = ftell(f);
            fseek(f, 0, SEEK_SET);
            data = new char[fileSize];
            state = State_Loading;
        }

        curReadSize += fread(&data[curReadSize], fileSize / 10, 1, f);

        // ファイル読み終えた?
        if (curReadSize == fileSize) {
            fclose(f);
            f = 0;
            state = State_Finish;
        }

        return state;
    }
};

 先の実装からHogeクラスの要素を消すと、純粋にファイルを分割読み込みするクラスになりました。このクラスを用いたゲームループはこんな感じになるかなと思います:

Hoge hoge1;
Foo foo1;
bool created_hoge1 = false, created_foo1 = false;

void update() {
    hoge1.update();
    foo1.update();
}

void draw() {
    hoge1.draw();
    foo1.draw();
}

int main() {
    FileLoader hogeLoader("Hoge01.dat");
   FileLoader fooLoader("Foo01.dat");

    // ゲームループ
    while(1) {
        // ファイル読み込み
        if (!created_hoge1 && hogeLoader.loadFile() == FileLoader::State_Finish) {
            hoge1.create(hogeLoader.getBlock());
            created_hoge1 = true;
        }
        if (!created_foo1 && fooLoader.loadFile() == FileLoader::State_Finihs) {
            foo1.create(fooLoader.getBlock());
            created_foo1 = true;
        }

        // 更新と描画
        update();
        draw();
    }
}

あれ、手間が増えてる(^-^;。そうなんです、ファイルロードとオブジェクト生成を分離すると「ファイルロードが終了しているか?」と「オブジェクトが生成し終わっているか?」という2つの判断が必要になってしまいます。上のコードの何がうまくないのでしょうか。



A 読み終わって生成したら無くなって欲しい

 上のコードは、ゲームループ中でファイル読み込みと生成の部分がず〜っと通り続けてしまうのがうまくありません。家を建てる時に、建て終わった後も大工さんがお家の前でウロウロしているようなものです。これは気持ちが悪い(笑)。お仕事が終わったらご帰宅願いたいわけです。プログラム上で一時的に仕事をして、終わったらいないことにするには、newで大工さんを一時的に作り「家を建てたらいなくなってね〜」と頼めばいいんです。

 大工さんである生成者オブジェクトを作り、それをリストに登録します。ゲームループでは毎フレームそのリストをチェックし、生成者がいれば仕事をしてもらいます。ある瞬間、生成者は「作り終わりました〜」と連絡してくるので、リスト管理者はその生成者をリストから外します。これでその生成者は消えてしまうので、全部生成し終われば何事もなかったようにゲームループが進みます。

 Hogeを作るHogeCreator、Fooを作るFooCreatorを新規に作成します:

class CreatorBase {
protected:
   FileLoader fileLoader;

public:
   CreatorBase(const std::string &fileName) : fileLoader(fileName) {}
    virtual ~CreatorBase() {}

   // ファイル読み込み
    FileLoader::State load() {
        return fileLoader.loadFile();
    }

    // オブジェクト生成
    virtual void create() = 0;   // 派生クラスで具体的に
};


class HogeCreator {
    Hoge *hoge;
    std::string fileName;

public:
    HogeCreator(Hoge *hoge, std::string fileName) : hoge(hoge) : CreatorBase(fileName) {}   // Hogeオブジェクトを一時的に貸し出し

    // オブジェクト生成
    virtual void create() {
        // メモリブロックを貰って生成
        hoge->create(fileLoader.getBlock());
    }
};

 HogeCreatorを作った時にファイル名を渡します。このオブジェクトをリストに登録すると、裏で勝手にHogeCreator::loadメソッドが呼ばれます。このメソッドではファイルがちょっとずつ読み込まれます。読み込み終わったらそのステートが返ってきますので、すぐにHogeCreator::createメソッドが呼ばれ生成作業が行われます。その後、そのオブジェクトはリストから外されます。そういう機構はこんな感じにります:

std::list<CreatorBase*> creatorList;

// リストに登録
void addList(CreatorBase *creator) {
    creatorList.push_bacl(creator);
}

// 読み込み処理
void updateLoading() {
    std::list<CreatorBase*>::iterator it = creatorList.begin();
    for (; it != creatorList.end();) {
        FileLoader::State state = (*it)->load();
        if (state == FileLoader::State_Finish) {
            (*it)->create();
            // 削除後リストから外す
            delete it;
            it = creatorList.erase(it);

            continue;
        }
        it++;
    }
}

updateLoading関数は毎フレームガンガン呼び出します。関数内では登録されている生成者にファイル読み込みの指示を与え続けます。読み込みが終わったら、createメソッドを呼び出し、すぐに消してしまいます。つまり、リストに登録されればいつでもファイル読み込みと生成作業が勝手に発生するわけです。

 ゲームループはつまりこうなります:

Hoge hoge1;
Foo foo1;

void update() {
    hoge1.update();
    foo1.update();
}

void draw() {
    hoge1.draw();
    foo1.draw();
}

int main() {
    addList(new HogeCreator(&hoge1, "Hoge01.dat"));
   addList(new FooCreator(&foo1, "Foo01.dat"));

    // ゲームループ
    while(1) {
        // 裏読み更新
        updateLoading();

        // 更新と描画
        update();
        draw();
    }
}

わ〜お!すっきりです(^-^)。



B カスケーダブル生成

 上の実装、かなりリファクタリングが進んで良い感じです。CreatorBaseを継承して生成方法を実装しリストに登録すれば、一つのデータファイルから裏読みで物を作る一連の流れが実現できます。しかし、ここでちょっと困ったことが起こるんです。例えば、テキストファイルに生成すべきオブジェクトが連なっているとしましょう。Sceneクラスはそのテキストファイルに書かれている情報を見て、オブジェクトを作るとしましょう。上の図式に従い、SceneCreatorクラスを作り、ファイル読み込みと生成をしてもらいます:

class SceneCreator {
public:
    SceneCreator(Scene *scene, const std::string &fileName) : CreatorBase(fileName) {}
    virtual ~SceneCreator() {}

    virtual create() {
        scene->create(fileLoader.getBlock());
    }
};

うん、まぁ、見た目には問題ありません。しかし、Sceneクラスは読み込まれたテキストにある「大量の」オブジェクトをScene::createメソッド内で作ろうとします。もちろんリソースはありませんから「ファイルから」作ります。もしScene::createクラス内で裏読みが無ければ、ゲームは完全に止まって見えます。ふり出しに戻ってしまうわけです(T-T)。

 こういうカスケード(連続に繋がること)な生成は、ゲーム製作では必要です。あるステージを作ろうと思ったら、そのステージを構成する情報をスクリプトなどに記述します。スクリプトファイルを読み込むのが1回目、そのスクリプトファイルにある情報からオブジェクトを作るのが2回目です。いや、もしかすると、あるオブジェクトを作るのにさらにファイルの読み込みが必要になる事も考えられます。3回目、4回目と実質際限はありません。よって、ファイル読み込みが発生するクラスは基本的にはすべて裏読み(非同期)機構を持たなければなりません(扱うファイルが極端に小さい場合はまぁ同期読み込みでもいいです)。

 先程のゲームループでは、読み込みリクエストは「ゲームループの外」でした。つまり、一度リクエストを出したらプログラムは二度とそこを通りません。しかし、一度ゲームループの中に入ると各オブジェクトのupdateメソッドは何度も何度も呼ばれますから、例えば次のようにupdateメソッド内に単純に読み込みリクエストを記述できません:

void Scene::update() {
   addList(new HogeCreator(this, "Stage01.dat"));
};

何度も何度もリクエストがかかってしまいます(^-^;。update内で一度だけリクエストされるようにするにはどうしたら良いのでしょうか?

 一番単純なのはupdate関数が呼ばれたか否かをチェックするフラグを設ける方法があります:

void Scene::update() {
    if (!bUpdated) {
        addList(new HogeCreator(this, "Stage01.dat"));
        bUpdate = true;
    }
};

bUpdatedはコンストラクタでfalseにしておきます。アップデートが1回通ればフラグが切り替わりますから2度とここが呼ばれることはありません。多くの場合実はこれで十分だったりします。1回目のリクエストはこれでいけます。で、スクリプトが読めたとして、2回目はScene::createメソッド内で発生します:

void Scene::create(unsigned char* memBlock) {
    // メモリブロックから作成するオブジェクトのIDとファイルのリストを作ったと思いねえ
    std::vector<ObjInfo> ary = getGameObjectAry(memBlock);

    // オブジェクトを作成
    for (size_t i = 0; i < ary.size(); i++) {
        switch(ary[i].objID) {
            case ID_HOGE:
            {
                Hoge *hoge = new Hoge;
                AddList(new HogeCreator(hoge, ary[i].dataFilePath));
                objectList.push_back(hoge);
                break;
            }
            case ID_FOO:
            {
                Foo *foo = new Foo;
                AddList(new FooCreator(foo, ary[i].dataFilePath));
                objectList.push_back(foo);
                break;
            }
        }
    }
};

メモリブロックから何らかの方法で作るべきオブジェクトの種類(objID)とそのデータが入ったファイル名を抽出します。次に作るべきオブジェクトを一気にリクエストにかけます。もちろん、新規で作成したオブジェクトはSceneクラス内にも保持しておいてゲーム内でアクセスできるようにします。作成部分は実際はファクトリに投げることになるのですが、ここではswitch〜caseで示してあります。

 各クラスでこのような作成の仕組みを整えておけば、せっかくの裏読みの機構が破壊されることはありません。カスケードな作成はこれで何とかなりそうです。



C 裏読み終わった?

 シーンなどで沢山のオブジェクトを生成すべく、ファイル読み込みのリクエストをかけます。あるオブジェクトは小さいですからあっという間に読み終わって生成作業が始まります。別のオブジェクトはメッシュやらテクスチャやらの読み込み作業で完成までに時間がかかります。よ〜いドンで一斉に作らせても終わる時間が異なるわけです。となると、「じゃぁ、いつゲームを始めて良いのか」となるわけです。

 CreateBaseクラスの派生群は読み込みと生成のきっかけまでは与えますが、生成の終了は感知しません。Scene::createメソッドを抜けた後も生成作業は続きますよね。ですから、多分ですが、自分がリクエストを掛けたオブジェクトに「ねぇねぇ、生成終わった?」と尋ねるしかないかなぁと思います。さてそうなると、updateメソッドに「読み込み終わった?状態」が必要になります。ちょっとベタに書いてみましょう:

void Scene::update() {
    if (!bUpdated) {
        addList(new HogeCreator(this, "Stage01.dat"));
        bUpdate = true;
    }

    if (!checkLoading())
        return;
};

checkLoadingメソッドは生成しているオブジェクトの読み込みと生成が終了したかどうかを毎フレームチェックし、完了していたらtrueを返します。これで読み込みが全部終了してからシーンの更新ができるようになります。checkLoadingメソッド内ではリクエストを出したオブジェクトが持っている(とします)isReadyメソッドを監視します:

bool Scene::checkLoading() {
    std::list<GameObject*>::iterator it = loadingObjList.begin();
    for (;it != loadingObjList.end();) {
        if ((*it)->isReady()) {
            // 生成が終わったのでリストから外す
            it = loadingObjList.erase(it);
            continue;
        }
        it++;
    }

    if (loadingObjList.size() != 0)
        return false;   // まだよ

    return true;  // 完了!
}

まぁ、こうするしかここは無いでしょうねぇ・・・。別の方法としては、オブジェクトに対して「君にこのボタンを渡しておくから、生成が終わったらボタンを押しなさい」という方法も考えられます。ボタンを渡したときにカウンタをひとつ上げておきます。生成後そのボタンを押すとカウンタが一つ減ります。Sceneはそのカウンタが0になるまで待っていればいいわけです。この方法は監視する側は楽ですが、ボタンの押し忘れや2度押しが発生するとカウンタが狂うというバグが起こりやすくなります。起こると・・・見つけるのは結構大変です(怖)。

 監視はまぁ良いとして、そろそろ「む〜」と思い出すのが、update内の監視機構です。現在一番最初のStage01.datファイルの読み込み要求、そしてcreateメソッド後全オブジェクトの読み込み完了を監視するcheckLoadingメソッドのチェック。これらは仕事が終わったら本来はもういらない部分です。でも、居座っています。…ん?この感じは既視感がありますね。そう、「大工さん」です。これら一過性のプログラムは一時的にサクッと作って、仕事をしてもらって、終わったらサヨナラしてもらうのがパターンです。

 Sceneクラス内で一時的にだけ働いてくれるプログラムは、Sceneクラスの入れ子クラスで作るのが一番楽です。それは「
入れ子クラスは親クラスのメンバにアクセスし放題」という大特権を持っているためです。そこで、まずScene::Taskクラスを作ります:

class Scene {
protected:
    // タスククラス
    struct Task {
        virtual bool proc(Scene *parent) = 0;
    };

    // 最初のファイル読み込み要求クラス
    struct Task_FirstUpdate : public Task {
        virtual Task* proc(Scene *parent) {
            addList(new HogeCreator(parent, "Stage01.dat"));
            return new Task_CreateGameObject;   // 仕事終わったので次へ
        }
    };

    // ゲームオブジェクト生成チェッククラス
    struct Task_CreateGameObject {
       virtual Task* proc(Scene *parent) {
            if (parent->checkLoading())
                return new Task_GameUpdate;   // 仕事終わった
            return this;   // まだよ(自分自身を返す)
        }
    };

    // ゲーム更新
    struct Task_GameUpdate(Scene *parent) {
        parent->gameUpdate();
        return this;
    };

    Task *curTask;

public:
    Scene() : curTask(new Task_FirstUpdate) {}
};

Taskクラス(構造体)は唯一procメソッドを持っています。このメソッドにはタスクがする小さな仕事を記述します。コンストラクタを見ると、curTaskにTask_FirstUpdate構造体が登録されています。

 Scene::updateメソッドは結局タスクを実行する人になります:

void Scene::update() {
    // タスクを実行
    (curTask) {
        Task *nextTask = curTask->proc(this);
        if (curTask != nextTask) {
            delete curTask;
            curTask = nextTask;
        }
    }
};

時間の掛かるファイル読み込みを分割しようとすると、結局こんな感じの小さな状態遷移がどうしても出てきます。形は違えども、クラスの中に小さなタスクを作って一時的な仕事をさせるというのは、この手の状態遷移の鉄板の一つかなと思います。



D まとめ

 ファイル読み込み関連に必要なクラスや機能をまとめます。

 まず、裏読みの機構としてFileLoaderクラスを作りました。指定のファイルをloadFileメソッドを呼びつづけることでちょっとずつ読んでくれます。読み込みと生成を勝手にやってもらうために、CreatorBaseクラスを作りました。このクラスを派生し、addList関数を通して登録すると裏読みとオブジェクト生成を勝手に行ってくれます。更新の最初の一度目だけに指定のファイルを読み込み、さらにそのファイルにある情報を元にして沢山のファイルを続けて(カスケータブルに)読み込む事は、ゲーム制作では非常に良くあります。そのため、クラスの中にタスククラスを設け、一過性のプログラムで仕事をしてもらいました。

 これらの要素が揃っていれば、ファイル読み込みについて根本的に困ることは少なくなるかなと思います。逆に言えば、こういうのが揃っているのがゲームエンジンですね。・・・んー、がんばろう。