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

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

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

書き手:肥田野

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

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

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

前回CSVファイルの使い方を解説したので、今回はそれを応用してもう一つ情報を追加します。

追加するのは、そのマスをプレイヤーが歩行可能かどうかという情報です。

まずはMap.hから。

≪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;
    bool canwalk;
};

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);
                if(c == EOF){
                    eofFlag = true;
                    break;
                }
                if(c != ','){//「,」が出てくるまで読み進める
                    strcat(buf,(const char*)&c);
                }else{
                    int num = atoi(buf);
                    cell[retu][gyou].gh = chipgh[num];
                    memset(buf,0,sizeof(buf));
                    break;
                }
            }
            if(eofFlag)break;
            while(1){
                c = fgetc(fp);
                if(c != ';' && c != '\n'){
                    strcat(buf,(const char*)&c);//cがセルの区切りか改行でなければ、bufに連結する
                }else{
                    int num = atoi(buf);
                    cell[retu][gyou].canwalk = num;//そのマスが歩行可能かどうかを記録
                    memset(buf,0,sizeof(buf));
                    break;
                }
            }
            //1セル分のループを抜けたら
            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();
    }
};

bool canwalk;

これが新たに追加した情報です。

true(=1)の時は歩行可能で、false(=0)なら侵入不可です。

情報の追加に従って、CSVは次のように修正します。

f:id:NUT_SoftwareDevelopper:20150515161553p:plain

前回は区切りの文字に「,」を使っていましたが、今回からは「;」をセルの区切りに、「,」は項目の区切りに使います。

≪注意≫Exelではセルの区切りが「,」でしかつけられないという報告がありました(筆者はOpenOffice使用)。その場合、セルの区切りは「,」のままで、項目の区切りを「;」にすると上手くいくかと思います。

そのため、CSVファイルの形式を事前に変更しておく必要があります。表計算ソフトからの変更方法がわからない方は、メモ帳で開いて「Ctrl+h」で「,」を「;」に置換するのが手っ取り早いです。

メモ帳で開くとこんな感じになります。

0,0;0,0;0,0;0,0;0,0;0,0;0,0;0,0;0,0;0,0;0,0;0,0;0,0;0,0;0,0;0,0
0,0;4,1;4,1;4,1;4,1;4,1;4,1;4,1;4,1;4,1;4,1;4,1;4,1;4,1;4,1;0,0
0,0;4,1;4,1;4,1;4,1;4,1;4,1;4,1;4,1;4,1;4,1;4,1;4,1;4,1;4,1;0,0
0,0;4,1;4,1;4,1;7,1;7,1;7,1;7,1;7,1;7,1;7,1;7,1;4,1;4,1;4,1;0,0
0,0;4,1;4,1;4,1;7,1;7,1;7,1;7,1;7,1;7,1;7,1;7,1;4,1;4,1;4,1;0,0
0,0;4,1;4,1;4,1;7,1;7,1;7,1;7,1;7,1;7,1;7,1;7,1;4,1;4,1;4,1;0,0
0,0;4,1;4,1;4,1;7,1;7,1;7,1;7,1;7,1;7,1;7,1;7,1;4,1;4,1;4,1;0,0
0,0;4,1;4,1;4,1;7,1;7,1;7,1;7,1;7,1;7,1;7,1;7,1;4,1;4,1;4,1;0,0
0,0;4,1;4,1;4,1;7,1;7,1;7,1;7,1;7,1;7,1;7,1;7,1;4,1;4,1;4,1;0,0
0,0;4,1;4,1;4,1;4,1;4,1;4,1;4,1;4,1;4,1;4,1;4,1;4,1;4,1;4,1;0,0
0,0;4,1;4,1;4,1;4,1;4,1;4,1;4,1;4,1;4,1;4,1;4,1;4,1;4,1;4,1;0,0
0,0;0,0;0,0;0,0;0,0;0,0;0,0;0,0;0,0;0,0;0,0;0,0;0,0;0,0;0,0;0,0

