ホームへ戻る

 sp6章 メニュー画面の作り方(C++編)

 前章までで紹介した作り方では非常に非効率に感じた部分も多かったでしょう。
今回はC++化することで、設計をスマートにしてみましょう。
まず、設計方針として、共通化できるものは共通の抽象クラスを使用します。



継承関係は上記のようになります。
SceneMgr、Game、Menu、Configはすべて「Initialize、Finalize、Update、Draw」という共通のメソッドを持っていました。
そこで、「Task」というすべてのクラスの元となる基底クラスを作ります。
さらに、Game,Menu,Configは同じ操作が重複していました。
そこで、「BaseScene」というシーンの抽象クラスを作ります。

所有関係は以下のようになります。

SceneMgrはGame、Menu、Configと言ったクラスのインスタンスを直接持ちません。

BaseScene* scene = (BaseScene*) new Game();

といった感じで、GameのインスタンスそのものをもつのではなくBaseSceneのポインタを持ちます。
こうすることで「Gameなら、Configなら、Menuなら・・・」といった処理をする必要がなくなります。
これは後からも説明するのでよく覚えておいてください。※1

そして今回ISceneChangerというインターフェイスクラスを追加しています。
インターフェイスクラスとは、純粋仮想関数のみを持つクラスで、
実装したクラスへメッセージを送ることが出来るようにするための窓口を提供するクラスです。

C言語版では、関数を持ったくくりをインスタンス化していなかったので、
Gameモジュールから所有元rootであるSceneMgrの関数をコールバックで呼ぶといったことが可能でしたが、
C++はクラスをインスタンス化しているので、所有元rootであるSceneMgrの関数を呼ぶことが出来ません。

SceneMgrのインスタンスをGameが持っていれば呼ぶことができますが、
そんなことをするとGameクラスが何でもできてしまうので危険です。
(この辺の説明はページ下部「■注目してもらいたい点2」と合わせてご覧ください)


不要な操作はできないようにするのがC++として適切な設計です。
GameモジュールなどがSceneMgrに伝えたいメッセージは「ChangeScene(シーンを変えて)」というメッセージのみです。
そこで、ChangeSceneメソッドのみをインターフェイスとして持ったISceneChangerをSceneMgrに実装し、
そのインスタンスポインタをGameモジュールなどに渡して使うことでSceneMgrに実装した「ChangeSceneのみに」アクセスが可能になるわけです。

# (メッセージはC++では関数コールで読み替えることができます)
詳細なクラス図は以下のようになります。


青はそのままではインスタンス化できない抽象クラス、ピンクはインスタンス化可能なクラスを表しています。
Gameモジュールなどから所有元rootであるSceneMgrにコールバックで「ChangeScene(シーンを変えて)」とメッセージを伝えたいときは
ISceneChangerというインターフェイスを窓口に伝えることになります。

それではコーディングをしていきます。
今回追加するファイルは

・Task.h
・BaseScene.h
・BaseScene.cpp
・ISceneChanger.h
・ISceneChanger.cpp

です。


↓Task.h↓


#pragma once

//タスククラス。何かのモジュールはすべてこのTaskクラスを継承する。
class Task {
public:
    virtual ~Task(){}
    virtual void Initialize(){}     //初期化処理は実装してもしなくてもいい
    virtual void Finalize()  {}     //終了処理は実装してもしなくてもいい
    virtual void Update()    = 0;   //更新処理は必ず継承先で実装する
    virtual void Draw()      = 0;   //描画処理は必ず継承先で実装する
};

↓ISceneChanger.h↓


#pragma once

typedef enum {
    eScene_Menu,    //メニュー画面
    eScene_Game,    //ゲーム画面
    eScene_Config,  //設定画面

    eScene_None,    //無し
} eScene ;

//シーンを変更するためのインターフェイスクラス
class ISceneChanger {
public:
    virtual ~ISceneChanger() = 0;
    virtual void ChangeScene(eScene NextScene) = 0;//指定シーンに変更する
};

↓ISceneChanger.cpp↓


#include "ISceneChanger.h"

ISceneChanger::~ISceneChanger(){
}

