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

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

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

書き手:肥田野

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

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

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

今回はこれまでに解説したことを応用して、マップを3レイヤーから合成してみます。

まずマップをレイヤー分けすることによるメリットですが、これまでは1枚絵の上をプレイヤーに歩かせていたので、何となくリアリティに欠けていました。

複数のレイヤーに分けることで、草原のマス目の上に木を生やしたり、道路に被さる位置に街灯を設置したりできるようになります。

レイヤー数はモデルにさせていただいたWOLFRPGエディターに習って3枚にしていますが、好みに応じて何枚でも作ることができます。そこが自分で作る強みですね。

更に今回は、csvにもう一つ情報を追加して、プレイヤーの上に表示させるか下に表示させるかを選択できるようにしました。

マップは斜めからの俯瞰視点になっているので、木や建物の裏側に回ったときはプレイヤーの画像が隠れないと不自然です。

そう言った裏から回れる所を通行不可にしてしまうのも手段の一つですが、パッと見では分からないところにアイテムが落ちていたり、物陰に隠れてエンカウントをやり過ごせたり出来た方が楽しいですよね。

というわけで今回のコードです。

《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;
    int drawMode;//0:プレイヤーより奥 1:プレイヤーより手前
};

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

    Map(char* add){
        LoadDivGraph("BaseChip.png",(128/16)*(1248/16),128/16,1248/16,16,16,chipgh);
        
        FILE* fp;//ファイルのポインタを宣言
        fp = fopen(add,"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 != ','){
                    strcat(buf,(const char*)&c);
                }else{
                    int num = atoi(buf);
                    cell[retu][gyou].canwalk = num;
                    memset(buf,0,sizeof(buf));
                    break;
                }
            }
            while(1){
                c = fgetc(fp);
                if(c != ';' && c != '\n'){
                    strcat(buf,(const char*)&c);
                }else{
                    int num = atoi(buf);
                    cell[retu][gyou].drawMode = num;
                    memset(buf,0,sizeof(buf));
                    break;
                }
            }
            //1セル分のループを抜けたら
            if(c == ';'){
                retu++;
            }
            if(c == '\n'){//改行だったら行を増やす
                gyou++;
                retu = 0;
            }
        }
        fclose(fp);
    }

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

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

    void All(){
        
    }
};

int drawMode;

構造体Cellに要素を追加しました。

こういう風に後から要素を付け足していけるのも構造体やクラスの強みですね。

