読者です 読者をやめる 読者になる 読者になる

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

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

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

書き手:肥田野

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

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

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

前回はウィンドウのクラスを定義したので、今回はそれを拡張して、メッセージウィンドウのクラスを定義します。

このブログではこれまで扱ってきませんでしたが、クラスのとても便利な(最もと言っても過言ではない)機能に、「継承」というものがあります。

あるクラス(今回はWakuクラス)を基底クラスとし、派生クラス(これから作るMessageWindowクラス)を作ることができます。

派生クラスは基底クラスの機能をそのまま継承しているので、今回の場合はMessageWindowクラスで新たに座標や枠のサイズを定義しなおす必要はありません。

まずはコードを見てみましょう。

≪WindowBox.h≫

#pragma once
#include"Function.h"

class Waku{
public:
    int x,y,width,height,gh[9];
    bool live;
    
    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 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);
    }
};

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

    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 Reset(){
        live = true;
        page = gyou = 0;
    }
    
    void Read(){
        if(PushKey(KEY_INPUT_SPACE)){
            if(gyou < 3 && txt[page][gyou+1][0] != '\0'){
                gyou++;
            }else{
                page++;
                gyou = 0;
            }
            if(page >= 9 || 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]);
        }
    }
    void All(){
        if(live){
            View();
            Read();
        }
    }
};

MessageWindowクラスのAll関数を見ると、MessageWindowクラスで定義していないView関数が呼び出せていますね。

これはMessageWindowクラスの基底クラスにあたるWakuクラスで定義しているので、そこから継承されたMessageWindowクラスもView関数を持っていると言えるのです。

ちなみに現段階では派生クラスが1つしか無いのであまりありがたみを感じませんが、メニュー画面やステータス画面など、ウィンドウに表示させる項目はまだたくさんありますよね。

そういったクラスを定義する時にも、Wakuクラスの派生クラスにしてやればいいんです。

まあそれはもっと先の話になるので、とりあえず解説に移りましょう。

class MessageWindow:public Waku{

クラスを継承する時は、「クラス名:継承方法 基底クラス名」と宣言します。

継承方法はいくつかありますが、今はpublicにしておくのが間違いないでしょう。

char txt[10][4][40];

char型の三次元配列です。二次元配列をイメージする時は方眼紙を連想する人が多いかと思いますが、この場合は樹形図を想像するとわかりやすいです。

一つのメッセージの塊には10ページを用意して、各ページには4行、各行には20文字(日本語は2バイトで1文字なので)を入れられるようにしました。

下記が今回使うテキストファイルです。普通にメモ帳で作って大丈夫ですよ。

≪立札.txt≫

これは立札です。
立札にはお得な情報が書かれているので、
小まめに調べてみましょう!;
この世界にはまだ物が少ないですね。
これからどんどん
賑やかにしていきましょう!

パッと見ただの文章ですが、「小まめに調べてみましょう!」の後に「;」がついていますね。

これをページ切り替えの目印として処理することにします。

MessageWindow(char* add):Waku(0,360,640,120){

基底クラスのコンストラクタに引数が設定されている場合、派生クラスのコンストラクタでこのように引数を設定してやる必要があります。

基底クラスと派生クラス、どちらにもコンストラクタがありまして、デストラクタも存在します。これらがどの順番で実行されるかというと、

基底クラスのコンストラクタ→派生クラスのコンストラクタ→派生クラスのデストラクタ→基底クラスのデストラクタ

という順序になります。

あとでControlクラスでこのMessageWindowクラスを実体化させますが、その時にまずWakuクラスのコンストラクタが先に実行されるため、こうして引数を指定してやる必要があるんです。

数値は画面下にいい感じのサイズで収まるようになっています。お好みで変更しても大丈夫ですよ。

そしてchar* addは派生クラスのコンストラクタの引数ですが、実体化する時はここに「立札.txt」のアドレスを指定してやります。

ファイル操作はもう何度もやっているので解説を省きます。「;」と「\n」で文の区切りを判断していることに注意してください。

void Reset(){

最終的に、このメッセージウィンドウは立札を調べた時に実行してほしいわけですが、ゲームでは何度も話しかけることができますよね。

そのたびにgyouやpageの値をリセットしてやらないと、文章が書かれていないところを参照したり、配列の範囲から飛び出してしまいます。

それを防ぐのがこの関数の役割ですね。

All関数でbool型のメンバ変数liveがtrueの時だけウィンドウを表示させるようにしているので、そのliveもここで切り替えています。

void Read(){

これが実際に読み進める関数です。前回作ったPushKey関数が役に立ちますね。

if(gyou < 3 && txt[page][gyou+1][0] != '\0'){

スペースキーが押されたとき、gyouが3より小さいとき(行数は0から数えて3が最大なので)かつ、次の行の1文字目が「'\0'(ヌル文字)」、つまり文字が入れられていない状態でなければgyouを増やして読み進めます。

この条件を破ったら、次のページに移ります。

if(page > 8 || txt[page][0][0] == '\0'){

ページの最大数9に達するか、次のページの1行目の1文字目がヌル文字だったらそこで文章が終わりなので、liveをfalseにして表示を消します。

あとはfor文でpageとgyouの指定する範囲を描画するだけです。

≪Control.h≫

#pragma once
#include"Map.h"
#include"Player.h"

class Control{
public:
    Player* pl;
    Map* map[3];
    int camX,camY;
    MessageWindow* msg;

    Control(){
        camX = camY = 0;
        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;

        msg = new MessageWindow("txt\\立札.txt");
        msg->Reset();
    }

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

    void All(){
        if(!CheckHitKeyAll())hitAnyKey = false;
        
        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();
        msg->All();
    }
};

今回も、テキストファイルに打ち込んだ文章が表示されるかテストをするだけなので、Controlクラスで付け焼刃的に実体化させています。

f:id:NUT_SoftwareDevelopper:20150527200648j:plain

今回はここまでですね。

次回は全体の構成を整えて、メッセージウィンドウをしかるべき場所に設置してみます。

かなり重い内容になると予想されるので、今回までのコードをよく理解しておいてくださいね。