ゆとりーなの日記

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

第12回〜タイトル画面のシーン遷移〜

前回はボタンでシーン遷移を行いましたが、今回は普通のゲームっぽく、タイトル画面に表示された項目を選んで指定したシーンに遷移するようにしてみましょう。今回のミソはboost.signal2を使って、複雑な条件分岐を全部省くとこですかね。
先ずはタイトル画面に表示される項目を描画するクラスを書きます。

// titlemenu.h
#pragma once

class FontImpl;

class TitleMenu : private boost::noncopyable {
public:
    TiitleMenu(const std::unique_ptr<GraphicDevice> &graphic_device, const std::string &caption, int id, float x, float y);
    void draw(DrawObject *draw_object, int id);
    template <typename T>
    Scene *next(int id) const {
        // 選択idが該当したら新しいシーンを生成して返す
        // 新しいシーンの型はテンプレート引数で渡す
        return id_ == id ? new T() : nullptr;
    }
private:
    const std::string caption_;
    const int id_;
    const float x_;
    const float y_;
    const std::shared_ptr<FontImpl> font_;
};

// titlemenu.cc
#include "titlemenu.h"
#include "graphicdevice.h"
#include "drawobject.h"
#include "fontimpl.h"

TitleMenu::TiitleMenu(const std::unique_ptr<GraphicDevice> &graphic_device, const std::string &caption, const int id, const float x, const float y) : caption_(caption), id_(id), x_(x), y_(y), font_(graphic_device->createFont(16, false, "MS ゴシック")) {
}

void TitleMenu::draw(DrawObject *draw_object, const int id) {
    // 選択idが該当したら色を赤に
    const float gb(id_ == id ? 0.f : 1.f);
    const D3DXCOLOR font_color(D3DXCOLOR(1.f, gb, gb, 1.f));
    draw_object->drawText("タイトル画面", 0.f, 0.f, D3DXCOLOR(1.f, 1.f, 1.f, 1.f), font_);
}

とまあ取り敢えずこんな感じに作ります。初期化時に、項目名と項目ID、それに表示座標を指定することにします。
TitleMenu::nextメソッドでは、次のシーンの型を外部から渡せるようにテンプレートメソッドになっています。
で、これを前回のTitleクラスに組み込みます。

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

class Title : public Scene, private boost::noncopyable {
public:
    Title(const std::unique_ptr<GraphicDevice> &graphic_device) : font_(graphic_device->createFont(16, false, "MS ゴシック")), id_(0), play_(graphic_device, "まったりプレイ", 320.f, 240.f), setting_(graphic_device, "設定", 1, 320.f, 288.f), draw_(), next_(), , next_scene_(nullptr) {
        // 描画シグナルにタイトル項目の描画メソッドをセット
        draw_.connect([this](DrawObject *draw_object, int id) {play_.draw(draw_object, id);});
        draw_.connect([this](DrawObject *draw_object, int id) {setting_.draw(draw_object, id);});
        // 選択シグナルに次の項目を調べるメソッドをセット
        next_.connect([this](int id) {return play_.next<Playing>(id);});
        next_.connect([this](int id) {return setting_.next<Setting>(id);});
        // 次のシーンをthisにする。
        next_scene_ = this;
    }
    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_);
        // 項目の描画メソッドをまとめて呼ぶ
        draw_(draw_object, id_);
        return next_scene_;
    }
    virtual void keyDownEvent(WPARAM key_code) {
        if (key_code == VK_DOWN) {
            id_ = ++id_ > 1 ? 0 : id_;
        } else if (key_code == VK_UP) {
            id_ = --id_ < 0 ? 1 : id_;
        } else if (key_code == 'Z') {
            // 次のシーンを調べる。
            const Scene *next(next_(id_));
            next_scene_ = next ? next : this;
        }
    }
private:
    const std::shared_ptr<FontImpl> font_;
    int id_;
    TitleMenu play_;
    TitleMenu setting_;
    boost::signals2::signal<void (int)> draw_;
    boost::signals2::signal<Scene *(int)> next_;
    Scene *next_scene_;
}

組み込むとこんな感じになります。先ずは、各選択項目のインスタンスを生成して、描画メソッドを描画シグナルにまとめて入れます。今回シグナル呼び出しに対してスロッドが死んでることは通常考えられないので、TitleMenuクラスは特に何も継承させていません。よってシグナル接続にはラムダ式が使えるので使ってやりましょう。ラムダ式の中身はthisメンバをキャプチャして、引数を取って単純にメソッド呼び出しを行うだけです。あと、next_メンバのシグナル接続では、TitleMenu::nextメソッドの接続の際に、項目が次に呼ぶシーンの型を指定しておきます。
これでdraw_シグナル呼び出しでは、各TitleMenuインスタンスのdrawメソッドが呼ばれ、且つTitleクラスの現在の選択項目のIDが渡されるので、これと自身のIDを比較して、同じであれば項目の色を赤くするということをやっています。
次にキー入力です。今回は項目の選択ということでキーリピートがあった方が良いだろうと思うので、メッセージループを用いた方で実装します。Title::keyDownEventイベントは、TestAplicationのkeyDownEventイベントからそのまま呼び出してやります。また、Titleの基底クラスであるSceneクラスにも同様の純粋仮想メソッドを追加してTitleクラスはこいつをオーバーライドしてやります。

// scene.h
#pragma once

class DrawObject;

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


// testapplication.cc
void TestApplication::keyDown(WPARAM key_code) {
    scene_->keyDownEvent(key_code);
}

Title::keyDownEventメソッドでは、上下キーが押されたら、選択項目IDを動かします。このとき無効なIDにならないように注意します。
'Z'キーが押されたら、next_シグナルを呼び出して、次のシーンを調べます。Title::nextメソッドは渡された項目のIDと自身のIDが同じであれば、項目が担当する次のシーンのポインタ、違えばnullptrを返します。boost.signal2を使って、関数呼び出し結果がnullptr以外を探して返すというのには、次のような関数オブジェクトを用意してやりテンプレート第二引数に渡してやります。

// title.h
struct MenuResult {
    typedef Scene *result_type;
    template <typename T>
    Scene *operator ()(const T &first, const T &last) const {
        // スロットなしならnullptrを返す
        if (first == last) {
            return nullptr;
        }
        // nullptr以外を返すスロットを探してその値を返す。
        return *std::find_if(first, last, [](Sequence *x) {return x;});
    }
};

class Title : public Scene {
public:
    // インターフェイス
private:
    // 内部実装関係
    boost::signals2::signal<Scene *(int), MenuResult> next_;
}

これで'Z'キーを押したとき、選択されている項目が担当する型のポインタがnext_sequence_に入るので、選択した項目に応じたシーン遷移が実現できます。