Map(char* add){

引数にchar*型の変数を追加しました。addはアドレスの略です。

レイヤーに分けるということは、その文csvファイルも増えるということです。Exelファイルでシートごとに編集できれば便利なのですが私の技量不足です……orz

なのでMapを実体化させる時に、使用するcsvファイルのアドレスを引数に指定してやることで管理しやすくしています。

察しの良い方は気づいたと思いますが、アドレスをわざわざ引数で動的に指定するということは、Mapクラスからオブジェクトを複数実体化させるということですよ。

今回使ったcsvはこちらになります。使っているマップチップ画像によって番号は変わるので、参考までに。

《flame1.csvf:id:NUT_SoftwareDevelopper:20150518122500j:plain 《flame2.csvf:id:NUT_SoftwareDevelopper:20150518122510j:plain 《flame3.csvf:id:NUT_SoftwareDevelopper:20150518122522j:plain

drawModeの追加は前回と同じ処理なので説明を省きます。

void FrontView(){

今まではView関数で表示していたのですが、プレイヤーの手前と奥に振り分ける為には、View関数を分けるのが手っ取り早いでしょう。

FrontViewとBackViewで基本的な処理は変わりません。for文とif文で全てのマスのDrawModeを調べ、それぞれ0か1かで表示するしないを振り分けています。

もちろん、今までのようにAll関数で一気に実行しては意味がありません。DXライブラリはどんどん上に画像を塗り重ねていくので、BackView→pl->View→FrontViewの順番で処理しなければなりません。

ですが、この先常に実行したい処理を増やすかもしれないので、空になったAll関数は一応残しておきましょう。

《Control.h》

#pragma once
#include<DxLib.h>
#include"Map.h"
#include"Player.h"

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

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

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

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

だんだん処理が増えてきてMain.cppが煩雑化してきたので、Controlクラスという物を作ってしまいました。

基本的な処理は全てここで行い、Main.cppはこのControlクラスを実体化させるだけにします。

WinMainのあるコードファイルは極力簡潔に、読みやすくしたいところです。

Map* map[3];

レイヤーが増える=オブジェクトを増やすという話は先述しましたが、やはりここは配列で管理した方が分かりやすいでしょう。

void All(){

ここが描画を振り分けている箇所です。mapのオブジェクトが配列なので、描画もfor文で一発です。

あ、コンストラクタの引数に使うcsvファイルは、下のレイヤーから順番に指定してくださいね。

《Player.h》

#pragma once
#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[0]->cell[x/CELL_WIDTH][y/CELL_HEIGHT+1].canwalk&&
                    map[1]->cell[x/CELL_WIDTH][y/CELL_HEIGHT+1].canwalk&&
                    map[2]->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[0]->cell[x/CELL_WIDTH-1][y/CELL_HEIGHT].canwalk&&
                    map[1]->cell[x/CELL_WIDTH-1][y/CELL_HEIGHT].canwalk&&
                    map[2]->cell[x/CELL_WIDTH-1][y/CELL_HEIGHT].canwalk)targetX-=CELL_WIDTH;
            }else if(CheckHitKey(KEY_INPUT_RIGHT)){
                walkFlag = true;
                walkVec = 6;
                if(map[0]->cell[x/CELL_WIDTH+1][y/CELL_HEIGHT].canwalk&&
                    map[1]->cell[x/CELL_WIDTH+1][y/CELL_HEIGHT].canwalk&&
                    map[2]->cell[x/CELL_WIDTH+1][y/CELL_HEIGHT].canwalk)targetX+=CELL_WIDTH;
            }else if(CheckHitKey(KEY_INPUT_UP)){
                walkFlag = true;
                walkVec = 8;
                if(map[0]->cell[x/CELL_WIDTH][y/CELL_HEIGHT-1].canwalk&&
                    map[1]->cell[x/CELL_WIDTH][y/CELL_HEIGHT-1].canwalk&&
                    map[2]->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();
    }
};

void Move(Map* map[]){

mapが配列になったので、引数も配列で指定させます。

このブログでポインタの配列を引数にしたのは初めてかもしれませんね。要素の数は無視されるのでこのような書き方になります。

同じようにAll関数の引数も修正しています。

if(map[0]->cell[x/CELL_WIDTH][y/CELL_HEIGHT+1].canwalk && map[1]->cell[x/CELL_WIDTH][y/CELL_HEIGHT+1].canwalk && map[2]->cell[x/CELL_WIDTH][y/CELL_HEIGHT+1].canwalk)targetY+=CELL_HEIGHT;//targetYを1マス分移動

条件式が凄い長さになっていますが、前回1レイヤーだけ識別していたのを3つに増やしただけですね。

4方向同じように条件を増やします。

《Main.cpp》

#include"DxLib.h"
#include"Control.h"

int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance,
                        LPSTR lpCmdLine, int nCmdShow ){
    ChangeWindowMode(TRUE);
    if( DxLib_Init() == -1 )return -1 ;
    SetDrawScreen(DX_SCREEN_BACK);
    
    Control* cont = new Control();
    
    while(ProcessMessage() != -1 && !ScreenFlip() && !ClearDrawScreen()){
        int startTime = GetNowCount();
        
        cont->All();
        
        if(CheckHitKey(KEY_INPUT_ESCAPE) == 1)break;
        int endTime = GetNowCount();
        WaitTimer((1000/60)-(endTime-startTime));
    }

    delete cont;
    DxLib_End();

    return 0 ;
}

Controlクラスによってより完結になったMain.cppです。

実体化させ、All関数を使って、deleteで消去です。

マップやプレイヤーのdeleteはControlクラスのデストラクタで実行しているので、ここに書く必要はありません。

さて、無事にレイヤーの分割ができたでしょうか。

次回は画面をはみ出すサイズのマップ編集について解説する予定です。