ゆとりーなの日記

日記的な事を書いて行くと思はれる

第11回〜シーン遷移〜

ゲームを作る基盤部であるウィンドウの作成、簡単な描画、音声再生、入力部の実装はひとまず終わったので今回からいよいよゲーム部の制作に入ります。今回はシーン遷移の部分です。
Applicationクラスを継承したTestApplicationクラスのupdateメソッドがメインループとなっている訳ですが、よくC言語とかで見かけるシーン遷移処理のコードをこの中に書くと次のようになります。

// testapplication.h
#include "application.h"

enum SceneID {
    SCENE_TITLE,
    SCENE_PLAYING,
    SCENE_MENU
};

class FontImpl;

class TestApplication : public Application {
public:
    TestApplication(const std::string &caption, int client_width, int client_height, bool windowed);
protected:
    virtual void update(DrawObject *draw_object);
private:
    void title(DrawObject *draw_object);
    void playing(DrawObject *draw_object);
    void menu(DrawObject *draw_object);
    SceneID scene_id_;
    const std::shared_ptr<FontImpl> font_;
};

// testapplication.cc
#include "testapplication.h"

TestApplication::TestApplication(const std::string &caption, const int client_width, const int client_height, const bool windowed) : Application(caption, client_width, client_height, windowed), scene_id_(SCENE_TITLE), font_(graphic_device_->createFont(16, false, "MS ゴシック")) {
}

void TestApplication::update(DrawObject *draw_object) {
    assert(draw_object);
    switch (scene_id_) {
    case SCENE_TITLE:
        title(draw_object);
        break;
    case SCENE_PLAYING:
        playing(draw_object);
        break;
    case SCENE_MENU:
        menu(draw_object);
        break;
    default:
        break;
    }
}

void TestApplication::title(DrawObject *draw_object) {
    assert(draw_object);
    draw_object->drawText("タイトル画面", 0.f, 0.f, D3DXCOLOR(1.f, 1.f, 1.f, 1.f), font_);
    if (GetAsyncKeyState('Z') & 0x8000) {
        scene_id_ = SCENE_PLAYING;
    } else if (GetAsyncKeyState('X') & 0x8000) {
        scene_id_ = SCENE_MENU;
    }
}

void TestApplication::playing(DrawObject *draw_object) {
    assert(draw_object);
    draw_object->drawText("プレイ画面", 0.f, 0.f, D3DXCOLOR(1.f, 1.f, 1.f, 1.f), font_);
    if (GetAsyncKeyState('Z') & 0x8000) {
        scene_id_ = SCENE_TITLE;
    }
}

void TestApplication::menu(DrawObject *draw_object) {
    assert(draw_object);
    draw_object->drawText("メニュー画面", 0.f, 0.f, D3DXCOLOR(1.f, 1.f, 1.f, 1.f), font_);
    if (GetAsyncKeyState('Z') & 0x8000) {
        scene_id_ = SCENE_TITLE;
    }
}

まあ取り敢えずこれでシーン遷移はできます。しかしシーンの種類が増えてきたらswitchは増えるし、関数呼び出しではできることが限られたりと色々不便です。そこでC++らしくシーンクラスを用いてこのシーン遷移を実現します。
まずは全てのシーンの基底となるインターフェイスクラスを用意します。

// scene.h
#pragma once

class DrawObject;

class Scene {
public:
    virtual ~Scene() {}
    virtual Scene *update(DrawObject *draw_object) = 0;
};

とりあえずこれだけ用意しとけば上のCっぽいシーン遷移は書き換えられます。インターフェイスクラスなので、仮想デストラクタと純粋仮想関数の定義にしておきます。
こいつを継承したそれぞれのシーンクラスを作ります。

// title.h
#pragma once
#include "scene.h"
#include "playing.h"
#include "menu.h"
#include "graphicdevice.h"
#include "drawobject.h"
#include "fontimpl.h"

