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

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

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

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

書き手:肥田野

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

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

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

何故、更新が遅れたのか。そして何故、第11回はいつまでたっても終わらないのか。

全ての謎は、⑪(前)でプロジェクトの構成を一新したときに、致命的な不具合が発生していたことに起因します。

こちらが該当箇所になります。

≪define.h≫

#pragma once

#define WALKTIME 15

#define CELL_WIDTH 40
#define CELL_HEIGHT 40
#define CELL_NUM_X 21
#define CELL_NUM_Y 17
#define WINDOW_Y 480
#define WINDOW_X 640

static int viewX = 0;//ココ!
static int viewY = 0;//こっちも
static bool hitAnyKey = false;//一応ここも

既に⑪(前)を試した方は気づいたかもしれませんが、プレイヤーが移動をしてもカメラが追随しなくなっていました

ControlクラスからviewX、viewYを更新しても、どこかのタイミングで0に戻されているように思えました。

そして、C++の常識的に、複数のファイルをまたぐグローバル変数は推奨されていません。

その理由がまさに、こういう不具合の温床になるからなんですね。

ではこれらの変数はどこで宣言するのかといいますと、Function.cppの関数内にします。

《Function.cpp》

#include"Function.h"

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

void CameraDraw(int x1,int y1,int x2,int y2,int gh,int trans,int changeVX,int changeVY){
    static int viewX;
    static int viewY;
    if(changeVX != 0)viewX = changeVX;
    if(changeVY != 0)viewY = changeVY;
    DrawExtendGraph(x1-viewX,y1-viewY,x2-viewX,y2-viewY,gh,trans);
}

bool PushKey(int keyCode){
    static bool hitAnyKey;
    if(CheckHitKey(keyCode)){
        if(!hitAnyKey){
            hitAnyKey = true;
            return true;
        }
    }else{
        hitAnyKey = false;
    }
    return false;
}

関数の中で変数を宣言すると、通常は関数の処理が終わると同時に破棄されてしまいます。

それを破棄させずにずっと使えるようにするのが、修飾子staticです。

static int viewX;と書いた場合、メモリ領域にviewXのスペースが確保され、プログラムが終了するまで解放されずに残ります。

もちろん、関数が2回3回と呼び出されても、上書きして宣言し直されることはありませんよ。

これでカメラ位置保存用の変数viewX,viewYが出来上がりました。

この値を更新するために、CameraDraw関数に二つ引数を追加しています。changeVXとchangeVYです。

ここで「えっ」と思った方は鋭いですね。

CameraDrawに引き数を増やしたということは、CharaクラスやMapクラスで呼び出している部分も全て書き換えなければならないのかと。

意外にも答えはNOなんです。

(こういう事態に備えているわけではないと思いますが)C++にはとても便利な機能が標準装備されているんです。

CameraDrawをプロトタイプ宣言しているFunction.hを見てみましょう。

《Function.h》

#pragma once

void CameraDraw(int,int,int,int,int,int,
    int changeVX = 0,int changeVY = 0);

bool PushKey(int);

なんと、引数のあとに直接値を代入していますね。

このブログでは初めて扱う処理になります。これはデフォルト引数というもので、関数の呼び出し時に引数が指定された場合はその値を、指定されなかった場合はデフォルトで定めた値を引数として利用します。

つまり、前回までControlクラスのAll関数で行っていた「プレイヤーが画面真ん中より右に移動しようとしたら、viewXの値を増やしてカメラに追いかけさせる」という処理を、PlayerクラスのView関数にデフォルト引数を指定することで直接行うことができるということです。

もう少し言い換えると、これまでControlクラスがプレイヤーキャラの動きを監視して、カメラを移動させる必要があったらグローバル変数を書き換えることでCameraDraw関数に指示していたのを、今回からプレイヤーキャラ自身が画面のどの辺りにいるのかを判断し、必要に応じてカメラに「○○ピクセルずれたから追いかけてきて」と支持できるようになったということです。

なお、今のように宣言部と実装部に関数が分かれている場合、デフォルト引数の「=○○」はどちらか片方でしか指定できません。

両方に書いたらどちらを取ればいいのか分からなくなりますしね。

また、普通の引数とデフォルト引数が混在している場合、デフォルト引数は右の方にまとめて書くというルールもあります。

if(changeVX != 0)viewX = changeVX;

気をつけなければいけないのがこの一文で、viewXを常に更新してしまうと、Mapオブジェクトがこの関数を呼び出した時に0で上書きされてしまい、本末転倒になります。

viewXを書き換えるのはchangeVXに値が指定された時だけにする必要があるということですね。

あとついでですが、PushKeyに使っていたグローバル変数hitAnyKeyも、PushKeyのstatic変数にしています。

それでは、今回の変更に従って修正された部分を列挙します。

まずは引数を追加しなければならないPlayer.cppから。

《Player.cpp》

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

Chara::Chara(int setX,int setY,char* add){
    x = targetX = setX*CELL_WIDTH;
    y = targetY = setY*CELL_HEIGHT;
    LoadDivGraph(add,12,3,4,20,28,gh);
    GetGraphSize(gh[0],&width,&height);
    walkVec = 2;
    walkFlag = false;
    animCount = 0;
    speed = 2;
}

