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

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

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

書き手:肥田野

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

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

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

今回は外部からファイルを読み込んで、プログラムに反映させる方法について解説します。

前回から変更するのはMap.hのみですが、ソースコードの前にいくつか新しい関数や要素を解説しておきます。

何故かというと、ファイルの読み込みには独自に用意された関数をいくつも使うので、先に解説しておかないと混乱しそうになるからです。

もちろんこれらは全部丸暗記する必要はありませんが、「こういう役割を持つ関数が存在する」ことだけは頭の片隅に留めておいてください。

FILE* fp;

FILE型という新しい型を使います。この型はC++が標準装備している構造体の一つで、その名の通りファイル(テキストファイルなど)を扱う時に、この型のポインタ変数で操作します。

クラスの実体化をするときに、「クラス名* 変数名」でポインタを作り、「変数名 = new クラス名」で実体化しますよね。これも同じ要領で、クラス名がFILEに、「new クラス名」をfopen関数に置き換えることで、プログラム内で使えるようになります。

fopen(const char* filename,const char* Mode)

そして、これがfopen関数です。その名の通り、ファイルをオープンする役割を持ちます。

引数に必要なのはファイルの名前と、どの形式でオープンするかです。

形式はいくつか種類がありますが、読み取り用の「"r"」書き込み用の「"w"」が代表的です。今回は読み込むだけなので「"r"」を使います。

ちなみに引数の型「char*」にくっついている「const」は、定数を表しています。

編集中にファイルの名前が変わったり、オープンの形式が変わっては大変ですね。

こうしてファイルをオープンすることで、ファイルに書かれている情報を変数に代入したり、逆に書き込んだりすることができます。

fclose(FILE* _File)

クラスを実体化したら、プログラム終了時に「delete」でメモリの開放をしますよね。

ファイルも同じように、fcloseという関数で閉じる処理をしなければいけません。

引数にはファイルのポインタ(今回ならfp)を使います。

fgetc(FILE* _File)

では実際に読み込むときはどうすればよいのでしょうか。

この関数は、引数で指定したファイルを1文字読み進める役割を持ちます。

読んだ文字は返り値として吐き出されるので、読んだ文字を使うときは「変数 = fgetc(fp)」と書いてやる必要があります。

また、文字を保存する必要がないとき、つまり読み飛ばしたいときはこの関数を1回実行させることで1文字読み飛ばせます。

ファイルを読み込むときに最低限必要な関数は以上です。

ちなみにここで紹介した関数は割と古典的な関数で、現在はより高性能な関数を代わりに使うことができます。

そのためコンパイラによっては「こっちの関数を使ったほうがいいよ」と警告を出すことがありますが、今回は無視してこのまま使いましょう。

ファイルの読み書きに慣れたら、より高性能な関数の使い方を調べてみてくださいね。

前置きが長くなりましたが、今回紹介するコードです。

≪Map.h≫

#pragma once

#include<DxLib.h>

#define CELL_WIDTH 40
#define CELL_HEIGHT 40
#define CELL_NUM_X 16
#define CELL_NUM_Y 12
#define WINDOW_Y 480
#define WINDOW_X 640

struct Cell{
    int gh;
};

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

    Map(){
        LoadDivGraph("BaseChip.png",(128/16)*(1248/16),128/16,1248/16,16,16,chipgh);
        
        FILE* fp;//ファイルのポインタを宣言
        fp = fopen("testmap.csv","r");//fpを読み取り形式で開く
        if(fp == NULL)DebugBreak();
        int c;//文字を格納する変数
        int retu = 0;
        int gyou = 0;
        char buf[10];//文字列を格納する
        memset(buf,0,sizeof(buf));
        bool eofFlag = false;
        while(1){
            while(1){
                c = fgetc(fp);//文字読んでcに格納
                if(c == EOF){
                    eofFlag = true;//EndOfFileの時にループを抜ける
                    break;
                }
                if(c != ',' && c != '\n'){
                    strcat(buf,(const char*)&c);//cがセルの区切りか改行でなければ、bufに連結する
                }else{
                    int num = atoi(buf);//bufをint型に直して、即席のローカル変数numに代入
                    cell[retu][gyou].gh = chipgh[num];//num番目のチップ画像のハンドルを取得
                    memset(buf,0,sizeof(buf));//bufをリセット
                    break;//区切りか改行ならループを抜ける
                }
            }
            //1セル分のループを抜けたら
            if(eofFlag)break;
            if(c == ',')retu++;
            if(c == '\n'){//改行だったら行を増やす
                gyou++;
                retu = 0;
            }
        }
        fclose(fp);
    }

    void View(){
        for(int i=0;i<CELL_NUM_X;i++){
            for(int j=0;j<CELL_NUM_Y;j++){
                DrawExtendGraph(i*CELL_WIDTH,j*CELL_HEIGHT,(i+1)*CELL_WIDTH,(j+1)*CELL_HEIGHT,cell[i][j].gh,TRUE);
            }           
        }
    }

    void All(){
        View();
    }
};

まず今回読み込ませるファイルですが、Exelなどの表計算ソフトで以下の画像のような表を作り、「名前を付けて保存」をするときに拡張子を「csv」にして保存してください。 f:id:NUT_SoftwareDevelopper:20150513201350p:plain 設定画面のようなものが出るかもしれませんが、特に変更はせずOKだと思います。