↓BaseScene.h↓


#pragma once

#include "Task.h"
#include "ISceneChanger.h"

//シーンの基底クラス。
class BaseScene : public Task {

protected:
    int mImageHandle;                //画像ハンドル格納用変数
    ISceneChanger* mSceneChanger;    //クラス所有元にシーン切り替えを伝えるインターフェイス

public :
    BaseScene(ISceneChanger* changer);
    virtual ~BaseScene(){}
    virtual void Initialize() override {}    //初期化処理をオーバーライド。
    virtual void Finalize() override ;        //終了処理をオーバーライド。
    virtual void Update() override {}        //更新処理をオーバーライド。
    virtual void Draw() override ;            //描画処理をオーバーライド。

};

↓BaseScene.cpp↓


#include "BaseScene.h"
#include "DxLib.h"

BaseScene::BaseScene(ISceneChanger* changer) : 
    mImageHandle(0) {
    mSceneChanger = changer;
}

void BaseScene::Finalize(){
    DeleteGraph(mImageHandle);
}

void BaseScene::Draw(){
    DrawGraph(0,0,mImageHandle,FALSE);
}

↓Config.h↓


#pragma once

#include "BaseScene.h"

//設定画面クラス
class Config : public BaseScene {

public :
    Config(ISceneChanger* changer);
    void Initialize() override;    //初期化処理をオーバーライド。
    //void Finalize() override;        //終了処理をオーバーライド。
    void Update() override;        //更新処理をオーバーライド。
    void Draw() override;            //描画処理をオーバーライド。

};

↓Config.cpp↓


#include "Config.h"
#include "DxLib.h"

Config::Config(ISceneChanger* changer) : BaseScene(changer) {
}

//初期化
void Config::Initialize(){
    mImageHandle = LoadGraph("images/Scene_Config.png");    //画像のロード
}

//更新
void Config::Update(){
    if(CheckHitKey(KEY_INPUT_ESCAPE)!=0){ //Escキーが押されていたら
        mSceneChanger->ChangeScene(eScene_Menu);//シーンをメニューに変更
    }
}

//描画
void Config::Draw(){
    BaseScene::Draw();//親クラスの描画メソッドを呼ぶ
    DrawString(0, 0,"設定画面です。",GetColor(255,255,255));
    DrawString(0,20,"Escキーを押すとメニュー画面に戻ります。",GetColor(255,255,255));
}

↓Game.h↓ ゲーム画面


#pragma once

#include "BaseScene.h"
#include "ISceneChanger.h"

//ゲーム画面クラス
class Game : public BaseScene {

public :
    Game::Game(ISceneChanger* changer);
    void Initialize() override;    //初期化処理をオーバーライド。
    //void Finalize() override;        //終了処理をオーバーライド。
    void Update() override;        //更新処理をオーバーライド。
    void Draw() override;            //描画処理をオーバーライド。

};

↓Game.cpp↓


#include "Game.h"
#include "DxLib.h"

Game::Game(ISceneChanger* changer) : BaseScene(changer) {
}

//初期化
void Game::Initialize(){
    mImageHandle = LoadGraph("images/Scene_Game.png");    //画像のロード
}

//更新
void Game::Update(){
    if(CheckHitKey(KEY_INPUT_ESCAPE)!=0){ //Escキーが押されていたら
        mSceneChanger->ChangeScene(eScene_Menu);//シーンをメニューに変更
    }
}

//描画
void Game::Draw(){
    BaseScene::Draw();//親クラスの描画メソッドを呼ぶ
    DrawString(0, 0,"ゲーム画面です。",GetColor(255,255,255));
    DrawString(0,20,"Escキーを押すとメニュー画面に戻ります。",GetColor(255,255,255));
}

↓Menu.h↓ メニュー画面


#pragma once

#include "BaseScene.h"

//メニュー画面クラス
class Menu : public BaseScene {

public :
    Menu(ISceneChanger* changer);
    void Initialize() override;    //初期化処理をオーバーライド。
    //void Finalize() override ;        //終了処理をオーバーライド。
    void Update() override;        //更新処理をオーバーライド。
    void Draw() override;            //描画処理をオーバーライド。

};

