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

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

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

書き手:肥田野

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

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

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

第10回ではテストとしてメッセージボックスをControlクラスで実体化させていました(前回のコードではこっそり消してあります)が、今後色々なイベントを追加していくにあたって、一々コードに書き加えていては膨大になりますし、パッと見ても分かりにくいですね。

そこで、どの座標にどんなイベントがあるのか、その他必要な情報(メッセージボックスの場合は読み取るテキストファイルのアドレス)をまとめて表にすれば、管理もしやすくなるでしょう。

そう、またCSVファイルの登場です。

マップの管理でトラウマを抱いた方もいるかもしれませんが、あそこで一度辛い経験をしたなら、今回は案外楽に感じるかもしれませんよ。

というわけでまずは新しいCSVファイルを作成して、以下のように記入します。

f:id:NUT_SoftwareDevelopper:20150607022746p:plain

たった1行です。

というかイベントが1つしかないので1行で済んでいるのであって、今回の内容が理解できたら自分でいくらでも増やしてみてください。そういう応用を効かせられるものCSVファイルを使う利点なので。

原則として、一つのマップにつきCSVファイル1つを割り当てることにしています。

そして、プロジェクトの方は新たにEvent.hとEvent.cppを作成し、以下のように編集します。

《Event.h》

#pragma once

class MessageWindow;

class Event{
public:
    int x,y;
    MessageWindow* msg;

    Event(int setX,int setY,char* add);

    void Activate();

    void All();
};

Eventクラスはそのイベントの存在する座標と、イベント内容のクラスのポインタ、後は各イベントを起動させる関数とイベントを実際に実行するAll関数を用意しています。

各イベントを作るときに、このEventクラスに必要な情報を全て保管して、Eventクラスのポインタを配列などにして扱えばグッと管理しやすくなりますね。

クラスの詳細はこんな感じです。

《Event.cpp》

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

Event::Event(int setX,int setY,char* add){
    x = setX;
    y = setY;

    char buf[255];
    int addp = 0;
    memset(buf,0,sizeof(buf));
    while(1){
        bool breakFlag = false;
        if(add[addp] != ','){//イベントタイプのセルを読みこむ
            buf[addp] = add[addp];
            addp++;
        }else{
            int bufp = 0;
            addp++;//セルの間の「,」を読み飛ばす
            if(strcmp(buf,"メッセージ") == 0){
                memset(buf,0,sizeof(buf));
                while(1){
                    if(add[addp] != '\n' && add[addp] != EOF){
                        buf[bufp] = add[addp];
                        bufp++;
                        addp++;
                    }else{
                        msg = new MessageWindow(buf);
                        breakFlag = true;
                        break;
                    }
                }
            }else{
                DebugBreak();
            }
            if(breakFlag)break;
        }
    }
}

void Event::Activate(){
    if(msg != NULL)msg->Reset();
}

void Event::All(){
    if(msg != NULL)msg->All();
}

コンストラクタにイベントのX座標とY座標、あとchar型の配列の先頭(=char型のポインタ)を引数にとっています。

ここまでしか読まないと疑問が生まれますよね。折角CSVファイルを作ったのに、fopenが無いじゃないかと。

そう、このコンストラクタCSVの中身を読み取る作業の「途中」の部分なんです。

そもそも最終的に一つのマップにいくつもイベントを配置するのに、その都度ファイルを読み込み直していては処理の無駄です。

よって、このEventクラスを統括する、EventMapクラスを作成しました。場所はMapクラスのすぐ下ですが、Mapクラスとの繋がりは皆無なので注意してください。

Eventクラスのコンストラクタの解説は後回しにして、まずはEventMapクラスから見ていきましょう。

《Map.h》

#pragma once
#include"define.h"
#include<vector>
using namespace std;

class Event;

struct Cell{
    int gh;
    bool canwalk;
    int drawMode;//0:プレイヤーより奥 1:プレイヤーより手前
};

class Map{
public:
    Cell cell[CELL_NUM_X][CELL_NUM_Y];
    int chipgh[(128/16)*(1248/16)];
    int viewX,viewY,width,height;

    Map(char* add);