保存されたら、それを右クリックして「プログラムから開く」→「メモ帳」を選択すると、以下のように表示されると思います。

0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,4,4,4,4,4,4,4,4,4,4,4,4,4,4,0
0,4,4,4,4,4,4,4,4,4,4,4,4,4,4,0
0,4,4,4,7,7,7,7,7,7,7,7,4,4,4,0
0,4,4,4,7,7,7,7,7,7,7,7,4,4,4,0
0,4,4,4,7,7,7,7,7,7,7,7,4,4,4,0
0,4,4,4,7,7,7,7,7,7,7,7,4,4,4,0
0,4,4,4,7,7,7,7,7,7,7,7,4,4,4,0
0,4,4,4,7,7,7,7,7,7,7,7,4,4,4,0
0,4,4,4,4,4,4,4,4,4,4,4,4,4,4,0
0,4,4,4,4,4,4,4,4,4,4,4,4,4,4,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0

セルの区切りが「,」で置き換えられていますね。

数字は前回読み込んだマップチップの番号です。つまりこのファイルが読み込めれば、表計算ソフトからマップの編集ができるということになります。

それではソースコードを解説します。

if(fp == NULL)DebugBreak();

fopen関数を使ったとき、もしファイルが存在しない・名前が違うなどでオープンできなかったとき、そのまま作業を続けると「存在しないファイルを参照する」ことになってしまいます。

ファイルの操作に限らず、プログラムの世界で「存在しないもの・正体の分からないもの」にアクセスすることは最大のタブーです。

簡単に理由を説明すると、アクセス先にシステムを安全に動作させるためのファイルが存在したときに、そのファイルを破壊してしまう恐れがあるからです。

最悪の場合、OSそのものがお亡くなりになることもあるとか。

まあそうならないように自衛機能が備わっていることが多いのですが、プログラマ自身が理解の及ばないところに手を突っ込んでしまうのは何かと面倒になりがちです。

逆に、世に言うハッカー達はこうした見当違いなアクセスを悪用することで、コンピュータに攻撃をしてくることが多いそうです。

そしてこのfopenは、エラーが発生したときにNULLを返してくれます。

なのでfpにNULLが入れられていたら、そこで作業を中断するのです。

あ、DebugBreak()はそこで処理を中断するための関数ですよ。

int c;

あれ、おかしいな? と思いませんでしたか。

文字といえばchar型なのに、ここでは文字を保管する変数にint型を使っています。

実は、文字は内部的には数字で扱われているのです。「ASCII文字コード」で検索すると、文字と数字の対応表が出てくると思います。

これらを暗記する必要は全くないですが、文字はint型の整数でも表現できるということは覚えておいてください。

int retu = 0;int gyou = 0;

見ての通りですが、現在何行目の何列目を読んでいるのかを記録します。

セル一つ読み終わるごとにgyouが加算され、1行読み終わったらretuを増やしてgyouをリセットします。

memset(buf,0,sizeof(buf));

新しい関数です。memsetは、1番目の引数で指定した配列の中身を2番目の引数で埋めることができます。3番目の引数は、配列のサイズを入力します。

sizeof()という関数は引数に指定した要素のメモリ上のサイズを返してくれるので、これをそのままmemsetの引数に埋め込んでいます。

つまりmemsetはbufの中身をリセットしているんです。いちいちfor文で指定するより、こっちの方が簡潔ですし、間違いも少ないでしょう。

if(c == EOF){

EOFという言葉が出てきました。

これはテキストファイルの末尾に隠されているキーワードです。

「ここでファイルが終わったよ」という意味で、EndOfFileの略です。そのままですね。

cがEOFだった場合、そこでファイルが終わっているのですから読み込みも終了、つまり二重のwhile文を抜けなければなりません。

一重なら普通にbreakをすればよいのですが、2重の場合はこのようにbool型のフラグを使ってやると便利です。

内側のwhile文を抜けた直後に、このフラグがtrueだったらさらにbreakするように処理しています。

strcat(buf,(const char*)&c);

strcatは1番目の引数(char型の配列)に2番目の引数(同じく)を連結させます。

fgetcは数字が複数桁に及んだ場合も最初の1文字しか読んでくれないので、「,」か「\n」でセルが終了するまでは文字をつなげて読み込んでくれないと困ります。

……まあ今回のcsvファイルはすべて1ケタなんですけどね。10以上の番号のマップチップを表示させたいときに、この処理を入れておかないと正しく読んでくれません。

int num = atoi(buf);

また新しい関数です。でも今度は単純。

atoiは引数の文字列をint型に変換してくれます。使える場所は限られてきますが、今回は適材適所ですね。

cell[gyou][retu].gh = chipgh[num];

ここが今回一番メインの処理です。事前にLoadExtendGraphで読み込んだchipghのnum番目のグラフィックハンドルを、構造体cellの二次元配列の、現在読み込んでいる場所のghに代入してやっています。

あとはView関数のDrawExtendGraphでしかるべき場所に描画してやれば、csvファイルで指定した通りにマップチップが並べられるはずです。

今回はここまで。次回はこの内容を応用して、一つのセルに複数の情報を記録して扱ってみます。