↓Menu.cpp↓


#include "Menu.h"
#include "DxLib.h"

Menu::Menu(ISceneChanger* changer) : BaseScene(changer) {
}

//初期化
void Menu::Initialize(){
    mImageHandle = LoadGraph("images/Scene_Menu.png");    //画像のロード
}

//更新
void Menu::Update(){
    if(CheckHitKey(KEY_INPUT_G)!=0){//Gキーが押されていたら
        mSceneChanger->ChangeScene(eScene_Game);//シーンをゲーム画面に変更
    }
    if(CheckHitKey(KEY_INPUT_C)!=0){//Cキーが押されていたら
        mSceneChanger->ChangeScene(eScene_Config);//シーンを設定画面に変更
    }
}

//描画
void Menu::Draw(){
    BaseScene::Draw();//親クラスの描画メソッドを呼ぶ
    DrawString(0, 0,"メニュー画面です。",GetColor(255,255,255));
    DrawString(0,20,"Gキーを押すとゲーム画面に進みます。",GetColor(255,255,255));
    DrawString(0,40,"Cキーを押すと 設定画面に進みます。",GetColor(255,255,255));
}

↓SceneMgr.h↓ シーン管理部


#pragma once

#include "ISceneChanger.h"
#include "BaseScene.h"

class SceneMgr : public ISceneChanger, Task {

private:
    BaseScene* mScene    ;    //シーン管理変数
    eScene mNextScene;    //次のシーン管理変数

public:
    SceneMgr();
    void Initialize() override;//初期化
    void Finalize() override;//終了処理
    void Update() override;//更新
    void Draw() override;//描画

    // 引数 nextScene にシーンを変更する
    void ChangeScene(eScene NextScene) override;

};

↓SceneMgr.cpp↓


#include "DxLib.h"
#include "Config.h"
#include "Game.h"
#include "Menu.h"
#include "SceneMgr.h"

SceneMgr::SceneMgr() : 
    mNextScene(eScene_None) //次のシーン管理変数
{
    mScene = (BaseScene*) new Menu(this);
}

//初期化
void SceneMgr::Initialize(){
    mScene->Initialize();
}

//終了処理
void SceneMgr::Finalize(){
    mScene->Finalize();
}

//更新
void SceneMgr::Update(){
    if(mNextScene != eScene_None){    //次のシーンがセットされていたら
        mScene->Finalize();//現在のシーンの終了処理を実行
        delete mScene;
        switch(mNextScene){       //シーンによって処理を分岐
        case eScene_Menu:        //次の画面がメニューなら
            mScene = (BaseScene*) new Menu(this);   //メニュー画面のインスタンスを生成する
            break;//以下略
        case eScene_Game:
            mScene = (BaseScene*) new Game(this);
            break;
        case eScene_Config:
            mScene = (BaseScene*) new Config(this);
            break;
        }
        mNextScene = eScene_None;    //次のシーン情報をクリア
        mScene->Initialize();    //シーンを初期化
    }

    mScene->Update(); //シーンの更新
}

//描画
void SceneMgr::Draw(){
    mScene->Draw(); //シーンの描画
}

// 引数 nextScene にシーンを変更する
void SceneMgr::ChangeScene(eScene NextScene){
    mNextScene = NextScene;    //次のシーンをセットする
}

↓main.cpp↓


#include "DxLib.h"
#include "SceneMgr.h"

int WINAPI WinMain(HINSTANCE,HINSTANCE,LPSTR,int){
    ChangeWindowMode(TRUE), DxLib_Init(), SetDrawScreen( DX_SCREEN_BACK ); //ウィンドウモード変更と初期化と裏画面設定

    SceneMgr sceneMgr;
    sceneMgr.Initialize();

    while( ScreenFlip()==0 && ProcessMessage()==0 && ClearDrawScreen()==0 ){//画面更新 & メッセージ処理 & 画面消去

        sceneMgr.Update();  //更新
        sceneMgr.Draw();    //描画

    }

    sceneMgr.Finalize();

    DxLib_End(); // DXライブラリ終了処理
    return 0;
}

