ここまでの冒険を記録しますか?

長岡技大ソフトウェア開発サークルの開発ブログ

【ゲーム製作入門】C/C++で簡単なRPGを作る⑪(前)【DXライブラリ】

書き手:肥田野

DXライブラリでRPGのベースになるマップ画面やデータ管理などのあれこれの制作に挑戦してみます。

RPGとして完成する保証はありませんが、途中経過だけでも参考になれば幸いです。

この連載は前回までの内容を理解していることを前提に進めていきます。

前回メッセージウィンドウのテスト表示をしたので、今回からはいよいよイベントとしてマップに実装していきます。

……が、その前に。

段々ヘッダファイルが増えてきてプロジェクトが混沌としてきましたね。

冒頭に「#pragama once」を付けているとは言え、色々なクラスが他のクラスを参照するので、インクルードの順番やヘッダファイル同士の関係性が分かりにくくなってきました。

というか私がエラーを頻発させて頭を抱えていたので、OBの方の知恵をお借りして、プロジェクト全体を整理したいと思います。

その上でイベントの実装をしたいので、今回の項目は前後編に分けました。

前編はファイルの整理に絞って解説します。

※おことわり(15.06.20追記)

当記事執筆時に気が付かなかった不具合(カメラが動かない)が確認できたので、第11.5回で修正方法を解説しています。

記事の通りに編集しても問題なくコンパイルできると思いますが、正常な動作はできなくなっています。

当連載を11.5回まで読み進めれば、不具合は修正されますのでご了承ください。

ソースを読む前に、この整理におけるルールを解説します。

今までは「Main.cpp」を除いて、クラスや関数の宣言、定義を同一のヘッダファイルで行ってきました。

しかし、今後はヘッダファイルではクラスや関数の宣言しか行わず、定義は各ヘッダファイルに対応させたcppファイルを新たに作成し、その中で行います。

「それではファイルの数が倍になって、なお分かりにくくなるのでは?」という疑問もあるかと思います。

しかし、原則同じ名前(例:Player.hとPlayer.cpp)を付けるので管理しにくくはなりませんし、何より「循環参照」と呼ばれるエラーを防ぐことができます。

循環参照の詳細については各自でググってもらうとして、この整理を怠ると私のように数百単位でエラーを吐かれる事になりますから、こういうルールがあるんだと言う事で覚えておいてもらえればと思います。

それでは前回作ったWindowBox.hとWindowBox.cppを実例にもう少し詳しく解説します。

≪WindowBox.h≫

#pragma once

class Waku{
public:
    int x,y,width,height,gh[9];
    bool live;
    
    Waku(int setx,int sety,int setwidth,int setheight);
    
    void View();
};

class MessageWindow:public Waku{
public:
    char txt[10][4][40];
    int page,gyou;

    MessageWindow(char* add);

    void Reset();
    
    void Read();

    void All();
};

非常にすっきりしていますね。

ここではクラスの中にどんな型・名前の物が入っているかしか決めません。

実際にコンストラクタやView関数でどんな作業を実行するのかは、次のWindowBox.cppで定義します。

他のヘッダファイルやクラスは参照しないので、#includeを付ける必要はありません。

多重インクルードを防ぐ「#pragama once」だけは忘れないようにしましょう。

≪WindowBox.cpp≫

#include<DxLib.h>
#include"WindowBox.h"
#include"Function.h"

Waku::Waku(int setx,int sety,int setwidth,int setheight){
    x = setx;
    y = sety;
    width = setwidth;
    height = setheight;
    LoadDivGraph("graphic\\WindowBase.png",9,3,3,6,6,gh);
    live = false;
}

void Waku::View(){
    DrawGraph(x,y,gh[0],TRUE);
    DrawExtendGraph(x+6,y,x+width-6,y+6,gh[1],TRUE);
    DrawGraph(x+width-6,y,gh[2],TRUE);
    DrawExtendGraph(x,y+6,x+6,y+height-6,gh[3],TRUE);
    DrawExtendGraph(x+6,y+6,x+width-6,y+height-6,gh[4],TRUE);
    DrawExtendGraph(x+width-6,y+6,x+width,y+height-6,gh[5],TRUE);
    DrawGraph(x,y+height-6,gh[6],TRUE);
    DrawExtendGraph(x+6,y+height-6,x+width-6,y+height,gh[7],TRUE);
    DrawGraph(x+width-6,y+height-6,gh[8],TRUE);
}

MessageWindow::MessageWindow(char* add):Waku(0,360,640,120){
    FILE* fp = fopen(add,"r");
    if(fp == NULL)DebugBreak();
    memset(txt,0,sizeof(txt));
    int p = 0,g = 0,num = 0;
    int c;
    while(1){
        c = fgetc(fp);
        if(c == EOF)break;
        if(c != '\n' && c != ';'){
            strcat(txt[p][g],(const char*)&c);
            num++;
        }else{
            if(c == '\n')g++;
            if(c == ';'){
                fgetc(fp);
                g = 0;
                p++;
            }
        }
    }
    fclose(fp);
}

void MessageWindow::Reset(){
    live = true;
    page = gyou = 0;
}

void MessageWindow::Read(){
    if(PushKey(KEY_INPUT_SPACE)){
        if(gyou < 3 && txt[page][gyou+1][0] != '\0'){
            gyou++;
        }else{
            page++;
            gyou = 0;
        }
        if(page > 8 || txt[page][0][0] == '\0'){
            live = false;
        }
    }
    for(int i=0;i<=gyou;i++){
        DrawFormatString(x+10,y+10+20*i,GetColor(255,255,255),txt[page][i]);
    }
}