    void FrontView();

    void BackView();

    void All();
};

class EventMap{
public:
    vector<Event*> ev;
    EventMap(char* add);
    void All();
};
include<vector

新しい要素が出てきました。

vectorとは言うなれば配列の強化版で、主な特徴は「配列の要素の数を増減させる」などです。

もしこれまで意識をしていなかった方はこの機会に覚えておいて欲しいのですが、C及びC++の配列は、宣言をしたあとに要素の数を変更することはできません。

理由を簡単に説明すると、配列が宣言された瞬間、メモリ空間内にその要素分だけ連続した領域が確保されるからです。

例えばint型が4バイトで、「int hairetu[4];」と宣言すれば、4×4で16バイト分、連続したメモリ領域が確保されます。

この「連続して」というのがキモで、配列の要素を添字で指定できるのは、配列の先頭の番地から添字×型のサイズ分ずれた場所に目的のデータがあるからなんです。

なのでもし先ほどの宣言で「hairetu[5] = 0;」などと命令した場合、コンピュータはhairetuの先頭アドレスから5×4バイト先の場所を調べに行ってしまいます。

勿論そこにはhairetuの変数なんてありませんし、ヘタをすればコンピュータを正しく動作をさせるためのシステムファイルが使っている場所かもしれません。

そこを「0」に書き換えたら……何が起こるんでしょうね。とても不味いことが起こりそうです。

まあ実際はそうならないようにコンパイル段階でエラーを出してくれますし、最近のOSはプロセスごとにメモリ空間を使い分けているなんて話も聞いたことがありますが、とにかくC++を使う以上はこの大原則を必ず理解しておいてください。

少し話が逸れてしまいましたが、普通の配列が以上の理由により要素の数を変更できないのに対し、vectorは要素の数が変更できるんです。

凄いですね。もう配列なんて使わずに全部vectorにしてもよいのではないでしょうか(笑)

実際に他のプログラミング言語では配列の要素を増減させられる物もありますが、C/C++の配列はそれができない分処理が軽いなどのメリットもありますし、上手に使い分けられるといいですね。

しかし今回は、将来的に複数のマップを使い分けると考えたとき、その都度イベントの数が変わるので、配列でなくvectorを使ったほうが良さそうです。

あまり深く悩みこまずに、こういう機能があるんだなーくらいの気持ちで読みすすめても今は大丈夫でしょう。

using namespace std;

またも新ワードですが、これはvectorとセットで宣言しておくもので、ここにこういうものを書くんだという認識さえあれば問題ないです。