マップの外周をぐるりと侵入不可(0)で囲んでいます。それではCSVの形式が変わったので、読み込むためのコードも変更していきます。

技術的には前回解説した内容と変わりありませんので、詳細な説明は省略します。

要するに1つ目の内容(グラフィックハンドル)と2つ目の内容(侵入可否)を別々のwhile文で読み込ませているんです。

注意する点は、EOFの処理ぐらいでしょうか。EOFを読み取るのは必ず1番目のwhile文となるので、外側のwhile文と抜ける処理1番目のwhile文の直下におかないといけません。そうしないと、2番目のwhile文で無限ループしてしまいますからね。

さて、各マス目に侵入の可否が設定されたら、今度はプレイヤーにもそれを判定させないといけません。

≪Player.h≫

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

#define WALKTIME 15

class Player{
public:
    int gh[12];
    int width,height;
    int walkVec;//歩く方向。2,4,6,8でテンキーに対応
    int animCount;//マイフレーム1ずつ増やす、アニメーションのためのカウンタ
    bool walkFlag;//歩いているか立ち止まっているかの判定
    int x,y;//グラフィックを描画する座標
    int targetX,targetY;//プレイヤーが次に向かうべき座標。現在位置の隣のマスのどれか
    int speed;
    Player(){
        LoadDivGraph("Chara.png",12,3,4,20,28,gh);
        GetGraphSize(gh[0],&width,&height);
        walkVec = 2;
        walkFlag = false;
        animCount = 0;
        x = 0;
        y = 0;
        speed = 1;
        targetX = 0;
        targetY = 0;
    }

    void AnimationView(int animState,int firstNum){
        if(animState == 0)DrawExtendGraph(x,y-2*(height-width),x+CELL_WIDTH,y+CELL_WIDTH,gh[firstNum],TRUE);
        if(animState == 1)DrawExtendGraph(x,y-2*(height-width),x+CELL_WIDTH,y+CELL_WIDTH,gh[firstNum+1],TRUE);
        if(animState == 2)DrawExtendGraph(x,y-2*(height-width),x+CELL_WIDTH,y+CELL_WIDTH,gh[firstNum+2],TRUE);
        if(animState == 3)DrawExtendGraph(x,y-2*(height-width),x+CELL_WIDTH,y+CELL_WIDTH,gh[firstNum+1],TRUE);
    }

    void Move(Map* map){
        walkFlag = false;//基本はfalse、歩いている時だけ変更する
        
        if(targetX == x && targetY == y){//移動中でなければ
            if(CheckHitKey(KEY_INPUT_DOWN)){
                walkFlag = true;
                walkVec = 2;
                if(map->cell[x/CELL_WIDTH][y/CELL_HEIGHT+1].canwalk)targetY+=CELL_HEIGHT;//targetYを1マス分移動
            }else if(CheckHitKey(KEY_INPUT_LEFT)){
                walkFlag = true;
                walkVec = 4;
                if(map->cell[x/CELL_WIDTH-1][y/CELL_HEIGHT].canwalk)targetX-=CELL_WIDTH;
            }else if(CheckHitKey(KEY_INPUT_RIGHT)){
                walkFlag = true;
                walkVec = 6;
                if(map->cell[x/CELL_WIDTH+1][y/CELL_HEIGHT].canwalk)targetX+=CELL_WIDTH;
            }else if(CheckHitKey(KEY_INPUT_UP)){
                walkFlag = true;
                walkVec = 8;
                if(map->cell[x/CELL_WIDTH][y/CELL_HEIGHT-1].canwalk)targetY-=CELL_HEIGHT;
            }else{
                animCount = 0;//キーが押されておらず、移動が完了していればanimCountをリセット
            }
        }else{
            walkFlag = true;
        }
        
        if(y < targetY)y+=speed;
        if(x > targetX)x-=speed;
        if(x < targetX)x+=speed;
        if(y > targetY)y-=speed;
    }

