ホームへ戻る

 sp5章 メニュー画面の作り方.3

 3本立てで紹介するメニュー画面の作り方も最後になりました。
今回は、モジュールを使い始める時にInitializeし、モジュールを使い終わる時にFinalizeする仕組みをつくります。
また、表示があまりにあじけないので、実際にInitializeで画像をロードし、少し表示を変えてみましょう。
使用する画像は以下の画像で、四聖龍神録Plusよりスクリーンショットを利用して用意しました。



画像をこちらからダウンロードし、プロジェクトがあるフォルダに格納して下さい。

このようにimagesフォルダが追加されていればOKです。

さて、前回まではモジュールごとに「更新」「描画」の2つの関数しかありませんでした。
そこに「初期化」と「終了処理」を追加します。
シーンが切り替わる時は以下のような順番で処理が行われるようにします。

ゲーム画面のシーンから、設定画面のシーンに切り替える例で見てみましょう。
まず、ゲーム画面の更新部から管理部へ
①設定画面へシーン変更の依頼
をかけます。
この部分が「シーン変更"予約"」になっている点に注意して下さい。

シーン変更予約を受け付けると、管理部は一旦すぐに処理を返します。



そして自分が更新するタイミングで改めて
②ゲームモジュールの終了処理
を行い、ゲームモジュールの画像解放やメモリ解放等の処理を行います。
続いて、新しく使用するモジュールの初期化処理である
③設定画面シーンの初期化処理
を行います。以降の更新時はずっと
④設定画面シーンの更新処理 (および描画処理)
をコールし続けることになります。

何故一旦「予約」状態にして後から処理を実行するかというと、すぐにメモリ解放などの処理が出来ないからです。
例えば以下のようなコードがあったとしましょう。

int *p = (int*)malloc(4);
SceneMgr_ChangeScene(**);//ここでpが解放される
p[0] = 0;


このコードはp[0]に代入が起こった時点で強制終了します。
Game_Update() 関数内のどこからでもSceneMgr_ChangeSceneが呼べますが、
そのタイミングでメモリの解放などが行われると、その後異常参照が行われてしまいます。
上記の例であれば、上でpにメモリが確報されているのに、SceneMgr_ChangeSceneを呼んだタイミングで解放され、
その後pにアクセスしているので強制終了してしまいます。
このようにモジュールのUpdate処理がすべて終わってからメモリの解放などのFinalize処理を行う必要があるので、
一旦予約状態にし、SceneMgrのUpdateのタイミングで改めて実際のシーン変更を行うのです。

ではこれを実現するためのコードを紹介します。
赤字部は今回前章から変更した部分です。

# Config, Menu, Game に対する変更はモジュール名以外は全て同じです。
# 似たような修正をひたすらやらないといけないのは面倒だな・・と思うでしょう。
# 次の節のために、その気持ちを大事にしておいてください。


↓Config.h↓


#pragma once

void Config_Initialize();//初期化
void Config_Finalize();//終了処理
void Config_Update();//更新
void Config_Draw();//描画

↓Config.cpp↓


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

static int mImageHandle;    //画像ハンドル格納用変数

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

//終了処理
void Config_Finalize(){
    DeleteGraph(mImageHandle);//画像の解放
}

//更新
void Config_Update(){
    if(CheckHitKey(KEY_INPUT_ESCAPE)!=0){//Escキーが押されていたら
        SceneMgr_ChangeScene(eScene_Menu);//シーンをメニューに変更
    }
}

//描画
void Config_Draw(){
    DrawGraph(0,0,mImageHandle,FALSE);
    DrawString(0, 0,"設定画面です。",GetColor(255,255,255));
    DrawString(0,20,"Escキーを押すとメニュー画面に戻ります。",GetColor(255,255,255));
}

↓Game.h↓ ゲーム画面


#pragma once

void Game_Initialize();//初期化
void Game_Finalize();//終了処理
void Game_Update();//更新
void Game_Draw();//描画

↓Game.cpp↓


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

static int mImageHandle;    //画像ハンドル格納用変数

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

//終了処理
void Game_Finalize(){
    DeleteGraph(mImageHandle);    //画像の解放
}

//更新
void Game_Update(){
    if(CheckHitKey(KEY_INPUT_ESCAPE)!=0){ //Escキーが押されていたら
        SceneMgr_ChangeScene(eScene_Menu);//シーンをメニューに変更
    }
}

//描画
void Game_Draw(){
    DrawGraph(0,0,mImageHandle,FALSE);
    DrawString(0, 0,"ゲーム画面です。",GetColor(255,255,255));
    DrawString(0,20,"Escキーを押すとメニュー画面に戻ります。",GetColor(255,255,255));
}

↓Menu.h↓ メニュー画面


#pragma once

void Menu_Initialize();//初期化
void Menu_Finalize();//終了処理
void Menu_Update();//更新
void Menu_Draw();//描画

↓Menu.cpp↓


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

static int mImageHandle;    //画像ハンドル格納用変数

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

