ホームへ戻る

5章. シーンの変更とパラメータの渡し方

 前の章で割愛した、Parameterについてお話していきましょう。
前の章でも言った通り、前のシーンから次のシーンへ値を渡す時、グローバル変数を使った渡し方をしてはいけません。
今回作ろうとしている仕組みはAndroidの設計手法と似ています。
Androidも画面から画面へパラメータを渡したい時は「Intent」と呼ばれるものを使います。
今回はこの機能をものすごく簡素化して、特定のint型のみ自由に渡せるようにしてみます。
と言っても、画面から画面へは巨大な独自クラスのような物を渡す必要はないので、int型で十分です。
今回はTitleSceneで選択された難易度をGameSceneに渡すことをやってみます。

まずParameterの仕組みですが、特定のタグをキー値とした値を保持できるようにします。
特定のキーと値をセットにするにはmapが便利ですね。std::mapを知らない人はググってください。
mapは要するに配列要素に好きな物が使える奴です。

int arr[2];
arr[0] = 1;

これがC言語である通常の配列ですよね。mapは

arr["レベル"] = 1;

こんなことができるのです。"レベル"というキーとなるタグに、1という値を持たせることが出来ました。
TitleSceneでレベルが選択された時

const int Normal = 1;
arr["レベル"] = Normal;

こんな形でParameterに格納してやって、移った先のGameSceneでは

arr["レベル"]

を取り出せばいいってわけです。
さて、Parameterクラスを見てみましょう


Parameter.h


#pragma once

#include <map>

class Parameter
{
public:
    const static int Error = -1;

    Parameter() = default;
    virtual ~Parameter() = default;

    void set(std::string key, int val);
    int  get(std::string key) const;

private:
    std::map<std::string, int> _map;
};

Parameter.cpp


#include "Parameter.h"

/*!
@brief パラメータのセット
*/
void Parameter::set(std::string key, int val)
{
    _map[key] = val;
}

/*!
@brief パラメータの取得
*/
int Parameter::get(std::string key) const
{
    auto it = _map.find(key);//指定キーを取得
    if (_map.end() == it) {//無かったら
        return Error;//エラー
    }
    else {
        return it->second;//あったら値を返す
    }
}

特に難しいことはしていません。
先ほど言ったことをクラスにしただけです。
setでキーと値をセットします。
getでセットしたキーの値を取り出します。
エラー処理をしているのは、指定したキーが無かった時の為のものです。
キーに対応した値はsecondに入るので、イテレータのsecondを返しています。

これを利用しているパラメータの渡し先のGameSceneクラスをまず見てみましょう。


GameScene.h


#pragma once

#include "AbstractScene.h"

class GameScene : public AbstractScene
{
public:
    const static char* ParameterTagStage;//パラメータのタグ「ステージ」
    const static char* ParameterTagLevel;//パラメータのタグ「レベル」

    GameScene(IOnSceneChangedListener* impl, const Parameter& parameter);
    virtual ~GameScene() = default;

    void update() override;
    void draw() const override;

private:
    int _level;
};

GameScene.cpp


#include "GameScene.h"
#include <DxLib.h>

const char* GameScene::ParameterTagStage = "ParameterTagStage";//パラメータのタグ「ステージ」
const char* GameScene::ParameterTagLevel = "ParameterTagLevel";//パラメータのタグ「レベル」

GameScene::GameScene(IOnSceneChangedListener* impl, const Parameter& parameter) : AbstractScene(impl, parameter)
{
    _level = parameter.get(ParameterTagLevel);
}

void GameScene::update()
{
}

void GameScene::draw() const
{
    DrawFormatString(100, 100, GetColor(255, 255, 255), "ゲームレベルは %d です", _level);
}

TitleSceneから渡されるパラメータはコンストラクタに入ってきます。
パラメータは読み込み専用です。書き込みをする必要はないのでconstです。
当然、呼び出すメソッドもconstでなければなりませんので、Parameter::get()はconstメンバ関数です。
GameSceneにint _level;として変数を一つ用意してみました。
受け取ったパラメータをここで受け取って保持します。

drawではこの受け取った値を表示しています。