本プログラムのプロジェクト一式はこちらからDLできます。

実行結果

■ 注目してもらいたい点1

SceneMgrクラスがすっきりしていることです。
これは上記※1で述べたことに続きます。
例えば、InitializeやFinalize部は以前このように書いていたものが


// 引数sceneモジュールを初期化する
static void SceneMgr_InitializeModule(eScene scene){
    switch(scene){          //シーンによって処理を分岐
    case eScene_Menu:       //指定画面がメニュー画面なら
        Menu_Initialize();  //メニュー画面の初期化処理をする
        break;//以下略
    case eScene_Game:
        Game_Initialize();
        break;
    case eScene_Config:
        Config_Initialize();
        break;
    }
}

// 引数sceneモジュールの終了処理を行う
static void SceneMgr_FinalizeModule(eScene scene){
    switch(scene){         //シーンによって処理を分岐
    case eScene_Menu:      //指定画面がメニュー画面なら
        Menu_Finalize();   //メニュー画面の終了処理処理をする
        break;//以下略
    case eScene_Game:
        Game_Finalize();
        break;
    case eScene_Config:
        Config_Finalize();
        break;
    }
}

こうなりました。

void SceneMgr::Initialize(){
    mScene->Initialize();
}

void SceneMgr::Finalize(){
    mScene->Finalize();
}

mSceneはGameやMenuやConfigを個別に保持しているわけではなく、
各インスタンスの抽象クラスのポインタを保持しているので、
new Game()でインスタンスを作った時はGameクラスのメソッドが、
new Menu()でインスタンスを作った時はMenuクラスのメソッドが呼ばれるため、
switchやifのような条件文が不要になりました。

このようにメッセージ(関数コール)は同じくして、異なる処理を実現することをポリモーフィズムと呼びます。

同様にUpdateやDrawもswitch文が不要になり非常にすっきりしています。

さらにGameやConfigやMenuにあったFinalize処理はなくなり、BaseScene::Finalizeに集約されています。
Drawメソッドも共通部はBaseScene::Drawに共通化してあります

従って、BaseScene::Drawの内容を変えるとGame,Config,Menuの表示すべてに影響します。
今まで、表示の仕方を変更したければ全てのモジュールに変更を加えなければなりませんでしたが、
共通化することで、必要な変更量がずっと減ったわけです。

 

■ 注目してもらいたい点2

インターフェイスによるコールバックです。
普通インスタンス所有先のメソッドから、所有元のメソッドをコールすることはできません。
例えば


void root::hoge(){
}

void root::main(){
    A a = new A();
    a.b();
}

のような場合、Aクラス内のbメソッドからroot::hoge()を呼び出す方法はありません。
呼び出すにはrootのthisポインタが必要です。
しかしrootそのもののポインタを教えてしまうと、rootのすべての操作が実行可能になってしまい、危険です。
「rootのhoge関数の呼び出し」のみを可能にする方法を伝達するために今回 ISceneChanger という名前のインターフェイスを使いました。


void Game::Update(){
    if(CheckHitKey(KEY_INPUT_ESCAPE)!=0){ //Escキーが押されていたら
        mSceneChanger->ChangeScene(eScene_Menu);//シーンをメニューに変更
    }
}

例えば上のような部分です。
シーンを管理しているのはGameクラスのインスタンスを所有しているSceneMgrですから、
GameはSceneMgrに所有されています。
Gameクラスが所有元のSceneMgr::ChangeSceneを呼び出すことは、前述の通り普通無理に思われますが、
ISceneChanger::ChangeSceneをSceneMgrが実装し、そのポインタをGameに渡してmSceneChangerに格納することで
「SceneMgr::ChangeSceneの呼び出しのみ可能」という処理のさせかたを実現出来るのです。
Gameクラスのメソッドは mSceneChanger->ChangeScene(); を呼び出すことでインスタンスの所有元にコールバックできます。

 

以上のようにC++では共通化し、ポリモーフィズムによって多態化することが可能になります。
また、コールバックを実装する時はインターフェイスを使って必要最小限な権限を委譲することができます。

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


- Remical Soft -