一応意味としては、「名前空間『std』を使いますよ」ということになります。書かれたこと訳しただけですね(^_^;

vectorC++が標準で提供しているクラスで、それがstdという場所に属しているため、その場所を明示しているんです。

vector<Event*> ev;

vectorを使うときは、「vector<型> 変数名」と宣言します。

先程サラっと「vectorはクラスである」と言いましたが、私たちが普段使っているクラスとは少し違って、「テンプレートクラス」という物のひとつです。

テンプレートクラスは、オブジェクトを作るときに特定のメンバ変数の型をその都度変更できるクラスなのですが、まあその型を決めるときに「<型>」と指定してやるんですね。

テンプレートクラスについてもここでは深く触れません(というか私がよく理解しきっていません…)。

要素の数を変更できるvectorは、当然ながら宣言時に要素数を指定する必要はありません。

それではクラスの詳細を見ていきましょう。

《Map.cpp》

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

Map::Map(char* add){
    //変更なし
}

void Map::FrontView(){
    //変更なし
}

void Map::BackView(){
    //変更なし
}

EventMap::EventMap(char* add){
    FILE* fp = fopen(add,"r");
    if(fp == NULL)DebugBreak();
    int c;
    char buf[255];
    memset(buf,0,sizeof(buf));
    bool eofFlag = false;
    int x,y;
    while(fgetc(fp)!='\n');
    while(1){
        while(1){
            c = fgetc(fp);
            if(c == EOF){
                eofFlag = true;
                break;
            }
            if(c != ','){
                strcat(buf,(const char*)&c);
            }else{
                x = atoi(buf);
                memset(buf,0,sizeof(buf));
                break;
            }
        }
        if(eofFlag)break;
        while(1){
            c = fgetc(fp);
            if(c != ','){
                strcat(buf,(const char*)&c);
            }else{
                y = atoi(buf);
                memset(buf,0,sizeof(buf));
                break;
            }
        }
        while(1){
            c = fgetc(fp);
            if(c != '\n' && c != EOF){
                strcat(buf,(const char*)&c);
            }else{
                ev.push_back(new Event(x,y,buf));
                memset(buf,0,sizeof(buf));
                break;
            }
        }
    }
    fclose(fp);
}

void EventMap::All(){
    for(int i=0;i<ev.size();i++){
        ev[i]->All();
    }
}

各マップに一つ、このクラスのオブジェクトを持たせてやります。

このコンストラクタに先程作ったCSVファイルのアドレスを指定してやると、またwhile文を駆使しながらデータを読み取っていきます。

そして最終段階に到達したとき、最初に解説したEventクラスが登場するのです。

ev.push_back(new Event(x,y,buf));

evはvector<Event*>型のメンバ変数でしたね。

push_backはvectorクラスが持っている関数で、これが要素の数を1増やす役割を持ちます。

引数には増やした要素に入れる中身が必要なので、ここでnew演算子からのEventコンストラクタを指定してやります。少しトリッキーですね。

コンストラクタの引数は、CSVから読み出したX、Y座標、それとイベントタイプ以下の文字列を収めた配列です。

それでは最初に戻って、Eventコンストラクタの解説です。

Event::Event(int setX,int setY,char* add){
    x = setX;
    y = setY;

    char buf[255];
    int addp = 0;
    memset(buf,0,sizeof(buf));
    while(1){
        bool breakFlag = false;
        if(add[addp] != ','){//イベントタイプのセルを読みこむ
            buf[addp] = add[addp];
            addp++;
        }else{
            int bufp = 0;
            addp++;//セルの間の「,」を読み飛ばす
            if(strcmp(buf,"メッセージ") == 0){
                memset(buf,0,sizeof(buf));
                while(1){
                    if(add[addp] != '\n'){
                        buf[bufp] = add[addp];
                        bufp++;
                        addp++;
                    }else{
                        msg = new MessageWindow(buf);
                        breakFlag = true;
                        break;
                    }
                }
            }else{
                DebugBreak();
            }
            if(breakFlag)break;
        }
    }
}

char型の配列bufを作る所まではいいですが、int型のaddpなる変数がありますね。

これは引数のaddの何文字目を読んでいるかを記録するための変数です。

普通にファイルを読むときはfgetcを使ったのですが、今回はファイルを読むのではなくchar型の配列を読むので、このような変数が必要になります。

if(strcmp(buf,"メッセージ") == 0){

新しい関数です。strcmpは二つの引数の中身を読み比べて、同じ内容なら0を返します。

普通に「if(buf == "メッセージ")」じゃ駄目なの? と思った方は要注意。bufは配列なので、一つだけ==で条件式を作っても上手く動きませんよ。

強いて言うならfor文で「buf[i] == temp[i]」(※:temp = "メッセージ")とか書けば可能ですが、その手間を省いてくれるのがstrcmpです。

現段階ではメッセージ以外のイベントがないので、もし異なる文字列だった場合DebugBreak()させていますが、今後ここにいろんなイベントを増設させていきますよ。

さて、イベント周りの構成はあらかた解説したのですが、実際に起動させる、つまりメッセージイベントの場合はMessageWindowオブジェクトのReset関数を実行しないといけませんね。

Reset関数はEventオブジェクト内のActivate関数を実行することで自動的に呼び出されるので、プレイヤーが立札の前でスペースキーを押した時に、このActivate関数を実行させる処理を、Playerクラスに追加します。

……が、今回は既にだいぶ重くなってしまったので、さらにもう1回区切ろうと思います。前後編と言ったな、あれは嘘だ

ここまでではクラスを定義しただけなので、プロジェクトを実行しても変化はないはずです。

次回の更新をお待ちください。