第16回:ローディングのテクニック

 これまでは、ひとつひとつのテクスチャのために使う画像ファイルのデータがそれほど大きくなかったため、即プログラムが実行状態に入っていましたが、一定規模を持つゲームの場合、大量のデータファイルを読み込むことになり、一時的にフリーズ状態になってしまうでしょう。そこで今回は、最近のゲームで見かけないことのない「ローディング画面」を実装することにしましょう。
 ローディングモードの肝は、ずばり、マルチスレッドです。タスクルーチンとは独立した、読み込み専用ルーチンを別スレッドで起動することで、メインとなる処理を停止させずにすむようになります。


ロード画面を表示している後ろで、ハードディスクからデータを読み込む

 ステージや状況によって、データを読み込むためのルーチンが異なってくるため、まずは、ローディングモードを実行するための基底クラスを作成します。このクラスでは、別スレッドで作成されたCGameObjectクラス群を格納するための一時的なリスト(temp)と、スレッドの完了を知るための変数(complete)をstatic属性にしていますが、これは、別スレッドで実行される関数はstatic属性でなければならず、その関数内で呼び出される変数もstatic属性にする必要があるためです。

 CLoadData::Exec()では、バックグラウンドでロード処理が行われている間の画面処理を行っています。「Now Loading...」という文字を描画する箇所を変更すれば、オリジナルのローディング画面を作成することができます。ロード処理が完了して、completeがtrueになったのであれば、tempリストに一時的に格納したゲームデータをすべて実際のタスクリストへ転送し、ロードクラスを終了します。また、ロード中にウィンドウを閉じるなどの強制中断がなされ、タスク追加処理が行われなかったときの処理を、CLoadData::~CLoadData()内で用意しています。
class CLoadData : public CGameObject
{
private:
    int brightness;
    CDxFont *font;

public:
    CLoadData();
    virtual ~CLoadData();

protected:
    static bool complete;
    static list<ListData> temp;

    virtual void Exec();
};
bool CLoadData::complete;
list<ListData> CLoadData::temp;

CLoadData::CLoadData()
{
    // staticデータをリセット
    complete = false;
    temp.clear();

    // 変数の初期化
    brightness = 255;
    font = new CDxFont(20);
}

CLoadData::~CLoadData()
{
    if(font) delete font;

    // 中断されたのであれば、追加予定だったオブジェクトを破棄する
    if(complete == false){
        list<ListData>::iterator i = temp.begin();
        while(i != temp.end()){
            if((*i).autodelete == true) delete (*i).gameobj;
            i = temp.erase(i);
        }
    }
}

void CLoadData::Exec()
{
    if(complete == false){
        // バックグラウンド処理がまだ行われている
        brightness -= 2;
        if(brightness < 0) brightness = 255;

        RECT rc = {0,0,630,470};
        font->Draw(L"Now Loading...", -1, &rc,
            DT_BOTTOM | DT_RIGHT | DT_SINGLELINE,
            0x00FFFFFF | ((BYTE)brightness << 24));
    }else{
        // バックグラウンドで用意されたクラスをタスクに追加する
        list<ListData>::iterator i;
        for(i = temp.begin(); i != temp.end(); i++){
            AppendObject(*i);
        }

        // 自分自身は用済みなので破棄
        RemoveObject(this);
    }
}
 データロード処理は、CLoadDataの派生クラスによっておこないます。以下は一例です。CLoadStage::Init()内の_beginthread()でCLoadStage::LoadThread()をバックグラウンドで動作させ、その関数内で、読み込み処理を指示するようにしています。
class CLoadStage : public CLoadData
{
protected:
    virtual void Init();
    static void LoadThread(void *data);
};
void CLoadStage::Init()
{
    // 別スレッドでロードを開始
    _beginthread(this->LoadThread, 0, NULL);
    RemoveObject(this);
}

void CLoadStage::LoadThread(void *data)
{
    // ここで時間のかかる処理を実行
    
    //-----------------------------------------
    // 以下はプログラム例
    //-----------------------------------------
    ListData ld;

    ResetItemBox();

    // 背景
    ld.gameobj = new CBackGround();
    ld.autodelete = true;
    strcpy_s(ld.name, 32, "bg");
    ld.priority = BACKGROUND_PRIORITY;
    temp.push_back(ld);

    // プレイヤー
    ld.gameobj = new CPlayer();
    ld.autodelete = true;
    strcpy_s(ld.name, 32, "player");
    ld.priority = PLAYER_PRIORITY;
    temp.push_back(ld);
    AppendItemBox("player", ld.gameobj);
    
    //-----------------------------------------
    // 以上はプログラム例
    //-----------------------------------------

    complete = true;
}
 追加すべきCGameObjectクラス群をLoadThread()ですべて用意し、CLoadStage()などのローディングクラス1つだけをメインタスクに追加すれば、スマートなゲーム処理が行えるようになるでしょう。
    CGameObject game;
    CoInitialize(NULL);
    game.Initialize(mainWnd, hInstance);
    CSound::CreateDirectSound(mainWnd);

    game.AppendObject(new CTaskHead(), 0, true);
    game.AppendObject(new CTaskTail(), INT_MAX, true);

    game.AppendObject(new CLoadStage(), 1, true);

    ShowWindow(mainWnd, nCmdShow);
 YouTubeのロゴマークと重なるため、見づらいですが、画面右下に「Now Loading...」という文字が点灯しているのがおわかりいただけるかと思います。