    void View(){

        int animState = animCount/WALKTIME;//animCountがWALKTIMEの公倍数になるたびにanimStateが1増える

        if(animState == 4){//animStateが4になったらリセット
            animCount = 0;
            animState = 0;
        }
        
        if(walkFlag){
            if(walkVec == 2)AnimationView(animState,0);
            if(walkVec == 4)AnimationView(animState,3);
            if(walkVec == 6)AnimationView(animState,6);
            if(walkVec == 8)AnimationView(animState,9);
            animCount++;
        }else{
            if(walkVec == 2)DrawExtendGraph(x,y-2*(height-width),x+CELL_WIDTH,y+CELL_WIDTH,gh[1],TRUE);
            if(walkVec == 4)DrawExtendGraph(x,y-2*(height-width),x+CELL_WIDTH,y+CELL_WIDTH,gh[4],TRUE);
            if(walkVec == 6)DrawExtendGraph(x,y-2*(height-width),x+CELL_WIDTH,y+CELL_WIDTH,gh[7],TRUE);
            if(walkVec == 8)DrawExtendGraph(x,y-2*(height-width),x+CELL_WIDTH,y+CELL_WIDTH,gh[10],TRUE);
        }
    }

    void All(Map* map){
        Move(map);
        View();
    }
};

if(animState==0)DrawExtendGraph(x,y-(height-width),x+CELL_WIDTH,y+CELL_WIDTH,gh[firstNum],TRUE);

今回の内容とは関係ないのですが、DrawExtendGraphに指定する座標の計算方法を変更しました。

前回までのは実際にプレイヤーが踏んでいるマスよりずれた位置に表示されていたようなので……(-_-;)

歩行していないときのDrawExtendGraphも同様に修正しておきました。

void Move(Map* map){

まず大前提として、マップの情報が必要ならば、それを引数として受け取ってやらなければなりません。

ここで引数にMapのポインタ型が使えるのは、Map.hをインクルードしているからですね。

引数の名前は、Main.cppで定義したポインタの変数名と同じにしています。違っても問題なく動きますが、この場合は同じにしておいたほうが混乱しにくいと考えました。

if(map->cell[x/CELL_WIDTH][y/CELL_HEIGHT+1].canwalk)targetY+=CELL_HEIGHT;

今回対応させたのがこの行です。

これは下に進ませる時の処理なので、配列の二つ目の添え字は+1されます。

同じように残り3方向にも処理を追加してください。

void All(Map* map){

そしてMove関数に引数が加わったので、All関数も同様に引数を追加してやります。

最終的にはWinMainの方でも引数を追加します。というわけで最後はMain.cppです。

≪Main.cpp≫

#include "DxLib.h"
#include "Map.h"
#include "Player.h"

int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance,
                        LPSTR lpCmdLine, int nCmdShow ){
    ChangeWindowMode(TRUE);
    if( DxLib_Init() == -1 )return -1 ;
    SetDrawScreen(DX_SCREEN_BACK);
    
    Player* pl = new Player();
    Map* map = new Map();
    pl->x = pl->targetX = CELL_WIDTH;
    pl->y = pl->targetY = CELL_HEIGHT;
    while(ProcessMessage() != -1 && !ScreenFlip() && !ClearDrawScreen()){
        int startTime = GetNowCount();
        map->All();
        pl->All(map);
        
        if(CheckHitKey(KEY_INPUT_ESCAPE) == 1)break;
        int endTime = GetNowCount();
        WaitTimer((1000/60)-(endTime-startTime));
    }

    delete pl;
    delete map;

    DxLib_End() ;
    return 0 ;
}

pl->x = pl->targetX = CELL_WIDTH;

プレイヤーの初期位置は(0,0)ですが、CSVを見るとここは侵入不可のマス目になっていますね。

一応侵入不可のマスから脱出はできますが、ゲーム上不自然なので1マスずらしています。

pl->All(map);

All関数を呼び出すところにも引数を追加します。まあ変更しないとコンパイルエラーのなるのでここはすぐ理解できると思いますが……

さて、無事に侵入不可の判定が実装できたでしょうか。

次回は……そうですね、実はまだ未定なのですが、マップ生成をフレームごとに分割できるようにしようかなと考えています。