class Title : public Scene {
public:
    Title(const std::unique_ptr<GraphicDevice> &graphic_device) : font_(graphic_device->createFont(16, false, "MS ゴシック")) {
    }
    virtual Scene *update(DrawObject *draw_object) {
        assert(draw_object);
        draw_object->drawText("タイトル画面", 0.f, 0.f, D3DXCOLOR(1.f, 1.f, 1.f, 1.f), font_);
        if (GetAsyncKeyState('Z') & 0x8000) {
            return new Playing();
        } else if (GetAsyncKeyState('X') & 0x8000) {
            return new Menu();
        }
        return this;
    }
private:
    const std::shared_ptr<FontImpl> font_;
}

// playing.h
#pragma once
#include "scene.h"
#include "title.h"
#include "graphicdevice.h"
#include "drawobject.h"
#include "fontimpl.h"

class Playing : public Scene {
public:
    Playing(const std::unique_ptr<GraphicDevice> &graphic_device) : font_(graphic_device->createFont(16, false, "MS ゴシック")) {
    }
    virtual Scene *update(DrawObject *draw_object) {
        assert(draw_object);
        draw_object->drawText("プレイ画面", 0.f, 0.f, D3DXCOLOR(1.f, 1.f, 1.f, 1.f), font_);
        if (GetAsyncKeyState('Z') & 0x8000) {
            return new Title();
        }
        return this;
    }
private:
    const std::shared_ptr<FontImpl> font_;
}

// menu.h
#pragma once
#include "scene.h"
#include "title.h"
#include "graphicdevice.h"
#include "drawobject.h"
#include "fontimpl.h"

class Menu : public Scene {
public:
    Menu(const std::unique_ptr<GraphicDevice> &graphic_device) : font_(graphic_device->createFont(16, false, "MS ゴシック")) {
    }
    virtual Scene *update(DrawObject *draw_object) {
        assert(draw_object);
        draw_object->drawText("メニュー画面", 0.f, 0.f, D3DXCOLOR(1.f, 1.f, 1.f, 1.f), font_);
        if (GetAsyncKeyState('Z') & 0x8000) {
            return new Title();
        }
        return this;
    }
private:
    const std::shared_ptr<FontImpl> font_;
}

各updateメソッドでは、キーが押されてシーンが遷移するときに遷移先のシーンのポインタを生成して返します。それ以外は自身のポインタを返します。こいつをTestAppliactionクラスに組み込んでやると、綺麗にシーン遷移が実現できます。

// testapplication.h
#include "application.h"

class Scene;

class TestApplication : public Application {
public:
    TestApplication(const std::string &caption, int client_width, int client_height, bool windowed);
protected:
    virtual void update(DrawObject *draw_object);
private:
    std::unique_ptr<Scene> scene_;
};

// testapplication.cc
#include "testapplication.h"
#include "title.h"

TestApplication::TestApplication(const std::string &caption, const int client_width, const int client_height, const bool windowed) : Application(caption, client_width, client_height, windowed), scene_(new Title()) {
}

void TestApplication::update(DrawObject *draw_object) {
    assert(draw_object);
    Scene *next(scene_->update(draw_object));
    if (next != scene_.get()) {
        scene_.reset(next);
    }   
}

先ず、コンストラクタ内でSceneのstd::unique_ptrメンバをTitleのポインタで初期化します。これで最初のscene_->updateメソッドは最初につくったTitleクラスのものが呼ばれます。次にこのメソッド呼び出しの返り値を取得して、scene_が保存しているポインタの値と比較します。返り値が同じであれば、シーンが変わらないことを表すのでそのまま、違えばシーンが変わったことを表すので、std::unique_ptrが保持するポインタを新しいシーンのものに入れ替えます。そうすることで次のTestApplication::updateメソッドでのscene_->updateメソッドは、新しいシーンのものが呼ばれます。
以上でシーン遷移の処理がswitch文を使った前時代的な物から、C++っぽいものになりました。しかもシーンをクラスで表わすようになったので、各シーンのリソース管理なども楽に行えます。