あ、そうそう、このdraw()にもconstが付いていることに注意してください。何故さっき付いていなかったかというと書き忘れていたからです(←
draw()メソッド内でやってはいけないことがあります。

1. メンバ変数の書き変え
2. 乱数の使用

draw()メソッドは処理落ちをした時に省略される可能性があります。
従ってdraw()の呼び出しを省略した時に計算結果が変わるような処理を書いてはいけません。
乱数を使わないように言語上守ることはできませんが、メンバ変数を書き換えないようにはできるのでconstメンバ関数としてください。
基本的にゲームプログラミングの設計は「update()で全部御膳立てをして、draw()はただそれに従って表示するだけ」だと思ってください。

ではパラメータを渡しているTitleSceneを見てみます。


TitleScene.h


#pragma once

#include "AbstractScene.h"

class TitleScene : public AbstractScene
{
public:
    TitleScene(IOnSceneChangedListener *impl, const Parameter& parameter);
    virtual ~TitleScene() = default;
    void update() override;
    void draw() const override;
};

TitleScene.cpp


#include "TitleScene.h"
#include <DxLib.h>
#include "GameScene.h"
#include "Define.h"

TitleScene::TitleScene(IOnSceneChangedListener* impl, const Parameter& parameter) : AbstractScene(impl, parameter)
{
}

void TitleScene::update()
{
    if (CheckHitKey(KEY_INPUT_E)) {
        Parameter parameter;
        parameter.set(GameScene::ParameterTagLevel, Define::eLevel::Easy);
        const bool stackClear = false;
        _implSceneChanged->onSceneChanged(eScene::Game, parameter, stackClear);
        return;
    }
    if (CheckHitKey(KEY_INPUT_N)) {
        Parameter parameter;
        parameter.set(GameScene::ParameterTagLevel, Define::eLevel::Normal);
        const bool stackClear = false;
        _implSceneChanged->onSceneChanged(eScene::Game, parameter, stackClear);
        return;
    }
}

void TitleScene::draw() const
{
    DrawString(100, 100, "タイトル画面", GetColor(255,255,255));
}

Eキーが押されていたら「Define::eLevel::Easy」を、Nキーが押されていたら「Define::eLevel::Normal」を渡すようにしてあります。
今回シーンのスタックはクリアしないようにしました。なので、stackClearにはfalseを渡しています。
(今回しれっとDefineの定義を足してあるので、配布しているプロジェクト内を確認しておいてください)

_implSceneChanedはLooperのインターフェイスポインタですから、ここでonSceneChaned()をコールすると、
Looper::onSceneChaned()がコールされます。

Looperを見てみましょう。


Looper.h


#pragma once

#include <stack>
#include <memory>
#include "AbstractScene.h"
#include "IOnSceneChangedListener.h"

class Looper final : public IOnSceneChangedListener
{
public:
    Looper();
    ~Looper() = default;
    bool loop() const;
    void onSceneChanged(const eScene scene, const Parameter& parameter, const bool stackClear) override;

private:
    std::stack<std::shared_ptr<AbstractScene>> _sceneStack; //シーンのスタック
};

Looper.cpp


#include "Looper.h"
#include "TitleScene.h"
#include "Error.h"
#include "GameScene.h"

using namespace std;

Looper::Looper()
{
    Parameter parameter;
    _sceneStack.push(make_shared<TitleScene>(this, parameter)); //タイトル画面シーンを作ってpush
}
/*!
@brief スタックのトップのシーンの処理をする
*/
bool Looper::loop() const
{
    _sceneStack.top()->update();    //スタックのトップのシーンを更新
    _sceneStack.top()->draw();      //スタックのトップのシーンを描画
    return true;
}

/*!
@brief シーン変更(各シーンからコールバックされる)
@param scene 変更するシーンのenum
@param parameter 前のシーンから引き継ぐパラメータ
@param stackClear 現在のシーンのスタックをクリアするか
*/
void Looper::onSceneChanged(const eScene scene, const Parameter& parameter, const bool stackClear)
{
    if (stackClear) {//スタッククリアなら
        while (!_sceneStack.empty()) {//スタックを全部ポップする(スタックを空にする)
            _sceneStack.pop();
        }
    }
    switch (scene) {
    case Title:
        _sceneStack.push(make_shared<TitleScene>(this, parameter));
        break;
    case Game:
        _sceneStack.push(make_shared<GameScene>(this, parameter));
        break;
    default:
        //どうしようもないエラー発生
        break;
    }
}

今回変更したのはonSceneChaned()メソッド内だけです。
stackClearがtrueならスタックをクリアします。
switchで次の画面が何のシーンなのか条件分岐してインスタンスの生成をし、スタックにpushしています。

実行結果(Nキーを押した場合)


今回onSceneChaned()のみ変更しました。処理のコアとなっているLooper::loop()には一切手を加えずにシーンの切り替えが出来たことに着目してください。
このようにnewするときだけ条件分岐し、抽象クラスのポインタを持って共通したI/Fでアクセスして処理することを先の章でも紹介したストラテージパターンと言います。
C++の大事な要素なのでよく覚えておいてください。

さて、提示したソースコードの最後に「どうしようもないエラー発生」と書きました。
通常そこに行くはずはないのですが、万が一来た時に処理できない、そんなときの対処方法を次の章で解説します。

→分からないことがあれば掲示板で質問して下さい


HPトップへ 質問掲示板へ

- Remical Soft -