void Chara::AnimationView(int animState,int firstNum,int cMoveX,int cMoveY){//ここにもデフォルト引数を追加
    if(animState == 0)CameraDraw(x,y-2*(height-width),x+CELL_WIDTH,y+CELL_WIDTH,gh[firstNum],TRUE,cMoveX,cMoveY);
    if(animState == 1)CameraDraw(x,y-2*(height-width),x+CELL_WIDTH,y+CELL_WIDTH,gh[firstNum+1],TRUE,cMoveX,cMoveY);
    if(animState == 2)CameraDraw(x,y-2*(height-width),x+CELL_WIDTH,y+CELL_WIDTH,gh[firstNum+2],TRUE,cMoveX,cMoveY);
    if(animState == 3)CameraDraw(x,y-2*(height-width),x+CELL_WIDTH,y+CELL_WIDTH,gh[firstNum+1],TRUE,cMoveX,cMoveY);
}

void Chara::View(){
    //変更なし
}

void Chara::Move(Map* map[],int walkSwitch){
    //変更なし
}

void Chara::All(){
    //変更なし
}

Player::Player(char* add):Chara(1,1,add){
    //変更なし
}

void Player::Action(EventMap* eMap){
    //変更なし
}

//↓View関数をオーバーライド(後述)
void Player::View(Map* map[]){//マップ情報が必要なので引数を追加

    int cMoveX = 0,cMoveY = 0;//まずローカル変数を定義

    if(x>WINDOW_X/2 && x<map[0]->width-WINDOW_X/2+CELL_WIDTH){//Controlクラスから移植した処理
        cMoveX = x-WINDOW_X/2;//この変数をデフォルト引数に指定する
    }
    if(y>WINDOW_Y/2 && y<map[0]->height-WINDOW_Y/2){
        cMoveY = y-WINDOW_Y/2;
    }

    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,cMoveX,cMoveY);
        if(walkVec == 4)AnimationView(animState,3,cMoveX,cMoveY);
        if(walkVec == 6)AnimationView(animState,6,cMoveX,cMoveY);
        if(walkVec == 8)AnimationView(animState,9,cMoveX,cMoveY);
        animCount++;
    }else{
        if(walkVec == 2)CameraDraw(x,y-2*(height-width),x+CELL_WIDTH,y+CELL_WIDTH,gh[1],TRUE,cMoveX,cMoveY);
        if(walkVec == 4)CameraDraw(x,y-2*(height-width),x+CELL_WIDTH,y+CELL_WIDTH,gh[4],TRUE,cMoveX,cMoveY);
        if(walkVec == 6)CameraDraw(x,y-2*(height-width),x+CELL_WIDTH,y+CELL_WIDTH,gh[7],TRUE,cMoveX,cMoveY);
        if(walkVec == 8)CameraDraw(x,y-2*(height-width),x+CELL_WIDTH,y+CELL_WIDTH,gh[10],TRUE,cMoveX,cMoveY);
    }
}

void Player::All(Map* map[],EventMap* eMap){
    int walkSwitch = 0;
    if(CheckHitKey(KEY_INPUT_DOWN ))walkSwitch = 2;
    if(CheckHitKey(KEY_INPUT_UP   ))walkSwitch = 8;
    if(CheckHitKey(KEY_INPUT_RIGHT))walkSwitch = 6;
    if(CheckHitKey(KEY_INPUT_LEFT ))walkSwitch = 4;
    
    Action(eMap);
    Move(map,walkSwitch);
    View(map);//引数を入れるのを忘れずに
}

実は前回解説しないでさらっと流してしまったのですが、派生したクラス(この場合はPlayer)が基底クラスと同じ名前の関数を定義した場合、player->Viewと呼び出した場合、派生クラスの関数が実行されます。

これを関数のオーバーライドといい、クラスを継承する場面では非常によく使われます。

また、似たような言葉に「オーバーロード」というものがありますが、これは全く異なる物ですので区別してください。派生したクラスが上書きをするのでオーバー「ライド(乗る)」ですよ。

次に、Player.hも上げておきます。

デフォルト引数が追加されているので確認してください。

《Player.h》

#pragma once
#include<DxLib.h>

class Map;
class EventMap;
class MessageWindow;

class Chara{

public:
    int gh[12];
    int width,height;
    int walkVec;
    int animCount;
    bool walkFlag;
    int x,y;//プレイヤーが実際に存在する座標
    int targetX,targetY;//プレイヤーが次に向かうべき座標
    int speed;
    
    Chara(int setX,int setY,char* add);

    void AnimationView(int animState,int firstNum,int cMoveX = 0,int cMoveY = 0);//デフォルト引数を追加

    void View();
    
    void Move(Map* map[],int walkSwitch);

    void All();
};

class Player:public Chara{
public:
    Player(char* add);

    void Action(EventMap* eMap);

    void View(Map* map[]);//引数追加

    void All(Map* map[],EventMap* eMap);
};

また、Control.cppにあった処理もなくして、よりスッキリしました。

《Control.cpp》

#include"Control.h"
#include"Player.h"
#include"define.h"
#include<DxLib.h>
#include"Map.h"

Control::Control(){
    //変更なし
}

Control::~Control(){
    //変更なし
}

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

そして最後に、忘れてはいけないのがdefine.hでしたね。

グローバル変数がなくなって本当にマクロ定義だけになりました。

《define.h》

#pragma once

#define WALKTIME 15

#define CELL_WIDTH 40
#define CELL_HEIGHT 40
#define CELL_NUM_X 21
#define CELL_NUM_Y 17
#define WINDOW_Y 480
#define WINDOW_X 640

さて、これで無事にカメラがプレイヤーを追ってくれるようになったと思います。

次回はついに村人の実装です。

こちらは既にソースコードが出来上がっているので、更新まではそう時間はかからないと思います。