【ゲーム製作入門】C/C++で簡単なRPGを作る⑬【DXライブラリ】
書き手:肥田野
DXライブラリでRPGのベースになるマップ画面やデータ管理などのあれこれの制作に挑戦してみます。
RPGとして完成する保証はありませんが、途中経過だけでも参考になれば幸いです。
この連載は前回までの内容を理解していることを前提に進めていきます。
今回はパーティメンバーの追加方法について解説します。
これまではプレイヤーが一人しかいませんでしたが、一般的なRPGでは複数人でパーティを組むことが多いですよね。
そして、マップ上でもパーティメンバーが後に着いてくることが多いと思います。
またパーティの概念がないゲームでも、チュートリアルなどで誰かの後についてトコトコ歩いていく場面は想像しやすいでしょう。今回は、そんな処理を実装してみます。
パーティの概要としては、新たにPartyクラスを作成し、そのメンバにPlayerクラスのポインタを複数持たせます。
パーティメンバーは動的に増えたり減ったりするので、普通の配列よりvectorを使ったほうが便利そうですね。
それではPlayer.hに新たなクラスを追加しましょう。
《Party.h》
#pragma once #include<vector> using namespace std; class Map; class EventMap; class MessageWindow; class Status; class Chara{ //変更なし }; class Player:public Chara{ public: Player(char* add); void Action(EventMap* eMap); void View(int cMoveX = 0,int cMoveY = 0); }; class Murabito:public Chara{ //変更なし }; class Party{ public: vector<Player*>party; EventMap* eMap; Map** map; Party(EventMap* setEMap,Map* setMap[]); ~Party(); void Insert(int num,Player* addition); void Erase(int num); void Move(); void View(); };
二人目以降のパーティメンバーはそれ専用にクラスを作るという手もあったのですが、CharaクラスとPlayerクラスを少し修正するだけで大丈夫だったのでそのようにしました。
Player::All関数を消去して、View関数をCharaクラスからオーバーライドさせています。
復習ですが、オーバーライドとは「クラスを継承したとき、子クラスが親クラスのメソッドを同名で再定義すること」ですよ。
こうすることでPlayerポインタ型の変数からView関数を呼び出したとき、オーバーライドさせた関数が実行されるようになります。
PartyクラスはPlayerポインタ型のベクタと、何かと情報をやりとりすることが多いのでEventMapクラスとMapクラスのポインタも持たせました。
Insert関数とErase関数は、vectorに同名の関数があるのですが、これをパーティで正しく動作させるために拡張しています。
それでは実行部を見ていきましょう。
《Player.cpp》
#include"Player.h" #include<DxLib.h> #include"Function.h" #include"Map.h" #include"WindowBox.h" #include"Event.h" #include"WindowBox.h" #include"define.h" //Charaクラスは変更なし Player::Player(char* add):Chara(1,1,add){ walkVec = 2; } void Player::Action(EventMap* eMap){ //変更なし } void Player::View(int cMoveX,int cMoveY){ 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); } } //Murabitoクラスも変更なし Party::Party(EventMap* setEMap,Map* setMap[]){ eMap = setEMap; map = setMap; Insert(0,new Player("graphic\\Player.png")); Insert(1,new Player("graphic\\PartyMember1.png")); Insert(2,new Player("graphic\\PartyMember2.png")); Insert(3,new Player("graphic\\PartyMember3.png")); Insert(4,new Player("graphic\\PartyMember4.png")); } Party::~Party(){ for(int i=party.size()-1;i>-1;i--){ delete party[i]; party.erase(party.begin()+party.size()-1); } } void Party::Insert(int num,Player* addition){ if(num>party.size())return; party.insert(party.begin()+num,addition); if(num > 0){ for(int i=num;i<party.size();i++){ party[i]->x = party[i]->targetX = party[i-1]->x; party[i]->y = party[i]->targetY = party[i-1]->y; } } } void Party::Erase(int num){ if(num>=party.size())return; map[0]->cell[party[num]->x/CELL_WIDTH][party[num]->y/CELL_HEIGHT].canwalk = true; delete party[num]; party.erase(party.begin()+num); for(int i=num;i<party.size();i++){ int walkSwitch = 0; if(abs(party[i]->y - party[i-1]->y)<CELL_HEIGHT){ if(party[i]->x<party[i-1]->x)walkSwitch = 6; if(party[i]->x>party[i-1]->x)walkSwitch = 4; }else if(abs(party[i]->x - party[i-1]->x)<CELL_WIDTH){ if(party[i]->y<party[i-1]->y)walkSwitch = 2; if(party[i]->y>party[i-1]->y)walkSwitch = 8; } party[i]->walkFlag = party[0]->walkFlag; if(party[i]->targetX/CELL_WIDTH != party[i-1]->targetX/CELL_WIDTH|| party[i]->targetY/CELL_HEIGHT != party[i-1]->targetY/CELL_HEIGHT){ party[i]->Move(map,walkSwitch); } } for(int i=0;i<party.size();i++) map[0]->cell[party[i]->x/CELL_WIDTH][party[i]->y/CELL_HEIGHT].canwalk = false; } void Party::Move(){ for(int i=0;i<party.size();i++) map[0]->cell[party[i]->x/CELL_WIDTH][party[i]->y/CELL_HEIGHT].canwalk = true; 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; party[0]->Move(map,walkSwitch); party[0]->Action(eMap); for(int i=1;i<party.size();i++){ int walkSwitch = 0; if(CheckHitKey(KEY_INPUT_DOWN ) || CheckHitKey(KEY_INPUT_UP )|| CheckHitKey(KEY_INPUT_RIGHT)|| CheckHitKey(KEY_INPUT_LEFT )){ if(party[0]->x!=party[0]->targetX || party[0]->y != party[0]->targetY){ if(abs(party[i]->y - party[i-1]->y)<CELL_HEIGHT){ if(party[i]->x<party[i-1]->x)walkSwitch = 6; if(party[i]->x>party[i-1]->x)walkSwitch = 4; }else if(abs(party[i]->x - party[i-1]->x)<CELL_WIDTH){ if(party[i]->y<party[i-1]->y)walkSwitch = 2; if(party[i]->y>party[i-1]->y)walkSwitch = 8; } } } party[i]->walkFlag = party[0]->walkFlag; if(party[i]->targetX/CELL_WIDTH != party[i-1]->targetX/CELL_WIDTH|| party[i]->targetY/CELL_HEIGHT != party[i-1]->targetY/CELL_HEIGHT){ party[i]->Move(map,walkSwitch); } } for(int i=0;i<party.size();i++) map[0]->cell[party[i]->x/CELL_WIDTH][party[i]->y/CELL_HEIGHT].canwalk = false; } void Party::View(){ int cMoveX = 0,cMoveY = 0; if(party[0]->x>WINDOW_X/2 && party[0]->x<map[0]->width-WINDOW_X/2+CELL_WIDTH){ cMoveX = party[0]->x-WINDOW_X/2; } if(party[0]->y>WINDOW_Y/2 && party[0]->y<map[0]->height-WINDOW_Y/2){ cMoveY = party[0]->y-WINDOW_Y/2; } for(int i=party.size()-1;i>=0;i--){ if(i==0)party[i]->View(cMoveX,cMoveY); else party[i]->View(); } }
まずはPartyクラスのコンストラクタから見ていきましょう。
引数で指定したイベントマップとマップのポインタを自身のメンバにコピーした後、Insert関数を5回呼び出しています。
Insert関数はパーティメンバーを追加する役割を担っています。引数には列の何番目に挿入するかと、挿入するPlayerクラスのポインタが必要です。
二つ目の引数に直接new演算子でPlayerクラスを実体化させていますが、このnew+コンストラクタを一つの関数と見立てた場合、返り値にアドレスを吐き出すのでこういう省略が可能になっています。
どうしても気になる人は、
Player *p; p = new Player("引数1"); Insert(0,p); delete p; p = new Player("引数2"); Insert(1,p); ・ ・ ・
と書いても大丈夫だと思います。
また、画像はこんなものを用意しました。例によって自分で作ってもOKです。
ではInsert関数の処理を確認します。
if(num>party.size())return;
当然ながら、引数numに指定した数がpartyの要素の数を上回っていてはエラーになってしまうので、ここで保険をかけています。
実際にゲーム中に呼び出す時も、partyのサイズを確認するなどして極力この条件を満たさないように注意しましょう。
party.insert(party.begin()+num,addition);
これがvectorが持っている関数です。自作のInsert関数の心臓部とも言えますね。
insert関数は、配列の要素を新たに挿入するときに使う関数ですが、パーティメンバーを追加するときにinsertを直接使ってしまうと、後述する座標の指定ができなかったり、上記で防いでいるような配列の要素を超えてしまうことが起こります。
なお、Charaクラスのコンストラクタでは初期座標を1×1の場所にしているので、Insert関数で追加するときは、一つ前にいる人の座標に修正してやる必要があります。
順番が前後しますが、次はParty::Moveを見てみましょう。
Party::Moveの仕組みを大雑把に説明すると「先頭だけキー操作で動かして、後続はひとつ前の人を追跡している」感じです。
単純にパーティメンバーの数だけChara::Moveを使うのでは正しく動かないので、ちょっと複雑な処理をさせることになってしまいました。
私もこの関数を作るのに何日も頭を悩ませていたので、もしすぐに理解できなくても安心して悩んでくださいね(笑)。もちろんサークルに所属している方は気軽に対面で質問してください。
for(int i=0;i<party.size();i++)map[0]->cell[party[i]->x/CELL_WIDTH][party[i]->y/CELL_HEIGHT].canwalk = true;
まず自分が立っている場所のcanwalkをtrueにします。
こうしないと、自分の後ろにいる人が前に進もうとした時、前の人が立ちふさがっていることになって進めなくなるからです。
勿論このまま放置すると村人やその他のイベントなどがキャラの上に被さってきてしまいますから、関数の終わりでcanwalkをfalseにしています。
次にローカル変数walkSwitchを宣言して、キーが押されていたらそれに対応する方向を代入します。
その値を使ってp0のMove関数を実行します。Action関数も先頭キャラしか使わないのでここで実行させています。
次のfor文はiが1から始まっていることに注意してください。
上下左右のどれかのキーが押されていることを確認して、さらに先頭のキャラが歩行中の時にparty[i]のキャラを歩かせます。
if(abs(party[i]->y - party[i-1]->y)<CELL_HEIGHT){
ちょっとここの処理が言葉にするのが難しいのですが、とりあえずabs()は絶対値のことです。
自分とひとつ前のキャラのy座標の差が1マス分より小さい時、つまり自分とひとつ前のキャラが縦に並んでいない時ですね。
そのときに、今度はひとつ前のキャラのx座標が左右どちらにあるのかを比べてwalkSwitchを決定します。
同様の処理を縦の移動にも行っています。
何故こんな回りくどいことをしているのかというと、これがないと隊列が曲がった時に上手くついて行ってくれないんですよね。気になった人は、上記のif文をコメントアウトして実行し、違いを比べてみてください。
party[i]->walkFlag = party[0]->walkFlag;
View関数で使う歩いているかどうかの情報は、先頭のキャラにお任せすることにしました。
if(party[i]->targetX/CELL_WIDTH != party[i-1]->targetX/CELL_WIDTH||party[i]->targetY/CELL_HEIGHT != party[i-1]->targetY/CELL_HEIGHT){
自分とひとつ前の人のtargetX,Yがずれている時だけ歩かせます。
こうしないと、先頭が立ち止まった時に後続が同じマス目まで歩いてしまって、列が潰れてしまいます。
そして最後にfor文で各キャラの立ち位置を侵入不可にします。これで村人などが隊列に踏み込んでくることはありません。
次にParty::Viewを解説します。
これまでPlayer::Viewで実行していた「カメラを動かす必要があるか調べる(=主人公が画面の半分よりはみ出しているか)」の処理をParty::Viewに移植しました。
そのためPlayer::Viewは引数Map *mapを削除しています。
party[0]の座標に応じてローカル変数cMoveX,cMoveYを設定し、各キャラごとのView関数を実行します。
ここでもparty[0]だけデフォルト変数を使っていますね。
そして最後にParty::Eraseです。
これはInsertの反対で、誰かをパーティから抜けさせる時に実行します。引数は抜けさせたいキャラの番号ですね。
これもInsert同様ただerase関数を使うだけではダメなので、まずnumがベクタの範囲内にあるかを調べ、今度は消すキャラの足元を侵入可能にします。
ベクタにはオブジェクトのポインタが入っているので、ベクタを消す前にdeleteでオブジェクトに使っていたメモリ領域を解放させます。
そしてerase関数でベクタの要素を消去、あとに続く要素を一つずつ前にずらします。
次のfor文は、消したキャラの後ろにいるキャラを一歩歩かせるための処理です。Party::Moveの応用なので、そう難しくはないと思います。
また、デストラクタはPartyに割いたメモリ領域を単純に解放すればいいので、Eraseより処理は単純になっています。
さて、キャラクターの管理がPartyクラスだけでできるようになったので、Controlクラスも修正しましょう。
《Control.h》
#pragma once class Murabito; class Map; class EventMap; class Party; class Control{ public: Party* pParty; Map* map[3]; EventMap* ev; Murabito* m1; Control(); ~Control(); void All(); };
Playerクラスのポインタが消えて、代わりにPartyクラスのポインタが加わりました。
Partyクラスをプロトタイプ宣言するのを忘れないようにしてください。
《Control.cpp》
#include"Control.h" #include"Player.h" #include"define.h" #include<DxLib.h> #include"Map.h" #include"Function.h" Control::Control(){ map[0] = new Map("csv\\flame1.csv"); map[1] = new Map("csv\\flame2.csv"); map[2] = new Map("csv\\flame3.csv"); ev = new EventMap("csv\\event.csv"); pParty = new Party(ev,map); } Control::~Control(){ delete pParty; for(int i=0;i<3;i++)delete map[i]; delete ev; } void Control::All(){ KeyUpdate(); for(int i=0;i<3;i++)map[i]->BackView(); pParty->View(); ev->All(map); for(int i=0;i<3;i++)map[i]->FrontView(); if(PushKey(KEY_INPUT_Q))pParty->Erase(1); pParty->Move(); }
こちらはそう目新しい要素はないですね。しいて言えば
if(PushKey(KEY_INPUT_Q))pParty->Erase(1);
Qキーを入力することで1番(0から始まっているので先頭から2番目)のキャラを消去させています。
これでパーティが追加できました。
今回は主人公含めて5人ですが、勿論10人でも100人でも追加はできるので、自分の好みのバランスになるよういろいろ試してみてください。
次回の内容もまだ未定です。折角パーティができたので、そろそろ簡単にでもステータスの管理をしたいとは思っています。