//終了処理
void Menu_Finalize(){
    DeleteGraph(mImageHandle);    //画像の解放
}

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

//描画
void Menu_Draw(){
    DrawGraph(0,0,mImageHandle,FALSE);
    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

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

    eScene_None,    //無し
} eScene ;

void SceneMgr_Initialize();//初期化
void SceneMgr_Finalize();//終了処理
void SceneMgr_Update();//更新
void SceneMgr_Draw();//描画

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

↓SceneMgr.cpp↓


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

static eScene mScene     = eScene_Menu;    //シーン管理変数
static eScene mNextScene = eScene_None;    //次のシーン管理変数

static void SceneMgr_InitializeModule(eScene scene);//指定モジュールを初期化する
static void SceneMgr_FinalizeModule(eScene scene);//指定モジュールの終了処理を行う

//初期化
void SceneMgr_Initialize(){
    SceneMgr_InitializeModule(mScene);
}

//終了処理
void SceneMgr_Finalize(){
    SceneMgr_FinalizeModule(mScene);
}

//更新
void SceneMgr_Update(){
    if(mNextScene != eScene_None){    //次のシーンがセットされていたら
        SceneMgr_FinalizeModule(mScene);//現在のシーンの終了処理を実行
        mScene = mNextScene;    //次のシーンを現在のシーンセット
        mNextScene = eScene_None;    //次のシーン情報をクリア
        SceneMgr_InitializeModule(mScene);    //現在のシーンを初期化
    }
    switch(mScene){       //シーンによって処理を分岐
    case eScene_Menu:    //現在の画面がメニューなら
        Menu_Update();   //メニュー画面の更新処理をする
        break;//以下略
    case eScene_Game:
        Game_Update();
        break;
    case eScene_Config:
        Config_Update();
        break;
    }
}

//描画
void SceneMgr_Draw(){
    switch(mScene){      //シーンによって処理を分岐
    case eScene_Menu:   //現在の画面がメニュー画面なら
        Menu_Draw();    //メニュー画面の描画処理をする
        break;//以下略
    case eScene_Game:
        Game_Draw();
        break;
    case eScene_Config:
        Config_Draw();
        break;
    }
}

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

// 引数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;
    }
}

↓main.cpp↓


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

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

    SceneMgr_Initialize();

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

        SceneMgr_Update();  //更新
        SceneMgr_Draw();    //描画

    }

    SceneMgr_Finalize();

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

# 「static」と先頭についている変数や関数はこのソースファイルからしかアクセスできないことを意味します。
# 外から参照する必要が無い変数や関数はアクセスさせないようにすることをC++ではカプセル化や隠蔽化といいます。
# staticは積極的に使っていき、安全なコーディングを意識しましょう。まず実行して動きを確認してみて下さい。

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

実行結果

見た目は画像が表示されるようになった以外に特に変化がありませんが、
中ではちゃんとInitialize処理やFinalize処理が走っています。
各自、Initialize部やFinalize部にprintfDx("hoge");を書いて動作を確認しておいてください。

さて、今回、Config, Menu, Game に似たような処理をひたすら追加しました

モジュールが今3つだからよいものの、これが今後増えて行けば気が遠くなりそうです。
何とか共通化できないのでしょうか。
また、もう一度SceneMgr.cppの中を見て下さい。


    switch(mScene){       //シーンによって処理を分岐
    case eScene_Menu:    //現在の画面がメニューなら
        Menu_Update();   //メニュー画面の更新処理をする
        break;//以下略
    case eScene_Game:
        Game_Update();
        break;
    case eScene_Config:
        Config_Update();
        break;
    }
    
    switch(mScene){      //シーンによって処理を分岐
    case eScene_Menu:   //現在の画面がメニュー画面なら
        Menu_Draw();    //メニュー画面の描画処理をする
        break;//以下略
    case eScene_Game:
        Game_Draw();
        break;
    case eScene_Config:
        Config_Draw();
        break;
    }

ひたすらswitch文を上のように書いていました。
これもモジュールが膨大な数になったら、case ... case ... case ...と書いて行かないといけないのでしょうか。
しかし、C++ではこれが見事に解決します。
どれ位見事かというと・・・上のコードが以下のようになる位です。


    scene->Update();
    scene->Draw();

!?

非常に簡単になりました。
どうしてでしょう。C言語版は

もしシーンがメニューならメニューのアップデートを
もしシーンがゲームならゲームのアップデートを...

という記述をしなければならないので、どうしてもコード量が多くなりがちでした。
しかしC++ではこのswitchやif文のような条件分岐が不要になるのです。
また、今まで個別に行っていたMenu,Config,Game..に加える変更を一つで済ませることもできます。
これは非常に大きなことです。
何故そのようなことができるのでしょう・・。

次の章ではC++を使ったスマートな書き方を紹介します。
しかしC++の知識が無いと理解できません。
C++の中で「継承、抽象クラス、ポリモーフィズム」といったことを利用するので、
もし知りたい人はその辺を勉強してから見てもらえると良いかと思います。

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


- Remical Soft -