ここで初めてヘッダファイルをインクルードします。

インクルードするのは、自身が必要としているファイルだけ。各ファイルは#pragma onceなどで多重インクルードされないようになっているので、他のヘッダファイルとの重複を気にする必要もありません。

いざクラスのメンバ関数(メソッド)を定義しようとしても、そのままだと何故かアクセスできません。

クラスの外側でメソッドを定義する場合、メソッド名の前に「クラス名::」を付ける必要があります。

今回の場合も、ファイル名を同じにしているのは人間が見やすいからであって、コンピュータの内部的には別々の処理ですから、「この関数はこのクラスに属していますよ」と教えてやらないといけないんです。

逆に言えば、こうして所属するクラスを明示することで、そのクラスのメンバ変数を使うことができるんですね。

さて、この作業をこれまでに作った全てのヘッダファイルに行います。

原則として、ヘッダファイルのインクルードはcppファイルで行うのがキモです。

ただし、ヘッダファイルの方で注意しなければならないことがあるので、次のControl.hの例を見てください。

≪Control.h≫

#pragma once

class Player;
class Map;

class Control{
public:
    Player* pl;
    Map* map[3];

    Control();
    ~Control();
    void All();
};

Controlクラスはメンバ変数に他のクラスのポインタを持つため、そのヘッダファイル単体では参照できません。

しかし、先頭で#includeを使うと、先ほどのルールに反してしまいます。

このルールに反した場合、循環参照のエラーが発生する場合があるので、このように先頭に「class 参照したいクラス名;」と書いてやることで解決できます。

これは「プロトタイプ宣言」と呼ばれる手法で、「まだ定義してないけど、後でこういうクラスを定義しますよ」と示すことができます。

自作の関数を使うときにもプロトタイプ宣言が重宝されます。今回のクラス内における関数の宣言も、ある意味でプロトタイプ宣言といえますね。

これでControlクラス内にPlayerクラスやMapクラスのポインタを持たせることができました。

≪Control.cpp≫

#include"Control.h"
#include"Player.h"
#include"define.h"
#include<DxLib.h>
#include"Map.h"

Control::Control(){
    pl = new Player("graphic\\Player.png");
    map[0] = new Map("csv\\flame1.csv");
    map[1] = new Map("csv\\flame2.csv");
    map[2] = new Map("csv\\flame3.csv");
    pl->x = pl->targetX = CELL_WIDTH;
    pl->y = pl->targetY = CELL_HEIGHT;
}

Control::~Control(){
    delete pl;
    for(int i=0;i<3;i++)delete map[i];
}

void Control::All(){
    
    if(pl->x>WINDOW_X/2 && pl->x<map[0]->width-WINDOW_X/2+CELL_WIDTH){
        viewX = pl->x-WINDOW_X/2;
    }
    if(pl->y>WINDOW_Y/2 && pl->y<map[0]->height-WINDOW_Y/2){
        viewY = pl->y-WINDOW_Y/2;
    }

    for(int i=0;i<3;i++)map[i]->BackView();
    pl->All(map);
    for(int i=0;i<3;i++)map[i]->FrontView();
}

Controlクラスの中身は大きな変更がありませんが、一つだけ見慣れないヘッダファイルをインクルードしていますね。

define.hは、その名の通りプロジェクト全体で使う定義文をまとめたファイルです。

以下のようになります。

≪define.h≫

#pragma once

#define WALKTIME 15

#define CELL_WIDTH 40
#define CELL_HEIGHT 40
#define CELL_NUM_X 21
#define CELL_NUM_Y 17
#define WINDOW_Y 480
#define WINDOW_X 640

static int viewX = 0;
static int viewY = 0;
static bool hitAnyKey = false;

歩行アニメーション表示時に使うWALKTIMEや、マス目のサイズを定義したCELL_WIDTHなどをまとめています。

さらに、グローバル変数viewXやviewYもここで宣言させています。

グローバル変数の前に「static」を付けたのですが、何故かこれを付けないと多重定義のエラーが出てしまうので、付けておくものだと考えておいてください。

詳細な理由が分かったら捕捉します。

ちなみにstatic自体は、関数内で宣言した変数に、関数から抜けた後も値を保持していて欲しい時に付け加えます。

define.hだけは例外的にcppファイルを作る必要はありません。この内容を宣言と定義に分けても仕方がないですからね。

あとは同様に全てのファイルに対応するcppファイルを作るのですが、Function.hだけ少し異なるので掲載しておきます。

≪Function.h≫

#pragma once

void CameraDraw(int,int,int,int,int,int);

bool PushKey(int);

Function.hの中身もクラスではないので、これは関数のプロトタイプ宣言と言えますね。

ちょっと驚くかもしれませんが、関数をプロトタイプ宣言するときには引数の名前は不要になります。型だけでいいんですよ。

≪Function.cpp≫

#include"Function.h"

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

void CameraDraw(int x1,int y1,int x2,int y2,int gh,int trans){
    DrawExtendGraph(x1-viewX,y1-viewY,x2-viewX,y2-viewY,gh,trans);
}

bool PushKey(int keyCode){
    if(CheckHitKey(keyCode)){
        if(!hitAnyKey){
            hitAnyKey = true;
            return true;
        }
    }else{
        hitAnyKey = false;
    }
    return false;
}

中身はこのようになっています。

hitAnyKeyをstatic宣言したところ、何故かControlクラスのAll関数でリセットできなくなってしまったので、PushKeyの仕様を暫定的に修正しています。

他は変化がないですね。

今回の解説はここまでです。あとのヘッダファイルもこのルールに従って直しておいてください。

次回は後編、イベントとしてのメッセージボックスの実装になります。