vol.11 ステータス、アイテム関係

目次

はじめに

今回はプレイヤーのステータス関係、及びアイテムに着手していきます。ステータス関係はレベルアップ等の仕様は中々難しそうなので今回は割愛しますが、ゲームとして最低限成立するようにステータスを設定して行けたらと思います。アイテムもダンジョン内にランダムに生成していきましょう。但し、今回はあくまでアイテムのアイコンをダンジョン内に設置するだけで、実際に所持したり効果を発揮したりするのは次回とします。

ステータスの仕様

プレイヤーの持つステータスとしては、体力・最大体力・レベル・満腹度・ちから・攻撃力・防御力・所持金・経験値くらいですかね。また何か追加しないといけないものはあると思いますが、ひとまずこれで進めていきましょう。
いくつか取り上げて説明します。

レベル
最大レベルは作品にもよりますが、初代は37みたいです。

満腹度
本家では最大満腹度は100で10ターン経過で1つ減るのですが、めんどくさいので最大満腹度を1000として、1ターン経過で1つ減ることにします。
満腹度が100を切るとメッセージとして「おなかが空いてきた。。。」、満腹度が50を切るとメッセージとして「空腹で目が回ってきた。。。」と表示するようにして、満腹度が0になると1ターン経過毎に体力が1ずつ減っていくようにします。これがルーグライクゲーム特有のシステムです。

ちから攻撃力
ちからは、レベル毎に与えられる個体の筋肉量のことです。素手で殴ったときの攻撃力とも言えるでしょう。レベル1で5、レベル37で100になるそうです。でちからに装備する武器の攻撃力を加えると、最終的なプレイヤーの攻撃力となります。ローグライクゲームの慣例に従って攻撃力の最大値は255とします。

防御力
通常のRPGではプレイヤーの守備力と防具による防御力を合算してプレイヤーの防御力を決めているのですが、ここでは単に防具による防御力のみをプレイヤーの防御力と定めます。つまり、防具を付けていない場合は防御力0です。

体力最大体力
最大体力は読んで字の如くです。レベル1で15、レベル37で250になるそうです。体力はプレイヤーがダメージを受けた場合実際に減少するステータスです。どれくらいダメージを受けるかといった詳細は後ほど。

さて、細かい設定は置いておいて、ひとまずステータスを実装しましょう。
構造体PERSON_t型で宣言してやります。

typedef struct {
	int HP; //体力
	int MHP; //最大体力
	int LV; //レベル
	int HUNGER; //満腹度
	int STR; //ちから
	int ATK; //攻撃力
	int DEF; //防御力
	int GOLD; //所持金
	int EXP; //経験値
}PERSON_t;

ついでに現在何階にいるのか分かるようにしましょう。
main関数でint floor=1;を宣言してフロア転換するたびにfloor++;としてやります。
場所としては //フィールドの描画 とコメントアウトしたwhile文の次です。

慣例としてフィールドを描画する前に、例えば
B1F Lv1 HP15/15 0G
のように階層、レベル、体力/最大体力、所持金(ゴールド、G)を表示しているので、それを実装します。
また、今回はデバッグ用に満腹度、ちから、攻撃力、防御力、経験値も表示しておきましょう。
以下、main関数(一部省略)

int main(void)
{
	int x, y, i;
	int floor = 1; //階層
	int pRoomNum, stairsRoomNum, stairsX, stairsY; //プレイヤーの初期ルーム、階段のあるルーム、階段の座標
	int cursorX, cursorY; //プレイヤーの座標
	PERSON_t player = { 15 , 15 , 1 , 1000 , 5 , 5 , 0, 0, 0 };

	srand((unsigned int)time(NULL));
	while (1){
		//フィールドの生成
		generateField();

		......(省略)......

		//フィールドの描画
		while (1) {
			system("cls");
			
			printf("B%dF Lv%d HP%d/%d %dG\n", floor, player.LV, player.HP, player.MHP, player.GOLD);
			printf("満腹度:%d ちから:%d 攻撃力:%d 防御力:%d 経験値:%d\n\n", player.HUNGER, player.STR, player.ATK, player.DEF, player.EXP);
			//プレイヤーの周囲のみ表示
			for (y = cursorY - 5; y < cursorY + 5; y++) {
				for (x = cursorX - 5; x < cursorX + 5; x++) {
					if (y < 0 || y >= FIELD_HEIGHT || x < 0 || x >= FIELD_WIDTH) continue;
					
					printf("%s", cellSymbol[field[y][x]]);
				}
				printf("\n");
			}

			//全体マップ表示用
			/*
			for (y = 0; y < FIELD_HEIGHT; y++) {
				for (x = 0; x < FIELD_WIDTH; x++) {

					/*field[y][x]でフィールドの情報を数字で取り出し、
					その数字に応じて対応した記号をcellSymbolで取り出し、
					それを描画
					fieldにはenumで定義したものしか入っていないので
					
					
					printf("%s", cellSymbol[field[y][x]]);
				}
				printf("\n");
			}
			*/

			if (player.HP <= 0) {
				player.HP = 0;
				break;
			}
			if (player.HUNGER <= 0) {
				printf("何か食べなければ死んでしまう!\n");
				player.HUNGER++;
				player.HP--;
			}
			else if (player.HUNGER < 50) printf("空腹で目が回ってきた。。。\n");
			else if (player.HUNGER < 100) printf("お腹が空いてきた。。。\n");			
			switch (_getch()) {
			case 'w':
				if (field[cursorY - 1][cursorX] == CELL_TYPE_WALL) break;
				
				field[cursorY][cursorX] = CELL_TYPE_NONE;
				cursorY--;
				field[cursorY][cursorX] = CELL_TYPE_PLAYER;
				player.HUNGER--;
				break;
			case 's':
				if (field[cursorY + 1][cursorX] == CELL_TYPE_WALL) break;

				field[cursorY][cursorX] = CELL_TYPE_NONE;
				cursorY++;
				field[cursorY][cursorX] = CELL_TYPE_PLAYER;
				player.HUNGER--;
				break;
			case 'a':
				if (field[cursorY][cursorX - 1] == CELL_TYPE_WALL) break;

				field[cursorY][cursorX] = CELL_TYPE_NONE;
				cursorX--;
				field[cursorY][cursorX] = CELL_TYPE_PLAYER;
				player.HUNGER--;
				break;
			case 'd':
				if (field[cursorY][cursorX + 1] == CELL_TYPE_WALL) break;

				field[cursorY][cursorX] = CELL_TYPE_NONE;
				cursorX++;
				field[cursorY][cursorX] = CELL_TYPE_PLAYER;
				player.HUNGER--;
				break;
			default:break;
			

			}

			if (player.HP <= 0) {
				player.HP = 0; 
				break;
			}

			if (cursorX == stairsX && cursorY == stairsY) break;
		}
		if (player.HP == 0) break;
		floor++;
	}

	printf("死んでしまった。。。\n");

	_getch();	
	return 0;
}

アイテムの実装

ダンジョン内にはアイテムが落ちているのですが、その位置は生成されるダンジョンにより様々です。まずはアイテムをダンジョン内にランダムに生成しましょう。ランダムに生成するのはプレイヤーと階段を生成するときに行ったのですが、今後色々ランダムに生成する機会があると思うので、プレイヤーと階段の生成を行った箇所を関数としてまとめておきましょう。

何をランダムに生成するかを選択できるようにしたいので、引数にはenumで列挙したCELL_TYPE_◯◯◯を入力することによって選択できるようにします。更に設置した座標も取得したいので引数にx,y座標のポインタを持ってきます。

以下、setRandomIcon関数

void setRandomIcon(int CELL_TYPE, int *x, int *y)
{
	int roomNum = random(0, areaCount - 1); //どのルーム(エリアとみなしても同値)に置くか
	*x = random(areas[roomNum].room.x, areas[roomNum].room.x + areas[roomNum].room.w - 1);
	*y = random(areas[roomNum].room.y, areas[roomNum].room.y + areas[roomNum].room.h - 1);
	field[*y][*x] = CELL_TYPE;
}

これを用いてプレイヤーと階段の生成を行っておきましょう。
以下、main関数(一部省略)

int main(void)
{
	int x, y, i;
	int floor = 1; //階層
	int pRoomNum, stairsRoomNum, stairsX, stairsY; //プレイヤーの初期ルーム、階段のあるルーム、階段の座標

	int cursorX;
	int cursorY;
	PERSON_t player = { 15 , 15 , 1 , 150 , 5 , 5 , 0, 0, 0 };

	srand((unsigned int)time(NULL));
	while (1){
		//フィールドの生成
		generateField();

		//座標に対応するエリア番号を取得
		for (i = 0; i < areaCount; i++){
			for (y = areas[i].y; y < areas[i].y + areas[i].h; y++){
				for (x = areas[i].x; x < areas[i].x + areas[i].w; x++){
					areaNumber[y][x] = i;
				}
			}
		}
		//すべてのエリアのチェックを0で初期化
		for (i = 0; i < areaCount; i++)
			areas[i].check = 0;

		areas[0].check = 1; //最初のエリアのみ、チェックを打っておく
		orderIndex = 0; //orderRoom関数で使用するために初期化
		order[0] = 0; //順番の最初はルーム0

		orderRoom(0); //0番目のエリアからどんどん隣接させる

		//エラー回避
		if (connectErrorCount >= areaCount){
			connectErrorCount = 0;
			continue;
		}

		//プレイヤーと階段の生成
		setRandomIcon(CELL_TYPE_PLAYER, &cursorX, &cursorY);
		setRandomIcon(CELL_TYPE_STAIRS, &stairsX, &stairsY);


		//フィールドの描画
		while (1) {
			
			......(省略)......
				
		}
		if (player.HP == 0) break;
		floor++;
	}

	printf("死んでしまった。。。\n");

	_getch();
	return 0;
}

さて、本題のアイテムのランダム生成を行いましょう。
複数のアイテムを整理するにはIDを使ってやれば良いです。enumで列挙しているCELL_TYPE_◯◯◯の続きに書き込んであげればそれでIDの設定は完了です。今回はアイテムとしてパンと大きなパンを追加しましょう。食料はローグライクゲームにとって必要不可欠な要素なので、これは押さえておくべきですね。パンは満腹度を50%、大きなパンは満腹度を100%回復させます。
アイテムは一つのフロアに5~7個生成されるとして、それらを1つずつランダムに設置しましょう。アイテムの総数をFIELD_ITEM_COUNTで定義してやります。それに伴ってアイテムの情報をITEM_t型の構造体で定義してやります。

#define FIELD_ITEM_COUNT 20
#define CELL_COUNT 5	

enum{
	CELL_TYPE_NONE, //0
	CELL_TYPE_WALL, //1
	CELL_TYPE_PLAYER, //2
	CELL_TYPE_STAIRS, //3
	CELL_TYPE_BREAD, //パン
	CELL_TYPE_BIG_BREAD //大きなパン
};

typedef struct {
	int x, y;
	int ID;
}ITEM_t;

char cellSymbol[CELL_COUNT][3] = {
	"・", //CELL_TYPE_NONE(0)番目の配列
	"■", //CELL_TYPE_WALL(1)番目の配列
	"@", //CELL_TYPE_PLAYER(2)番目の配列
	"巛", //CELL_TYPE_STAIRS(3)番目の配列
	"ア"  //4番目の配列にアイテムアイコン
};

ここで、アイテムのIDはCELL_TYPE_STAIRS以降で管理されるため、setRandomIcon関数の引数にあるCELL_TYPEに問題が発生します。fieldでアイテムを管理する際に、アイテムのIDに依らず全て"ア"で表現することにしたので、fieldに代入する値はアイテムIDの初期値である4に固定するよう改造します。

void setRandomIcon(int CELL_TYPE, int *x, int *y)
{
	int roomNum = random(0, areaCount - 1); //どのルーム(エリアとみなしても同値)に置くか
	*x = random(areas[roomNum].room.x, areas[roomNum].room.x + areas[roomNum].room.w - 1);
	*y = random(areas[roomNum].room.y, areas[roomNum].room.y + areas[roomNum].room.h - 1);
	if (CELL_TYPE_BREAD <= CELL_TYPE && CELL_TYPE_BIG_BREAD) //CELL_TYPEがアイテムなら
		CELL_TYPE = 4;
	field[*y][*x] = CELL_TYPE;
}

以下、アイテムの生成のソースコード

//アイテムの生成
int itemCount = random(5, 7);
ITEM_t item[FIELD_ITEM_COUNT]; //アイテムの識別

for (i = 0; i < itemCount; i++) {
	item[i].ID = random(CELL_TYPE_BREAD, CELL_TYPE_BIG_BREAD);
	setRandomIcon(item[i].ID, &item[i].x, &item[i].y);
}

以下、全体のソースコード

#include…

#define FIELD_HEIGHT 64
#define FIELD_WIDTH 64
#define AREA_MAX 64 //エリアの最大個数
#define AREA_SIZE_MIN 16 //エリア
#define CELL_COUNT 5
#define ROOM_SIZE_MIN (AREA_SIZE_MIN-4-4)
#define FIELD_ITEM_COUNT 20

enum{…};

//ルームの座標・横幅・高さをひとまとめに管理するために構造体ROOM_tで定義
typedef struct{…}ROOM_t;

/*
エリアの座標・横幅・高さをひとまとめに管理するために構造体AREA_tで定義
また、エリア内にルームを作成するのでルームもここで管理します。
orderRoom関数用にチェック項目を追加
*/

typedef struct{…}AREA_t;

typedef struct {…}PERSON_t;

typedef struct {
 int x, y;
 int ID;
}ITEM_t;

AREA_t areas[AREA_MAX]; //各エリアの座標・横幅・高さの情報をしまっておける
int areaCount; //エリアの個数
int field[FIELD_HEIGHT][FIELD_WIDTH]; //座標毎のフィールドの状態
int areaNumber[FIELD_HEIGHT][FIELD_WIDTH]; //座標に対応するエリア番号をしまっておける
char cellSymbol[CELL_COUNT][3] = {
 "・", //CELL_TYPE_NONE(0)番目の配列
 "■", //CELL_TYPE_WALL(1)番目の配列
 "@", //CELL_TYPE_PLAYER(2)番目の配列
 "巛", //CELL_TYPE_STAIRS(3)番目の配列
 "ア"  //4番目の配列にアイテムアイコン
};

int order[AREA_MAX]; //ルームをつなげる順番
int orderIndex = 0; //order[]の添え字
int connectErrorCount = 0; //エラー発見用

int random(int, int);
void spritArea(int);
void generateField(void);
void orderRoom(int);
void setRandomIcon(int, int *, int *);

int main(void)
{
 int x, y, i;
 int floor = 1; //階層
 int pRoomNum, stairsRoomNum, stairsX, stairsY; //プレイヤーの初期ルーム、階段のあるルーム、階段の座標

 int cursorX;
 int cursorY;
 PERSON_t player = { 15 , 15 , 1 , 150 , 5 , 5 , 0, 0, 0 };

 srand((unsigned int)time(NULL));
 while (1){
  //フィールドの生成
  generateField();

  //座標に対応するエリア番号を取得
  for (i = 0; i < areaCount; i++){…}
  //すべてのエリアのチェックを0で初期化
  for (i = 0; i < areaCount; i++)
   areas[i].check = 0;

  areas[0].check = 1; //最初のエリアのみ、チェックを打っておく
  orderIndex = 0; //orderRoom関数で使用するために初期化
  order[0] = 0; //順番の最初はルーム0

  orderRoom(0); //0番目のエリアからどんどん隣接させる

  //エラー回避
  if (connectErrorCount >= areaCount){…}

  //プレイヤーと階段の生成
  setRandomIcon(CELL_TYPE_PLAYER, &cursorX, &cursorY);
  setRandomIcon(CELL_TYPE_STAIRS, &stairsX, &stairsY);

  //アイテムの生成
  int itemCount = random(5, 7);
  ITEM_t item[FIELD_ITEM_COUNT]; //アイテムの識別

  for (i = 0; i < itemCount; i++) {
   item[i].ID = random(CELL_TYPE_BREAD, CELL_TYPE_BIG_BREAD);
   setRandomIcon(item[i].ID, &item[i].x, &item[i].y);
  }
  

  //フィールドの描画
  while (1) {…}

 printf("死んでしまった。。。\n");

 _getch();
 return 0;
}

int random(int start, int end){…}
//エリアを分割して、エリア毎に座標・横幅・高さを取得する関数
//oldAreaIndex : 分割前のエリア番号
//newAreaIndex : 分割後のエリア番号
void spritArea(int oldAreaIndex){…}

void generateField(void){…}

void orderRoom(int areaIndex){…}

void setRandomIcon(int CELL_TYPE, int *x, int *y)
{
 int roomNum = random(0, areaCount - 1); //どのルーム(エリアとみなしても同値)に置くか
 *x = random(areas[roomNum].room.x, areas[roomNum].room.x + areas[roomNum].room.w - 1);
 *y = random(areas[roomNum].room.y, areas[roomNum].room.y + areas[roomNum].room.h - 1);
 if (CELL_TYPE_BREAD <= CELL_TYPE && CELL_TYPE_BIG_BREAD) //CELL_TYPEがアイテムなら
  CELL_TYPE = 4;
 field[*y][*x] = CELL_TYPE;
}

おわりに

今回はステータスの概念の追加、及びアイテムの設置を行いました。アイテムの設置自体は本当にただアイコンとして置いているだけなので次回それを使えるように実装していきましょう。実装する際にゲーム中のメニュー画面を作るところから始めます。ではでは、お疲れ様でした。

vol.10 プレイヤー・階段の実装、フロア転換

目次

はじめに

実際にランダムダンジョン生成が可能になったということですが、実際にプレイできないとまだ何とも実感が湧かないものです。今回はプレイヤーと階段を実際にダンジョン内に置いて、プレイヤーを操作して階段まで進んだら次のフロアに進むという機構を作って行こうと思います。また、フィールドの描画も工夫していきます。ローグライクゲームでは基本全体マップは公開されずに限られた視野のみでダンジョンを攻略することになるので、プレイヤー中心に縦横10マス分のみ描画することにします。まさにゲームでよく見るプレイ画面を作っていこうというわけです。

プレイヤーと階段の生成

当然のことですが、プレイヤーも階段もルーム内になければいけません。壁にめり込んでいたら何もできませんからね。ここではまず、プレイヤーと階段がどのエリアにいるのかを乱数で決定し、そのエリア内にあるルームで更に乱数を用いてプレイヤーと階段の座標を確定させます。場所が確定したらその情報をfieldに入れてあげます。
そのためにいくつか定義を追加・更新します。

・#define CELL_COUNT 4
・enum{
	CELL_TYPE_NONE,   //0
	CELL_TYPE_WALL,   //1
	CELL_TYPE_PLAYER, //2
	CELL_TYPE_STAIRS  //3
 };
・char cellSymbol[CELL_COUNT][3] = {
	"・",  //CELL_TYPE_NONE(0)番目の配列
	"■",  //CELL_TYPE_WALL(1)番目の配列
	"@",  //CELL_TYPE_PLAYER(2)番目の配列
	"巛"   //CELL_TYPE_STAIRS(3)番目の配列
 };
・(main関数内)
    int pRoomNum, stairsRoomNum, stairsX, stairsY; //プレイヤーの初期ルーム、階段のあるルーム、階段の座標
    int cursorX, cursorY; //プレイヤーの座標

これらを準備した上で、フィールドの描画前にプレイヤーと階段を生成してやります。
以下、該当のソースコード

//プレイヤーと階段の生成
pRoomNum = random(0, areaCount-1); //どのルームにプレイヤーを置くか
cursorX = random(areas[pRoomNum].room.x, areas[pRoomNum].room.x + areas[pRoomNum].room.w - 1);
cursorY = random(areas[pRoomNum].room.y, areas[pRoomNum].room.y + areas[pRoomNum].room.h - 1);
field[cursorY][cursorX] = CELL_TYPE_PLAYER;

stairsRoomNum = random(0, areaCount-1); //どのルームに階段を置くか
stairsX = random(areas[stairsRoomNum].room.x, areas[stairsRoomNum].room.x + areas[stairsRoomNum].room.w - 1);
stairsY = random(areas[stairsRoomNum].room.y, areas[stairsRoomNum].room.y + areas[stairsRoomNum].room.h - 1);
field[stairsY][stairsX] = CELL_TYPE_STAIRS;

プレイヤーの移動・壁判定・10マスだけ描画・フロア転換

やることはタイトル通りです。_getch()でキー入力し、それに応じてプレイヤーの座標を動かす。その際に動かそうとした先が壁だった場合は動かさず、動かせる状況ならそれに応じてfieldを更新してあげます。
操作はフィールドを描画した後に行うので、そこにswitch文で記述してあげましょう。
また、描画からプレイヤー操作の箇所までは無限ループにしておくことを忘れないでください。そうしないと繰り返し入力できないですからね。
以下、該当のソースコード

switch (_getch()) {
case 'w':
	if (field[cursorY - 1][cursorX] == CELL_TYPE_WALL) break;
				
	field[cursorY][cursorX] = CELL_TYPE_NONE;
	cursorY--;
	field[cursorY][cursorX] = CELL_TYPE_PLAYER;
	break;
case 's':
	if (field[cursorY + 1][cursorX] == CELL_TYPE_WALL) break;

	field[cursorY][cursorX] = CELL_TYPE_NONE;
	cursorY++;
	field[cursorY][cursorX] = CELL_TYPE_PLAYER;
	break;
case 'a':
	if (field[cursorY][cursorX - 1] == CELL_TYPE_WALL) break;

	field[cursorY][cursorX] = CELL_TYPE_NONE;
	cursorX--;
	field[cursorY][cursorX] = CELL_TYPE_PLAYER;
	break;
case 'd':
	if (field[cursorY][cursorX + 1] == CELL_TYPE_WALL) break;

	field[cursorY][cursorX] = CELL_TYPE_NONE;
	cursorX++;
	field[cursorY][cursorX] = CELL_TYPE_PLAYER;
	break;
default:break;
}

また、10マスだけ描画するところもここに記述します。
プレイヤーの座標±5の部分のみを描画しますが、プレイヤーがマップの端の方にいた場合配列外アクセス(配列の添え字がマイナスの値を取ったり、大きすぎたり)を起こすので、配列外になりそうな場合をスキップすることを忘れないように。
以下、該当のソースコード

//プレイヤーの周囲のみ表示
for (y = cursorY - 5; y < cursorY + 5; y++) {
	for (x = cursorX - 5; x < cursorX + 5; x++) {
		if (y < 0 || y >= FIELD_HEIGHT || x < 0 || x >= FIELD_WIDTH) continue;

		printf("%s", cellSymbol[field[y][x]]);
	}
	printf("\n");
}

最後にフロア転換を行います。
これは単純で、プレイヤーの座標が階段の座標と一致したらbreakしてやればいいです。
以下にmain関数ソースコード

int main(void)
{
	int x, y, i;
	int pRoomNum, stairsRoomNum, stairsX, stairsY; //プレイヤーの初期ルーム、階段のあるルーム、階段の座標
	int cursorX, cursorY; //プレイヤーの座標

	srand((unsigned int)time(NULL));
	while (1){
		//フィールドの生成
		generateField();

		//座標に対応するエリア番号を取得
		for (i = 0; i < areaCount; i++){
			for (y = areas[i].y; y < areas[i].y + areas[i].h; y++){
				for (x = areas[i].x; x < areas[i].x + areas[i].w; x++){
					areaNumber[y][x] = i;
				}
			}
		}
		//すべてのエリアのチェックを0で初期化
		for (i = 0; i < areaCount; i++)
			areas[i].check = 0;

		areas[0].check = 1; //最初のエリアのみ、チェックを打っておく
		orderIndex = 0; //orderRoom関数で使用するために初期化
		order[0] = 0; //順番の最初はルーム0

		orderRoom(0); //0番目のエリアからどんどん隣接させる

		//エラー回避
		if (connectErrorCount >= areaCount){
			connectErrorCount = 0;
			continue;
		}

		//プレイヤーと階段の生成
		pRoomNum = random(0, areaCount-1); //どのルーム(エリアとみなしても同値)にプレイヤーを置くか
		cursorX = random(areas[pRoomNum].room.x, areas[pRoomNum].room.x + areas[pRoomNum].room.w - 1);
		cursorY = random(areas[pRoomNum].room.y, areas[pRoomNum].room.y + areas[pRoomNum].room.h - 1);
		field[cursorY][cursorX] = CELL_TYPE_PLAYER;

		stairsRoomNum = random(0, areaCount-1); //どのルームに階段を置くか
		stairsX = random(areas[stairsRoomNum].room.x, areas[stairsRoomNum].room.x + areas[stairsRoomNum].room.w - 1);
		stairsY = random(areas[stairsRoomNum].room.y, areas[stairsRoomNum].room.y + areas[stairsRoomNum].room.h - 1);
		field[stairsY][stairsX] = CELL_TYPE_STAIRS;


		//フィールドの描画
		while (1) {
			system("cls");
			
			//プレイヤーの周囲のみ表示
			for (y = cursorY - 5; y < cursorY + 5; y++) {
				for (x = cursorX - 5; x < cursorX + 5; x++) {
					if (y < 0 || y >= FIELD_HEIGHT || x < 0 || x >= FIELD_WIDTH) continue;
					
					printf("%s", cellSymbol[field[y][x]]);
				}
				printf("\n");
			}

			printf("\n");
			//全体表示用
			for (y = 0; y < FIELD_HEIGHT; y++) {
				for (x = 0; x < FIELD_WIDTH; x++) {

					/*field[y][x]でフィールドの情報を数字で取り出し、
					その数字に応じて対応した記号をcellSymbolで取り出し、
					それを描画
					fieldにはenumで定義したものしか入っていないので
					*/

					printf("%s", cellSymbol[field[y][x]]);
				}
				printf("\n");
			}
	        

			switch (_getch()) {
			case 'w':
				if (field[cursorY - 1][cursorX] == CELL_TYPE_WALL) break;
				
				field[cursorY][cursorX] = CELL_TYPE_NONE;
				cursorY--;
				field[cursorY][cursorX] = CELL_TYPE_PLAYER;
				break;
			case 's':
				if (field[cursorY + 1][cursorX] == CELL_TYPE_WALL) break;

				field[cursorY][cursorX] = CELL_TYPE_NONE;
				cursorY++;
				field[cursorY][cursorX] = CELL_TYPE_PLAYER;
				break;
			case 'a':
				if (field[cursorY][cursorX - 1] == CELL_TYPE_WALL) break;

				field[cursorY][cursorX] = CELL_TYPE_NONE;
				cursorX--;
				field[cursorY][cursorX] = CELL_TYPE_PLAYER;
				break;
			case 'd':
				if (field[cursorY][cursorX + 1] == CELL_TYPE_WALL) break;

				field[cursorY][cursorX] = CELL_TYPE_NONE;
				cursorX++;
				field[cursorY][cursorX] = CELL_TYPE_PLAYER;
				break;
			default:break;
			}

			if (cursorX == stairsX && cursorY == stairsY) break;
		}

	}

	
	return 0;
}

おわりに

ようやくこれでゲームを行う段階に至りました。次回からはプレイヤーのステータス関係、及びアイテム作成を行いましょう。ローグライクゲーム特有のルールもあるので、それも確認しつつ新しいことを実装していきます。
お疲れ様でした。

vol.9 コリドーの作成

目次

はじめに

ダンジョンの自動生成をするということで、前回はルームの作成を行いました。
今回は
①エリアの分割
②ルームの作成
③ドアの設置←←
④廊下(コリドー、Corridor)の作成←←

③④をやりましょう。
※後述にもありますが、③は考えた結果別に必要ないということで④が中心です。

ドアの設置の準備

ドアを設置するにあたって、ルームにドアをつけるだけならまだ簡単なんです。(簡単なんですよ。)大切なのはちゃんとコリドーを通すことを見通したドアの位置決定なんです。ドアを生成してコリドーを通した結果、すべてのルームに行けるようにしなければせっかく作ったルームがかわいそうですので、ちゃんと全部のルームに行けるようにドアを生成したいのです。
そのために、どの順番でどの方向でドアを生成していくかを確定させていきます。
各エリアに必ず1ルーム存在するので、すべてのエリアに行けるようになれば良いわけです。そのためにこのような考え方でドアを生成する順番を確定させていきたいと思います。
エリア0からスタートして隣接する最も大きなエリア番号のところへと向かう。それを繰り返して最終的にすべてつながれば終了。もしまだ行っていないエリアがあるのにもう次に進めるエリアがなくなってしまった場合には、ひとつ前に行ったエリアに戻ってそこで隣接する最も大きなエリア番号のところへ向かうことによってすべてのエリアに行こう。やってみた結果すべてエリアに行けないパターンがあったので、それはもう仕方ありません。いけないパターンにぶち当たったら、なかったことにしてもう一度順番を決めなおします。
f:id:sion2000114:20190906121653p:plain
これをorderRoom関数で行います。

以下、orderRoom関数。
ただし、グローバル変数として

int order[AREA_MAX]; //ルームをつなげる順番
int orderIndex = 0; //order[]の添え字
int connectErrorCount = 0; //エラー発見用

を準備していて、AREA_t構造体に

int check; //0:まだ 1:チェックされた

の項目を追加しています。

void orderRoom(int areaIndex)
{
	int x, y;
	int neighborAreaNum[2] = { -1, 0 }; //areaIndex番目のエリアに隣接するエリア番号の最大値をneiborAreaNum[0]に格納できる
	int nextDirection; //1:上 2:下 3:左 4:右
	
	//areaIndex番目のエリアに隣接するエリアを見つける
	//上側
	if (areas[areaIndex].y != 0){
		for (x = areas[areaIndex].x; x < areas[areaIndex].x + areas[areaIndex].w; x++){
			if (areas[areaNumber[areas[areaIndex].y - 1][x]].check == 1) continue;

			neighborAreaNum[1] = areaNumber[areas[areaIndex].y - 1][x];
			if (neighborAreaNum[0] < neighborAreaNum[1]){
				neighborAreaNum[0] = neighborAreaNum[1]; //一番大きな番号を[0]に格納
				nextDirection = 2;
			}
		}
	}
	//下側
	if (areas[areaIndex].y != FIELD_HEIGHT-1){
		for (x = areas[areaIndex].x; x < areas[areaIndex].x + areas[areaIndex].w; x++){
			if (areas[areaNumber[areas[areaIndex].y + areas[areaIndex].h][x]].check == 1) continue;

			neighborAreaNum[1] = areaNumber[areas[areaIndex].y + areas[areaIndex].h][x];
			if (neighborAreaNum[0] < neighborAreaNum[1]){
				neighborAreaNum[0] = neighborAreaNum[1]; //一番大きな番号を[0]に格納
				nextDirection = 3;
			}
		}
	}
	//左側
	if (areas[areaIndex].x != 0){
		for (y = areas[areaIndex].y; y < areas[areaIndex].y + areas[areaIndex].h; y++){
			if (areas[areaNumber[y][areas[areaIndex].x - 1]].check == 1) continue;

			neighborAreaNum[1] = areaNumber[y][areas[areaIndex].x-1];
			if (neighborAreaNum[0] < neighborAreaNum[1])
				neighborAreaNum[0] = neighborAreaNum[1]; //一番大きな番号を[0]に格納
		}
	}
	//右側
	if (areas[areaIndex].x != FIELD_WIDTH){
		for (y = areas[areaIndex].y; y < areas[areaIndex].y + areas[areaIndex].h; y++){
			if (areas[areaNumber[y][areas[areaIndex].x + areas[areaIndex].w]].check == 1) continue;

			neighborAreaNum[1] = areaNumber[y][areas[areaIndex].x +areas[areaIndex].w];
			if (neighborAreaNum[0] < neighborAreaNum[1]){
				neighborAreaNum[0] = neighborAreaNum[1]; //一番大きな番号を[0]に格納
				nextDirection = 4;
			}
		}
	}

	//次のエリアが見つからない場合、
	if (neighborAreaNum[0] == -1) {
		connectErrorCount++;
		if (connectErrorCount >= 10) return;
		orderRoom(order[orderIndex - 1]); //ひとつ前のエリアに戻ってチェックされていないエリアを探す

	}

	//次のエリアが見つかった場合、
	if (neighborAreaNum[0] != -1){
		areas[neighborAreaNum[0]].check = 1; //次のエリアにチェックを入れる
		order[orderIndex + 1] = neighborAreaNum[0];
		orderIndex++;
	}

	
	if (orderIndex+1 == areaCount) return;

	orderRoom(neighborAreaNum[0]);
}

コリドーの生成

作ってる途中で「あれ、ドアって要らなくね?」ってなったので、もう一気にコリドーを通します(冒頭の宣言通り)。ドアをいちいち考えているとめんどくさかったのでそのまま作成しようと思います。
接続されるルーム内にそれぞれ任意に点(d1x,d1y),(d2x,d2y)を取って、繋げたいルームの方向へその点を移動していきます。移動していく度に穴を掘っていくようなイメージでコリドーを生成していきたいと思います。移動の方法としては、x、y座標のどちらかのみを動かしていって、そのどちらかが一致するまで続けます。
f:id:sion2000114:20190906121839p:plain

一致したら後はもう片方の座標が一致するまで動かしていけば晴れてコリドー完成というわけです。
f:id:sion2000114:20190906122017p:plain
orderRoom関数の次のエリアが見つかった場合の箇所を書き換えていきます。
以下、orderRoom関数内

//次のエリアが見つかった場合、
if (neighborAreaNum[0] != -1){
	areas[neighborAreaNum[0]].check = 1; //次のエリアにチェックを入れる
	order[orderIndex + 1] = neighborAreaNum[0];
	orderIndex++;

		
	//ここからコリドーを伸ばしていく
	int d1x, d1y, d2x, d2y; //d1:移動前のドアの座標 d2:移動後のドアの座標
	//両ルーム内の適当な点を1つずつ取る
	d1x = random(areas[order[orderIndex - 1 -connectErrorCount]].room.x, areas[order[orderIndex - 1 - connectErrorCount]].room.x + areas[order[orderIndex - 1 - connectErrorCount]].room.w - 1);
	d1y = random(areas[order[orderIndex - 1 - connectErrorCount]].room.y, areas[order[orderIndex - 1 - connectErrorCount]].room.y + areas[order[orderIndex - 1 - connectErrorCount]].room.h - 1);
	d2x = random(areas[order[orderIndex]].room.x, areas[order[orderIndex]].room.x + areas[order[orderIndex]].room.w - 1);
	d2y = random(areas[order[orderIndex]].room.y, areas[order[orderIndex]].room.y + areas[order[orderIndex]].room.h - 1);

		
	connectErrorCount = 0;


		
	//各ルームからコリドーを伸ばしていって同じ位相まで行く
	//その際に進んだ経路は穴を掘るようになるので、CELL_TYPE_NONEを代入
	switch (nextDirection){
	case 1:
		while (d1y!=d2y){
			if (abs(d1y - d2y) == 1){
				d1y--;
				field[d1y][d1x] = CELL_TYPE_NONE;

			}
			else{
				d1y--, d2y++;
				field[d1y][d1x] = CELL_TYPE_NONE;
				field[d2y][d2x] = CELL_TYPE_NONE;
			}
		}

		if (d1y == d2y) {
			while (d1x != d2x) {
				if (d1x < d2x) d1x++;
				else d1x--;
				field[d1y][d1x] = CELL_TYPE_NONE;
			}
		}

		break;
	case 2:
		while (d1y != d2y){
			if (abs(d1y - d2y) == 1){
				d1y++;
				field[d1y][d1x] = CELL_TYPE_NONE;
			}
			else{
				d1y++, d2y--;
				field[d1y][d1x] = CELL_TYPE_NONE;
				field[d2y][d2x] = CELL_TYPE_NONE;
			}
		}

		if (d1y == d2y) {
			while (d1x != d2x) {
				if (d1x < d2x) d1x++;
				else d1x--;
				field[d1y][d1x] = CELL_TYPE_NONE;
			}
		}

		break;
	case 3:
		while (d1x != d2x){
			if (abs(d1x - d2x) == 1){
				d1x--;
				field[d1y][d1x] = CELL_TYPE_NONE;
			}
			else{
				d1x--, d2x++;
				field[d1y][d1x] = CELL_TYPE_NONE;
				field[d2y][d2x] = CELL_TYPE_NONE;
			}
		}

		if (d1x == d2x) {
			while (d1y != d2y) {
				if (d1y < d2y) d1y++;
				else d1y--;
				field[d1y][d1x] = CELL_TYPE_NONE;
			}
		}

		break;
	case 4:
		while (d1x != d2x){
			if (abs(d1x - d2x) == 1){
				d1x++;
				field[d1y][d1x] = CELL_TYPE_NONE;
			}
			else{
				d1x++, d2x--;
				field[d1y][d1x] = CELL_TYPE_NONE;
				field[d2y][d2x] = CELL_TYPE_NONE;
			}
		}

		if (d1x == d2x) {
			while (d1y != d2y) {
				if (d1y < d2y) d1y++;
				else d1y--;
				field[d1y][d1x] = CELL_TYPE_NONE;
			}
		}

		break;
	default: break;
	}
}

ここまでの全体のソースコード、実行結果例

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<time.h>
#include<conio.h>

#define FIELD_HEIGHT 64
#define FIELD_WIDTH 64
#define AREA_MAX 64 //エリアの最大個数
#define AREA_SIZE_MIN 16 //エリア
#define CELL_COUNT 2
#define ROOM_SIZE_MIN (AREA_SIZE_MIN-4-4)

enum {
	CELL_TYPE_NONE, //0
	CELL_TYPE_WALL  //1
};

//ルームの座標・横幅・高さをひとまとめに管理するために構造体ROOM_tで定義
typedef struct {
	int x;
	int y;
	int w;
	int h;
	int doorX;
	int doorY;
}ROOM_t;

/*
エリアの座標・横幅・高さをひとまとめに管理するために構造体AREA_tで定義
また、エリア内にルームを作成するのでルームもここで管理します。
orderRoom関数用にチェック項目を追加
*/

typedef struct {
	int x;
	int y;
	int w; //width
	int h; //height
	ROOM_t room;
	int check; //0:まだ 1:チェックされた
}AREA_t;

AREA_t areas[AREA_MAX]; //各エリアの座標・横幅・高さの情報をしまっておける
int areaCount; //エリアの個数
int field[FIELD_HEIGHT][FIELD_WIDTH]; //座標毎のフィールドの状態
int areaNumber[FIELD_HEIGHT][FIELD_WIDTH]; //座標に対応するエリア番号をしまっておける
char cellSymbol[CELL_COUNT][3] = {
 "・", //CELL_TYPE_NONE(0)番目の配列
 "■" //CELL_TYPE_WALL(1)番目の配列
};

int order[AREA_MAX]; //ルームをつなげる順番
int orderIndex = 0; //order[]の添え字
int connectErrorCount = 0; //エラー発見用

int random(int, int);
void spritArea(int);
void generateField(void);
void orderRoom(int);

int main(void)
{
	int x, y, i;
	srand((unsigned int)time(NULL));
	while (1) {
		//フィールドの生成
		generateField();

		//座標に対応するエリア番号を取得
		for (i = 0; i < areaCount; i++) {
			for (y = areas[i].y; y < areas[i].y + areas[i].h; y++) {
				for (x = areas[i].x; x < areas[i].x + areas[i].w; x++) {
					areaNumber[y][x] = i;
				}
			}
		}
		//すべてのエリアのチェックを0で初期化
		for (i = 0; i < areaCount; i++)
			areas[i].check = 0;

		areas[0].check = 1; //最初のエリアのみ、チェックを打っておく
		orderIndex = 0; //orderRoom関数で使用するために初期化
		order[0] = 0; //順番の最初はルーム0

		orderRoom(0); //0番目のエリアからどんどん隣接させる

		//エラー回避
		if (connectErrorCount >= areaCount) {
			connectErrorCount = 0;
			continue;
		}

		system("cls");
		//フィールドの描画
		for (y = 0; y < FIELD_HEIGHT; y++) {
			for (x = 0; x < FIELD_WIDTH; x++) {
				/*
				field[y][x]でフィールドの情報を数字で取り出し、
				その数字に応じて対応した記号をcellSymbolで取り出し、
				それを描画
				fieldにはenumで定義したものしか入っていないので
				*/
				printf("%s", cellSymbol[field[y][x]]);
			}
			printf("\n");
		}

		for (i = 0; i < areaCount; i++)
			printf("%d\n", order[i]); //移動するエリアの順番確認用


		_getch();
	}


	return 0;
}

int random(int start, int end)
{
	return rand() % (end - start + 1) + start;
}

//エリアを分割して、エリア毎に座標・横幅・高さを取得する関数
//oldAreaIndex : 分割前のエリア番号
//newAreaIndex : 分割後のエリア番号
void spritArea(int oldAreaIndex)
{
	int flag = 0;
	int newAreaIndex = areaCount;

	//分割をキャンセルする時に戻せるようにする(再帰処理なので、終了条件を満たしたときに直前の分割をキャンセルしたい)
	int w = areas[oldAreaIndex].w; //保存用
	int h = areas[oldAreaIndex].h; //保存用

	//縦切り
	if (rand() % 2 == 0) {
		areas[oldAreaIndex].w /= 2; //半分
		areas[newAreaIndex].x = areas[oldAreaIndex].x + areas[oldAreaIndex].w; //新しいエリアのx座標
		areas[newAreaIndex].y = areas[oldAreaIndex].y; //新しいエリアのy座標
		areas[newAreaIndex].w = w - areas[oldAreaIndex].w; //新しいエリアの横幅
		areas[newAreaIndex].h = areas[oldAreaIndex].h; //新しいエリアの高さ
	}
	//横切り
	else {
		flag = 1;//横切りのサイン
		areas[oldAreaIndex].h /= 2; //半分
		areas[newAreaIndex].x = areas[oldAreaIndex].x; //新しいエリアのx座標
		areas[newAreaIndex].y = areas[oldAreaIndex].y + areas[oldAreaIndex].h; //新しいエリアのy座標
		areas[newAreaIndex].w = areas[oldAreaIndex].w; //新しいエリアの横幅
		areas[newAreaIndex].h = h - areas[oldAreaIndex].h; //新しいエリアの高さ
	}
	//分割後の二つのエリアがエリアの条件(AREA_SIZE_MINの範囲内かどうか)を満たしているか
	if ((areas[oldAreaIndex].w < AREA_SIZE_MIN) || (areas[oldAreaIndex].h < AREA_SIZE_MIN)
		|| (areas[newAreaIndex].w < AREA_SIZE_MIN) || (areas[newAreaIndex].w < AREA_SIZE_MIN)) {
		areas[oldAreaIndex].w = w;
		areas[oldAreaIndex].h = h;
		return; //void型関数を終了させたいときは、単にreturnと書けばよい
	}

	areaCount++;

	spritArea(newAreaIndex); //新エリアの分割
	spritArea(oldAreaIndex); //旧エリアの分割

}

void generateField(void)
{
	int i, x, y;
	//最初のエリアの情報
	//つまり、エリアの初期化
	areaCount = 0;
	areas[0].x = 0;
	areas[0].y = 0;
	areas[0].w = FIELD_WIDTH;
	areas[0].h = FIELD_HEIGHT;
	areaCount++;
	spritArea(0);

	//まず、フィールドをすべて壁(1)で埋める。あとで部屋の部分に穴をあけていく。
	for (y = 0; y < FIELD_HEIGHT; y++) {
		for (x = 0; x < FIELD_WIDTH; x++) {
			field[y][x] = CELL_TYPE_WALL;
		}
	}

	//ここでルームを作成している!
	for (i = 0; i < areaCount; i++) {
		//まずは元となるルームを作成
		areas[i].room.x = areas[i].x + 2;
		areas[i].room.y = areas[i].y + 2;
		areas[i].room.w = areas[i].w - 4;
		areas[i].room.h = areas[i].h - 4;
		//ここからルームをランダムに小さくします。
		int r1 = random(0, areas[i].room.w - ROOM_SIZE_MIN);
		int r2 = random(0, areas[i].room.h - ROOM_SIZE_MIN);

		areas[i].room.x += r1;
		areas[i].room.y += r2;
		areas[i].room.w = random(ROOM_SIZE_MIN, areas[i].room.w - r1);
		areas[i].room.h = random(ROOM_SIZE_MIN, areas[i].room.h - r2);

		//ルームの上から下まで
		for (y = areas[i].room.y; y < areas[i].room.y + areas[i].room.h; y++) {
			//ルームの左から右まで
			for (x = areas[i].room.x; x < areas[i].room.x + areas[i].room.w; x++) {
				field[y][x] = CELL_TYPE_NONE; //ルームの部分だけ穴が空く(ルームでのfieldが0になった)
			}
		}
	}
}

void orderRoom(int areaIndex)
{
	int x, y;
	int neighborAreaNum[2] = { -1, 0 }; //areaIndex番目のエリアに隣接するエリア番号の最大値をneiborAreaNum[0]に格納できる
	int nextDirection; //1:上 2:下 3:左 4:右

	//areaIndex番目のエリアに隣接するエリアを見つける
	//上側
	if (areas[areaIndex].y != 0) {
		for (x = areas[areaIndex].x; x < areas[areaIndex].x + areas[areaIndex].w; x++) {
			if (areas[areaNumber[areas[areaIndex].y - 1][x]].check == 1) continue;

			neighborAreaNum[1] = areaNumber[areas[areaIndex].y - 1][x];
			if (neighborAreaNum[0] < neighborAreaNum[1]) {
				neighborAreaNum[0] = neighborAreaNum[1]; //一番大きな番号を[0]に格納
				nextDirection = 1;
			}
		}
	}
	//下側
	if (areas[areaIndex].y + areas[areaIndex].h != FIELD_HEIGHT - 1) {
		for (x = areas[areaIndex].x; x < areas[areaIndex].x + areas[areaIndex].w; x++) {
			if (areas[areaNumber[areas[areaIndex].y + areas[areaIndex].h - 1][x]].check == 1) continue;

			neighborAreaNum[1] = areaNumber[areas[areaIndex].y + areas[areaIndex].h][x];
			if (neighborAreaNum[0] < neighborAreaNum[1]) {
				neighborAreaNum[0] = neighborAreaNum[1]; //一番大きな番号を[0]に格納
				nextDirection = 2;
			}
		}
	}
	//左側
	if (areas[areaIndex].x != 0) {
		for (y = areas[areaIndex].y; y < areas[areaIndex].y + areas[areaIndex].h; y++) {
			if (areas[areaNumber[y][areas[areaIndex].x - 1]].check == 1) continue;

			neighborAreaNum[1] = areaNumber[y][areas[areaIndex].x - 1];
			if (neighborAreaNum[0] < neighborAreaNum[1]) {
				neighborAreaNum[0] = neighborAreaNum[1]; //一番大きな番号を[0]に格納
				nextDirection = 3;
			}
		}
	}
	//右側
	if (areas[areaIndex].x + areas[areaIndex].w != FIELD_WIDTH) {
		for (y = areas[areaIndex].y; y < areas[areaIndex].y + areas[areaIndex].h; y++) {
			if (areas[areaNumber[y][areas[areaIndex].x + areas[areaIndex].w]].check == 1) continue;

			neighborAreaNum[1] = areaNumber[y][areas[areaIndex].x + areas[areaIndex].w];
			if (neighborAreaNum[0] < neighborAreaNum[1]) {
				neighborAreaNum[0] = neighborAreaNum[1]; //一番大きな番号を[0]に格納
				nextDirection = 4;
			}
		}
	}

	//次のエリアが見つからない場合、
	if (neighborAreaNum[0] == -1) {
		connectErrorCount++;
		if (connectErrorCount >= 10) return;
		orderRoom(order[orderIndex - 1]); //ひとつ前のエリアに戻ってチェックされていないエリアを探す

	}

	//次のエリアが見つかった場合、
	if (neighborAreaNum[0] != -1) {
		areas[neighborAreaNum[0]].check = 1; //次のエリアにチェックを入れる
		order[orderIndex + 1] = neighborAreaNum[0];
		orderIndex++;


		//ここからコリドーを伸ばしていく
		int d1x, d1y, d2x, d2y; //d1:移動前のドアの座標 d2:移動後のドアの座標
		//両ルーム内の適当な点を1つずつ取る
		d1x = random(areas[order[orderIndex - 1 - connectErrorCount]].room.x, areas[order[orderIndex - 1 - connectErrorCount]].room.x + areas[order[orderIndex - 1 - connectErrorCount]].room.w - 1);
		d1y = random(areas[order[orderIndex - 1 - connectErrorCount]].room.y, areas[order[orderIndex - 1 - connectErrorCount]].room.y + areas[order[orderIndex - 1 - connectErrorCount]].room.h - 1);
		d2x = random(areas[order[orderIndex]].room.x, areas[order[orderIndex]].room.x + areas[order[orderIndex]].room.w - 1);
		d2y = random(areas[order[orderIndex]].room.y, areas[order[orderIndex]].room.y + areas[order[orderIndex]].room.h - 1);


		connectErrorCount = 0;



		//各ルームからコリドーを伸ばしていって同じ位相まで行く
		//その際に進んだ経路は穴を掘るようになるので、CELL_TYPE_NONEを代入
		switch (nextDirection) {
		case 1:
			while (d1y != d2y) {
				if (abs(d1y - d2y) == 1) {
					d1y--;
					field[d1y][d1x] = CELL_TYPE_NONE;

				}
				else {
					d1y--, d2y++;
					field[d1y][d1x] = CELL_TYPE_NONE;
					field[d2y][d2x] = CELL_TYPE_NONE;
				}
			}

			if (d1y == d2y) {
				while (d1x != d2x) {
					if (d1x < d2x) d1x++;
					else d1x--;
					field[d1y][d1x] = CELL_TYPE_NONE;
				}
			}

			break;
		case 2:
			while (d1y != d2y) {
				if (abs(d1y - d2y) == 1) {
					d1y++;
					field[d1y][d1x] = CELL_TYPE_NONE;
				}
				else {
					d1y++, d2y--;
					field[d1y][d1x] = CELL_TYPE_NONE;
					field[d2y][d2x] = CELL_TYPE_NONE;
				}
			}

			if (d1y == d2y) {
				while (d1x != d2x) {
					if (d1x < d2x) d1x++;
					else d1x--;
					field[d1y][d1x] = CELL_TYPE_NONE;
				}
			}

			break;
		case 3:
			while (d1x != d2x) {
				if (abs(d1x - d2x) == 1) {
					d1x--;
					field[d1y][d1x] = CELL_TYPE_NONE;
				}
				else {
					d1x--, d2x++;
					field[d1y][d1x] = CELL_TYPE_NONE;
					field[d2y][d2x] = CELL_TYPE_NONE;
				}
			}

			if (d1x == d2x) {
				while (d1y != d2y) {
					if (d1y < d2y) d1y++;
					else d1y--;
					field[d1y][d1x] = CELL_TYPE_NONE;
				}
			}

			break;
		case 4:
			while (d1x != d2x) {
				if (abs(d1x - d2x) == 1) {
					d1x++;
					field[d1y][d1x] = CELL_TYPE_NONE;
				}
				else {
					d1x++, d2x--;
					field[d1y][d1x] = CELL_TYPE_NONE;
					field[d2y][d2x] = CELL_TYPE_NONE;
				}
			}

			if (d1x == d2x) {
				while (d1y != d2y) {
					if (d1y < d2y) d1y++;
					else d1y--;
					field[d1y][d1x] = CELL_TYPE_NONE;
				}
			}

			break;
		default: break;
		}
	}

	if (orderIndex + 1 == areaCount) return;
	orderRoom(neighborAreaNum[0]);
}

f:id:sion2000114:20190907012052j:plain
f:id:sion2000114:20190907012107j:plain
f:id:sion2000114:20190907012119j:plain
f:id:sion2000114:20190907012129j:plain
f:id:sion2000114:20190907012140j:plain

おわりに

ダンジョン自動生成はもうほぼ完成となりましたが、まあなんとも難しかったです。特に再帰処理でバグが酷かったのでバグを取り除くのにとても苦労しました。今は頭の中に思い描いたことを直感的にプログラムにしただけなので効率的かと言われれば恐らくそうではないと思います。本場のゲームプログラマーは最高効率で動くような工夫を凝らしているので、改めて凄いなあと感じます。まあそこまで追い求めるつもりはないので、余力があれば。。。
とにかく!大切なのは思いついたことをプログラムに直す力です。これまでに書いたプログラムを参考にしつつ考え方及びそれに対応するプログラムの書き方を知れば発展の余地は沢山あると思います。
疲れました。お疲れ様でした。

vol.8 ルーム作成

目次

はじめに

ダンジョンの自動生成をするということで、前回はエリアの分割を行いました。
今回は
①エリアの分割
②ルームの作成←←
③ドアの設置
④廊下(コリドー、Corridor)の作成

②のルームの作成に入ります。

ルームの作成(1)

分割されたエリアの中にルームを作成していきます。
エリアにつき1つだけルームを作成します。(そもそもエリアという概念を追加したのは部屋が重ならないようにするためだから、エリアにつき1つだけ作るのは当たり前だけども。。。)
前回、displayArea関数やspritArea関数などのエリアを分割し、可視化する関数を作成しましたが、目的はルームを作成することなのでdisplayArea関数とはここでお別れとなります。代わりに、generateField関数を作成して実際にエリア内にルームを作成し、描画したいです。なので、generateField関数の中にspritArea関数を入れておいて、一度に実行できるようにしましょう。
main関数の冒頭に書いてあった

areaCount=0;
・・・
spritArea(0);

までを全部generateField関数の中にコピペしてください。
また、関数の優先順位等が怪しくなってくるので、今後は関数はプロトタイプ宣言で作成していきます。

今回新たに準備したものは
・ルームの構造体

typedef struct{
        int x;
        int y;
        int w;
        int h;
} ROOM_t;

・フィールドのどこに何があるかを数字で管理するための配列

int field[FIELD_HEIGHT[]FIELD_WIDTH];

・描画する記号の個数

#define CELL_COUNT 個数

・実際に描画する記号を管理するための配列

char cellSymbol[CELL_COUNT][3];

・フィールドを生成する関数

void generateField(void);

です。
1か0でそれぞれ壁があるかないかを判断(それをfieldに保存しておく)して、
1か0かに応じて記号を描画していく(cellSymbol[]をprintfする)ことが最終目標です。

generateField関数を以下に書きます。

void generateField(void)
{
	int i, x, y;
	//最初のエリアの情報
	//つまり、エリアの初期化
	areaCount = 0;
	areas[0].x = 0;
	areas[0].y = 0;
	areas[0].w = FIELD_WIDTH;
	areas[0].h = FIELD_HEIGHT;
	areaCount++;
	spritArea(0);

	//まず、フィールドをすべて壁(1)で埋める。あとで部屋の部分に穴をあけていく。
	for (y = 0; y < FIELD_HEIGHT; y++){
		for (x = 0; x < FIELD_WIDTH; x++){
			field[y][x] = CELL_TYPE_WALL;
		}
	}

	//ここでルームを作成している!
	//簡単にするために、各ルームは各エリアの座標+2、縦横の長さ-4としている
	for (i = 0; i < areaCount; i++){
		areas[i].room.x = areas[i].x + 2; //i番目のエリアにあるルームのx座標について
		areas[i].room.y = areas[i].y + 2; //i番目のエリアにあるルームのy座標について
		areas[i].room.w = areas[i].w - 4; //i番目のエリアにあるルームの幅について
		areas[i].room.h = areas[i].h - 4; //i番目のエリアにあるルームの高さについて
		//ルームの上から下まで
		for (y = areas[i].room.y;y< areas[i].room.y + areas[i].room.h; y++){
			//ルームの左から右まで
			for (x = areas[i].room.x; x < areas[i].room.x + areas[i].room.w; x++){
				field[y][x] = CELL_TYPE_NONE; //ルームの部分だけ穴が開く(ルームでのfieldが0になった)
			}
		}
	}
}

ここで行っているのは、分割されたエリアに対して、座標+2、縦横の長さ-4としたルームの作成です。ルームの作成というのは結局のところ、各ルームの座標と縦横の長さをareas.roomに格納することなので、エリアを基準にルームの情報を取得してやればOKです。
areas[i].room.xというのは、areas[i]の中にある「roomの中にあるx」のことを指します。構造体の中に構造体が入っているような状況ではこのようにドット演算子を連続して使うことができます。

最初にフィールドをすべて壁(CELL_TYPE_WALL)としています。
作成されたルームの情報をもとに、ルームの範囲だけfield[]にCELL_TYPE_NONEを代入してあげればルームの部分だけ穴が開くことになり、無事にルームの部分が空洞になりました。
このようにフィールドをすべて壁で満たしてからルームをくり抜く手法を「穴掘り法」とか言ったりするらしいです。
f:id:sion2000114:20190907005944p:plain

ルームの作成(2)

さて、このgenerateField関数の構成を把握したところで、いよいよルームの場所を乱数で決めていきましょう。今はずいぶん均等な部屋になってしまっているので、どうもダンジョンっぽくありません。構成はこのままでルームのランダム生成を実装しましょう!
ここで、今後のことも考えると通路の分の余白は保っておいたほうが良さそうなので、少なくとも今作成したルーム以下の大きさにしましょう。考え方としては、今作成したルームを基準としてもう少しルームを小さくしていきます。

少し、計算してみましょう。(ここは自力で考えてみてもいいかも?)
今生成できる最小のルームの大きさはAREA_SIZE_MIN-4(縦)×AREA_SIZE_MIN-4(横)です。数値としては12×12となるので、ここからさらに小さいルームを作成するということで、ルームの最小値を定義します。

#define ROOM_SIZE_MIN (AREA_SIZE_MIN - 4 - 4)

さて、今作成したルームについて、(x,y),w×hを満たしているとしましょう。
そこからルームを小さくするので、新しく作成されるルームのx座標をx+r1,y座標をy+r2とします。ここで、r1,r2は非負整数とします。(つまり、r1,r2の分だけ右下にずらすということ)
新しく作成されたルームは8×8以上の大きさでなければならないので、
w-r1>=8,
h-r2>=8
を満たさなければなりません。
r1,r2が非負整数であることに注意すれば、
0<=r1<=w-8
0<=r2<=h-8
と求められます。
よって平行移動用の乱数の範囲は
0<=random<=w-8,(x軸方向)
0<=random<=h-8 (y軸方向)
となりました。

次に、新しく作成するルームの縦横の長さをそれぞれh',w'としましょう。
まず、ルームの条件を満たしていないといけないので、8<=h',8<=w'です。
さらに、もとのルームから飛び出してはいけないので、h'の最大値はh-r2,w'の最大値はw-r1となります。
よって
8<=w'<=w-r1
8<=h'<=h-r2
と求められます。
よって幅用の乱数の範囲は
8<=random<=w-r1,(横方向)
8<=random<=h-r2(縦方向)
となりました。

さくっと範囲付きの乱数を生成する関数を作って実装しましょう!

以下、ソースコード

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<time.h>
#include<conio.h>

#define FIELD_HEIGHT 64
#define FIELD_WIDTH 64
#define AREA_MAX 64 //エリアの最大個数
#define AREA_SIZE_MIN 16 //エリア
#define CELL_COUNT 2
#define ROOM_SIZE_MIN (AREA_SIZE_MIN-4-4)

enum{
	CELL_TYPE_NONE, //0
	CELL_TYPE_WALL  //1
};

//ルームの座標・横幅・高さをひとまとめに管理するために構造体ROOM_tで定義
typedef struct{
	int x;
	int y;
	int w;
	int h;
}ROOM_t;

//エリアの座標・横幅・高さをひとまとめに管理するために構造体AREA_tで定義
//また、エリア内にルームを作成するのでルームもここで管理します。
typedef struct{
	int x;
	int y;
	int w; //width
	int h; //height
	ROOM_t room;
}AREA_t;

AREA_t areas[AREA_MAX]; //各エリアの座標・横幅・高さの情報をしまっておける
int areaCount; //エリアの個数
int field[FIELD_HEIGHT][FIELD_WIDTH];
char cellSymbol[CELL_COUNT][3] = {
	"・", //CELL_TYPE_NONE(0)番目の配列
	"■" //CELL_TYPE_WALL(1)番目の配列
};

int random(int, int);
void spritArea(int);
void generateField(void);

int main(void)
{
	int x, y;
	srand((unsigned int)time(NULL));
	while (1){
		//フィールドの生成
		generateField();

		system("cls");
		//フィールドの描画
		for (y = 0; y < FIELD_HEIGHT; y++){
			for (x = 0; x < FIELD_WIDTH; x++){
				/*
				field[y][x]でフィールドの情報を数字で取り出し、
				その数字に応じて対応した記号をcellSymbolで取り出し、
				それを描画
				fieldにはenumで定義したものしか入っていないので
				*/
				printf("%s", cellSymbol[field[y][x]]);
			}
			printf("\n");
		}

		_getch();
	}
	return 0;
}

int random(int start, int end)
{
	return rand() % (end - start + 1) + start;
}

//エリアを分割して、エリア毎に座標・横幅・高さを取得する関数
//oldAreaIndex : 分割前のエリア番号
//newAreaIndex : 分割後のエリア番号
void spritArea(int oldAreaIndex)
{
	int newAreaIndex = areaCount;
	//分割をキャンセルする時に戻せるようにする(再帰処理なので、終了条件を満たしたときに直前の分割をキャンセルしたい)
	int w = areas[oldAreaIndex].w; //保存用
	int h = areas[oldAreaIndex].h; //保存用

	//縦切り
	if (rand() % 2 == 0){
		areas[oldAreaIndex].w /= 2; //半分
		areas[newAreaIndex].x = areas[oldAreaIndex].x + areas[oldAreaIndex].w; //新しいエリアのx座標
		areas[newAreaIndex].y = areas[oldAreaIndex].y; //新しいエリアのy座標
		areas[newAreaIndex].w = w - areas[oldAreaIndex].w; //新しいエリアの横幅
		areas[newAreaIndex].h = areas[oldAreaIndex].h; //新しいエリアの高さ
	}
	//横切り
	else {
		areas[oldAreaIndex].h /= 2; //半分
		areas[newAreaIndex].x = areas[oldAreaIndex].x; //新しいエリアのx座標
		areas[newAreaIndex].y = areas[oldAreaIndex].y + areas[oldAreaIndex].h; //新しいエリアのy座標
		areas[newAreaIndex].w = areas[oldAreaIndex].w; //新しいエリアの横幅
		areas[newAreaIndex].h = h - areas[oldAreaIndex].h; //新しいエリアの高さ
	}
	//分割後の二つのエリアがエリアの条件(AREA_SIZE_MINの範囲内かどうか)を満たしているか
	if ((areas[oldAreaIndex].w < AREA_SIZE_MIN) || (areas[oldAreaIndex].h < AREA_SIZE_MIN)
		|| (areas[newAreaIndex].w < AREA_SIZE_MIN) || (areas[newAreaIndex].w < AREA_SIZE_MIN)) {
		areas[oldAreaIndex].w = w;
		areas[oldAreaIndex].h = h;
		return; //void型関数を終了させたいときは、単にreturnと書けばよい
	}

	areaCount++;

	spritArea(newAreaIndex); //新エリアの分割
	spritArea(oldAreaIndex); //旧エリアの分割

}

void generateField(void)
{
	int i, x, y;
	//最初のエリアの情報
	//つまり、エリアの初期化
	areaCount = 0;
	areas[0].x = 0;
	areas[0].y = 0;
	areas[0].w = FIELD_WIDTH;
	areas[0].h = FIELD_HEIGHT;
	areaCount++;
	spritArea(0);

	//まず、フィールドをすべて壁(1)で埋める。あとで部屋の部分に穴をあけていく。
	for (y = 0; y < FIELD_HEIGHT; y++){
		for (x = 0; x < FIELD_WIDTH; x++){
			field[y][x] = CELL_TYPE_WALL;
		}
	}

	//ここでルームを作成している!
	for (i = 0; i < areaCount; i++){
		//まずは元となるルームを作成
		areas[i].room.x = areas[i].x + 2;
		areas[i].room.y = areas[i].y + 2;
		areas[i].room.w = areas[i].w - 4;
		areas[i].room.h = areas[i].h - 4;
		//ここからルームをランダムに小さくします。
		int r1 = random(0, areas[i].room.w - ROOM_SIZE_MIN);
		int r2 = random(0, areas[i].room.h - ROOM_SIZE_MIN);

		areas[i].room.x += r1;
		areas[i].room.y += r2;
		areas[i].room.w = random(ROOM_SIZE_MIN, areas[i].room.w - r1);
		areas[i].room.h = random(ROOM_SIZE_MIN, areas[i].room.h - r2);
		
		//ルームの上から下まで
		for (y = areas[i].room.y; y < areas[i].room.y + areas[i].room.h; y++){
			//ルームの左から右まで
			for (x = areas[i].room.x; x < areas[i].room.x + areas[i].room.w; x++){
				field[y][x] = CELL_TYPE_NONE; //ルームの部分だけ穴が空く(ルームでのfieldが0になった)
			}
		}
	}
}

f:id:sion2000114:20190907010010p:plain

おわりに

基本的に今回やったのはgenerageField関数を作っただけです。
簡単に言いますが正直結構難しいです。
最近結構な頻度でやってますが、皆さんはゆっくり真似しながらやっていってくださいねー。
どの段階の話でも構わないので、わからないところがあったら聞いてくださいな。
時間のある時にゆっくりやりましょう。

次回は各ルームにドアを設置します。コリドーを通します。
この話はコリドー(廊下)の作成の話と直結するので、ドアをどのように設置するかがポイントとなります。せっかくドアを作ってもコリドーが通らなければ話になりませんからね。
ではでは、お疲れさまでした。

vol.7 ローグライクゲームの実装 エリアの分割

目次

はじめに

vol.1~vol.6までの内容で、基本的なRPGの概形は構成できます。もちろんまだすごく目の粗い説明しかしてませんので大それたゲームには至りませんが、依然と比べればゲームプログラムに関する知識が随分と備わったと思います。(ちゃんとvol.6までの内容を理解していればの話。)
今回からはRPGの一部である「ローグライクゲーム」を実装していきたいと思います。
といってもローグライクゲームという名称はあまり耳にしないと思うので、まずはこのローグライクゲームについて説明を入れていきます。(知ってる人は読み飛ばしてくれて構いませんよー。)

ローグライクゲームとは

ローグライクゲームを一言で表すなら、ダンジョンを攻略するゲームとなります。
通常のRPGでは世界地図を片手に色々な場所に訪れ、それぞれのイベントを通して伝説の武具を手に入れて、ラスボスに挑む...みたいな構成が多いとは思います。今回のこのローグライクゲームは世界を冒険するわけではなく、一つの建物の中に生成されるダンジョンを攻略していき、最終的にお宝を持ち帰ることができればゲームクリアとなります。
ところが、歴史的にはあまりゲームクリアをさせるつもりはなく、どこまでフロアを進め、高いスコアを獲得するかを競うケースが多かったです。それだけ難易度が高かったということですね。
ローグライクゲームの大きな特徴として次のものが挙げられます。
・ランダム生成ダンジョン(いわゆる、不思議のダンジョンと呼ばれるもの)
・ターン制の戦闘システム
・ノンモーダル
・シングルプレイヤーゲーム
・所持金などに応じて生じるスコア
・空腹要素
・クリア後の初期化

順に説明を加えていきます。

ローグライクゲームでのダンジョンは自動生成されたもので、階層を移動するたびに異なったマップが生成されます。ダンジョン自体は何階もあるので、マップデータをいちいち用意していては大変です。
ちなみに、敵キャラもマップ上に自動生成されます。

・ターン制の戦闘システムということで、これは通常のRPGでも多いケースですね。
次に書いてあるノンモーダルというのが大きな特徴となります。
ノンモーダルというのは、移動と戦闘で画面変遷を伴わないということです。わかりやすく言うと、戦闘画面がないということです。通常の移動フィールド上でそのまま戦闘が始まります。これはvol.2で書いたビーム照射がそれにあたります。
ターンはプレイヤーが行動すると経過します。つまり、プレイヤーは動かない限り敵も動かないということです。これは戦闘でも同様のことが言えます。

・シングルプレイヤーゲームということで、これは読んで字のごとくですね。
仲間はおらず、主人公一人でゲームが進行します。

・スコアについてですが、詳しいスコア算出方法を知らないので何とも言えないのですが、スコアは高いほうがいいですよね。ゲーム終了時にスコアが確定します。

空腹要素があるのがローグライクゲームにおける大きな特徴となります。プレイヤーは定期的に食事をとらないと最終的には餓死します。食糧はダンジョン内に落ちていたり、店で購入できたりします。

・初期化ということで、ぎょっとした人もいるのではないでしょうか。ローグライクゲームではダンジョンをクリアすることが第一目標なので、一度ダンジョンをクリアしてしまえば再びダンジョンに入るときはレベルは1からになります。常にダンジョンはレベル1からスタートするということです。
ちなみに所持金は持ち帰ることができ、アイテムもすべて売却し、家の貯金になります
そのあたりの細かい説明はあとにしましょう。

さて、このようなゲーム機構を実装していく次第ですが、今回はゲームの継続性(ダンジョンをクリアしてお金を家にためて、またダンジョンに行く)を意識していきたいので、従来のローグライクゲームのように激ムズにはしません。ちゃんとクリアできるものを作成します。

トルネコの大冒険 不思議のダンジョン

ローグライクゲームの代表作として古くからあるのがトルネコの大冒険です。
トルネコドラクエIVのキャラとして登場していました。その後日譚(その後の物語)ということになります。ストーリーを以下に記します。

レイクナバに住む武器屋トルネコ。彼は武器屋の雇われだったが、一つの夢があった。自分の店を持ちその店を世界一にすること。トルネコは旅に出て、鉄の金庫を探したり、ボンモールへ行ったりと様々な仕事をこなし、ついにエンドールに店を持つ。妻ネネの働きもあり、トルネコの店は大繁盛。そんな折、トルネコは「不思議なダンジョンには誰も見たことも無いようなすごいお宝が眠っているらしい」という噂を耳にする。

トルネコはこれを聞いて不思議のダンジョンに行ってみたいと思うが、店や家族のことを考えると悩んでしまう。妻のネネはそんなトルネコに理解を示し、不思議なダンジョンに行くことを決定する。トルネコと妻のネネ、息子のポポロは海を渡り、山を越え、何年も旅を続け、ついに不思議のダンジョンにたどり着く。

不思議のダンジョンの近くにある新たな村の一本の大樹のそばにトルネコは店を構える。

この国の王様から不思議のダンジョン探索の許可を得ようとするが、王様は危険だと首を縦に振らない。しかしトルネコは引き下がらず、王様は「ちょっと不思議のダンジョン」から宝石箱を取って帰れたら許可を出すと約束する。トルネコは王様の宝石箱を持ち帰り、不思議のダンジョン探索の許可を得る。
(引用:トルネコの大冒険 不思議のダンジョン - Wikipedia


まあこんな流れで不思議のダンジョンに行くことになります。
このゲームのメインは、ダンジョンを攻略し、ゴールドを貯めて自分の店舗を成長させていくことです。
これらをもとにプログラムを実際に書いていきたいと思います。
プログラムの詳細も大事ですが、どのようにゲームを構成していくかという手順を意識していきたいと思います。

ダンジョンの自動生成実装について

※注意
ダンジョンの自動生成が最初にして最大の山です。
おそらく長期化するので今回は途中段階で終了となります。
自分も調べながらやっていますが、何せC言語での資料が少ないので手探りになります。
ご了承くださいな。

ダンジョンの自動生成を実装するために、4つのプロセスを立てます。
①エリアの分割
②ルームの作成
③ドアの設置
④廊下(コリドー、Corridor)の作成

※関数で機能を分けていこうと思うので、フィールド関連の変数はグローバル変数(main関数の外)で宣言してあげると扱いやすくなります。
フィールドの配列を操作したいときにいちいち引数に配列のポインタを渡すのはめんどくさいので、引数に渡さなくてもいつどこからでも配列を操作できるようにすればだいぶ楽になります。

エリアの分割

エリアの分割をする理由は、ルームを作成するにあたってルームが重なってしまうとよくないので、エリアに分割して各エリア内にルームを作成できるようにしたいからです。
簡略化のために分割は半分ずつとしましょう。
エリアを分割することによって複数個のエリアが作成されますが、エリアを区別するためにそれらのエリアの座標(左上の点を見る)と横幅・高さがわかれば良いので、それを関数spritAreaで実装しましょう。

spritArea :
簡略化のために分割は半分ずつとします。
縦・横のどちらで切るかは乱数(0 or 1)で決定して、分割された上・左のエリアを「旧エリア」、下・右のエリアを「新エリア」と呼ぶことにします。
旧エリアと新エリアに分割したら、その二つを更に分割する処理を行いたいので、これには再帰処理が有効です。
これにあたって再帰処理には終了条件が必要なので、エリアの最小の大きさを定義しておきます。

分割しても目に見えなければ正常に動作しているか分からないので、分割しているかどうかを確認するデバッグ用の関数displayAreaも作成します。
displayArea :
spritAreaで取得したエリアの情報をもとに分割された順番にその順番の数字をエリア領域に書き込みます。具体的にはこのような感じになればOKです。
最初のエリアはすべて0のものとして、そこから1, 2,...と分割されたエリアに対して番号を振っていきます。

例:縦16、横16、最小のエリア幅4で定義されたエリア分割

0 0 0 0 6 6 6 6 4 4 4 4 5 5 5 5
0 0 0 0 6 6 6 6 4 4 4 4 5 5 5 5
0 0 0 0 6 6 6 6 4 4 4 4 5 5 5 5
0 0 0 0 6 6 6 6 4 4 4 4 5 5 5 5
8 8 8 8 7 7 7 7 4 4 4 4 5 5 5 5
8 8 8 8 7 7 7 7 4 4 4 4 5 5 5 5
8 8 8 8 7 7 7 7 4 4 4 4 5 5 5 5
8 8 8 8 7 7 7 7 4 4 4 4 5 5 5 5
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1  
2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3
2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3
2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3
2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3  

※因みにこの時のエリア情報は、
( x, y) w × h
_____________________________
areas[ 0] : ( 0, 0) 4 × 4
areas[ 1] : ( 0, 8) 16 × 4
areas[ 2] : ( 0, 12) 8 × 4
areas[ 3] : ( 8, 12) 8 × 4
areas[ 4] : ( 8, 0) 4 × 8
areas[ 5] : (12, 0) 4 × 8
areas[ 6] : ( 4, 0) 4 × 4
areas[ 7] : ( 4, 4) 4 × 4
areas[ 8] : ( 0, 4) 4 × 4

分割の過程を絵で表現してみたので見て下さいな。
f:id:sion2000114:20190906110503p:plain

以下、ソースコード

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<time.h>
#include<conio.h>

#define FIELD_HEIGHT 64
#define FIELD_WIDTH 64
#define AREA_MAX 64 //エリアの最大個数
#define AREA_SIZE_MIN 16 //エリア

//エリアの座標・横幅・高さをひとまとめに管理するために構造体AREA_tで定義
typedef struct{
	int x;
	int y;
	int w; //width
	int h; //height
}AREA_t;

AREA_t areas[AREA_MAX]; //各エリアの座標・横幅・高さの情報をしまっておける
int areaCount; //エリアの個数

//エリアを分割して、エリア毎に座標・横幅・高さを取得する関数
//oldAreaIndex : 分割前のエリア番号
//newAreaIndex : 分割後のエリア番号
void spritArea(int oldAreaIndex)
{
	int newAreaIndex = areaCount;
	//分割をキャンセルする時に戻せるようにする(再帰処理なので、終了条件を満たしたときに直前の分割をキャンセルしたい)
	int w = areas[oldAreaIndex].w; //保存用
	int h = areas[oldAreaIndex].h; //保存用

	//縦切り
	if(rand()%2==0){
		areas[oldAreaIndex].w /= 2; //半分
		areas[newAreaIndex].x = areas[oldAreaIndex].x + areas[oldAreaIndex].w; //新しいエリアのx座標
		areas[newAreaIndex].y = areas[oldAreaIndex].y; //新しいエリアのy座標
		areas[newAreaIndex].w = w - areas[oldAreaIndex].w; //新しいエリアの横幅
		areas[newAreaIndex].h = areas[oldAreaIndex].h; //新しいエリアの高さ
	}
	//横切り
	else {
		areas[oldAreaIndex].h /= 2; //半分
		areas[newAreaIndex].x = areas[oldAreaIndex].x; //新しいエリアのx座標
		areas[newAreaIndex].y = areas[oldAreaIndex].y + areas[oldAreaIndex].h; //新しいエリアのy座標
		areas[newAreaIndex].w = areas[oldAreaIndex].w; //新しいエリアの横幅
		areas[newAreaIndex].h = h - areas[oldAreaIndex].h; //新しいエリアの高さ
	}
	//分割後の二つのエリアがエリアの条件(AREA_SIZE_MINの範囲内かどうか)を満たしているか
	if ((areas[oldAreaIndex].w < AREA_SIZE_MIN) || (areas[oldAreaIndex].h < AREA_SIZE_MIN)
		|| (areas[newAreaIndex].w < AREA_SIZE_MIN) || (areas[newAreaIndex].w < AREA_SIZE_MIN)) {
		areas[oldAreaIndex].w = w;
		areas[oldAreaIndex].h = h;
		return; //void型関数を終了させたいときは、単にreturnと書けばよい
	}

	areaCount++;

	spritArea(newAreaIndex); //新エリアの分割
	spritArea(oldAreaIndex); //旧エリアの分割
	
}

//デバッグ用
void displayArea() {
	int x, y, i;
	int field_num[FIELD_HEIGHT][FIELD_WIDTH] = { 0 };

	for (i = 0; i < areaCount; i++) //全ての分割されたエリアに対して
		for (y = areas[i].y; y < areas[i].y + areas[i].h; y++) //エリアの上から下まで
			for (x = areas[i].x; x < areas[i].x + areas[i].w; x++) //エリアの左から右まで
				field_num[y][x] = i; //iの番号でエリアを区別してみる

	system("cls");
	for (y = 0; y < FIELD_HEIGHT; y++) {
		for (x = 0; x < FIELD_WIDTH; x++) {
			printf("%2d", field_num[y][x]);
		}
		printf("\n");
	}
}

int main(void)
{
	int y;
	srand((unsigned int)time(NULL));

	//最初のエリアの情報
	//つまり、エリアの初期化
	areaCount = 0;
	areas[0].x = 0;
	areas[0].y = 0;
	areas[0].w = FIELD_WIDTH;
	areas[0].h = FIELD_HEIGHT;
	areaCount++;

	spritArea(0);
	displayArea();

	//エリアの情報確認用
	for (y = 0; y < areaCount; y++) {
		printf("areas[%2d] : (%2d, %2d) %2d × %2d\n",y, areas[y].x, areas[y].y, areas[y].w, areas[y].h);
	}

	_getch();
	return 0;
}

おわりに

今回は、ローグライクゲームの説明をして実際にエリアの分割まで行いました。
そこそこ難しくなってきているので、1つ1つ確実に把握していくことが大切ですね。
結局ゲームを作る時に自分の中で思い描いた実装のプロセスをいかにプログラムに書けるかが大切になってくるので、プログラム内容を理解するというよりは「こういう書き方もあるんだな」という把握の方が大切です。
今回のプログラム内に、「デバッグ」と書かれたものがありましたが、これがとても大切になってきます。実際に作りたいプログラムを作る前に一つ一つの動作がちゃんと正常に行われるかを確認することデバッグと言います。エリア分割を実装する最終目標は、分割されたエリアごとの座標と縦横の長さを取得することです。別に画像のように数字で出力する必要はないのですが、出力してちゃんと目で見えるようにしておけば作ってる側としてもわかりやすいので、ミスが減ります。(というか、今回はこれを使ってちゃんと確認しながら作らないとほぼ無理。)
一気にプログラムを書こうとせず、パーツごとにしっかり確認しながら進めるようにしましょう。
次回は、作成したエリアの中にルームを作成していきたいと思います。
お疲れ様でした。

vol.6 関数(ポインタ、構造体使用)、モードによる場面転換(列挙型)

目次

はじめに

今回からついに関数についてやっていきたいと思います。
といっても普通の数値に関する関数は皆さん実装できると思うので、今回はポインタ絡みの関数についてやっていけたらなと思います。
ポインタ絡みということで、構造体に出てきたアロー演算子についても関数で使っていきたいですね。
また、関数毎にゲームシーンを分ける場合があるので、直接関数に関係はありませんが、少し新しいことを導入したいと思います。

関数でのポインタ使用

まず基本的にポインタ絡みの関数を実装する際に、必ずしも関数が値を返してほしいわけではないということを念頭に置きましょう。そういった場合には関数をvoid型で宣言します。
void型で宣言した関数は戻り値を持たないので、関数内で動作をひとしきりした後、何も返さずに関数を終了してくれます。
ところが、何も返さない関数など何に使えるのかと思う人はいると思います。
void型関数は主に2種類の用途があって
①単純に動作をまとめておきたい場合
②複数の値を返したい場合

があります。return ●; では、戻り値が1つしか設定できないので、通常の関数の考え方では実装できないということです。
今回は②の場合について考えていきたいと思います。
複数の値を返してほしい場合、「ポインタ」を使うことになります。
ポインタを介して複数の値を変更することで実質②を適えるというわけです。この時に、returnの箇所は不要となるので、void型関数で宣言することになります。

さて、早速サンプルプログラムを見ていましょう。

例:足し算した結果と引き算した結果を同時に返すプログラム

#include<stdio.h>

void add_sub(int a, int b, int *p, int *q)
{
	*p=a+b;
	*q=a-b;
	//実際return していなくでも*p,*qを介して値を複数返しているような状況になる
}

int main(void)
{
	int a=2, b=3;
	int p, q; //p:足し算の結果 q:引き算の結果
	add_sub(a,b,&p,&q);
	printf("%d %d\n",p,q);

	return 0;
}

関数での構造体使用

関数の引数に構造体を使うことも可能です。実際にこれを使えば構造体で定義したプレイヤーの体力などを変動させることができます。実際に例を見てみましょう。

例:敵にxポイントのダメージを与えるプログラム

#include<stdio.h>

typedef struct{

    int HP;

}player_t; //プレイヤータイプの略


void damage(int x, player_t *enemy) //型宣言の player_tに注意
{
	(enemy->HP) -=x; //enemyのHPを「enemy->HP」で表している。それをxだけ減らす
}

int main(void)
{
	player_t enemy={100};
	printf("減少前:%d\n",enemy.HP); //出力結果:減少前:100

	int x=30;
	damage(x,&enemy);
	printf("減少後:%d\n",enemy.HP); //出力結果:減少後:70

	return 0;
}

このようにアロー演算子を使うことによってポインタと同等の動作をすることができます。

ちなみに、前回の戦闘シーンでこうげきとかいふくの動作を関数にするとこのようになります。

void atack(PERSON player1, PERSON *player2)
{
	int db=rand_bonus(player1.ATK /10); //ダメージ増減分 ダメージボーナスの略
	PERSON *p2;
	p2 = player2;
	if ((p2->def_flag) == 1){ //防御判定があれば
		(p2->HP) -= (player1.ATK + db) / 2; //ダメージ半減
		(p2->def_flag) = 0; //防御判定リセット
	}
	else (p2->HP) -= player1.ATK + db;
	if ((p2->HP) < 0) (p2->HP) = 0; //HPは非負整数なので、最小値は0
	printf("%sの残りHP %d\n", p2->name, p2->HP);
}

void heal(PERSON *player)
{
	int heal_base = 30; //基本回復量
	PERSON *p;
	p = player;
	if ((p->MP) < 4)
		printf("しかしMPが足りない!\n");
	
	else{
		(p->MP) -= 4; //MPを減らす
		(p->HP) += heal_base + rand_bonus(3); //体力回復
		printf("%sの体力は%dに回復した!\n", p->name, p->HP);
		printf("残りMPは%d\n", p->MP);
	}
}

モードの管理

ここからは関数自体から少し離れて、場面転換に着目したいと思います。
場面転換についてはvol.3でやりましたが、ゲームでは複数の場面転換が存在することが当たり前です。それらを区別するために「モード番号」という考え方をしてみましょう。
例えば、外フィールド画面、町フィールド画面、戦闘画面、メニュー画面の4種類がシーンがあるとしましょう。これらのシーン毎にモード番号を割り振ります。
今回はそれぞれ0,1,2,3番というモード番号を割り振ることにします。
シーン変遷を簡単なプログラムで実装してみましょう。

#include<stdio.h>
#include<conio.h>

void field(void);
void m_field(void);
void battle_field(void);
void menu_field(void);

int main(void)
{
	int mode=0; //外フィールドにいる

	switch (mode)
	{
	case 0:
		field();
		break;
	case 1:
		m_field();
		break;
	case 2:
		battle_field();
		break;
	case 3:
		menu_field();
		break;
	}

	_getch(); //待機

	mode = 3; //メニュー画面を開いた
	
	switch (mode)
	{
	case 0:
		field();
		break;
	case 1:
		m_field();
		break;
	case 2:
		battle_field();
		break;
	case 3:
		menu_field();
		break;
	}

	_getch();
	return 0;
}

void field(void)
{
	printf("ここは外フィールド\n");
}

void m_field(void)
{
	printf("ここは町フィールド\n");
}
void battle_field(void)
{
	printf("ここは戦闘画面\n");
}
void menu_field(void)
{
	printf("ここはメニュー画面\n");
}

このようにすればmodeの値によってシーンを変えることができます。

ところが、シーンの数が増えてくると、例えば

mode = 157;

としたときに一体何のシーンなのか把握しきれません。
そんなときに活躍するのが、今回新しく登場する「列挙型(enum)」と呼ばれるものです。
使い方としては構造体と似ているので早速構文を紹介します。

宣言

typedef enum{
	モード名0,
	モード名1,
	モード名2,
・・・
}列挙体名;

今回で言えば、

typedef enum{
	FIELD_TYPE_FIELD,
	FIELD_TYPE_M_FIELD,
	FIELD_TYPE_BATTLE,
	FIELD_TYPE_MENU
}mode_t;

とすれば、新しく「mode_t」型の列挙体を作成することになります。
この時、自動的に上から順に0,1,2,・・・と番号を振ってくれます。
CELL_TYPE_FIELDは0のことですし、FIELD_TYPE_BATTLEは2のことを指します。

これを使って先ほどのプログラムを書き換えるとこのようになります。

#include<stdio.h>
#include<conio.h>

void field(void);
void m_field(void);
void battle_field(void);
void menu_field(void);

typedef enum{
	FIELD_TYPE_FIELD,
	FIELD_TYPE_M_FIELD,
	FIELD_TYPE_BATTLE,
	FIELD_TYPE_MENU
}mode_t;

int main(void)
{
	mode_t mode=FIELD_TYPE_FIELD; //外フィールドにいる

	switch (mode)
	{
	case FIELD_TYPE_FIELD:
		field();
		break;
	case FIELD_TYPE_M_FIELD:
		m_field();
		break;
	case FIELD_TYPE_BATTLE:
		battle_field();
		break;
	case FIELD_TYPE_MENU:
		menu_field();
		break;
	}

	_getch(); //待機

	mode = FIELD_TYPE_MENU; //メニュー画面を開いた
	
	switch (mode)
	{
	case FIELD_TYPE_FIELD:
		field();
		break;
	case FIELD_TYPE_M_FIELD:
		m_field();
		break;
	case FIELD_TYPE_BATTLE:
		battle_field();
		break;
	case FIELD_TYPE_MENU:
		menu_field();
		break;
	}

	_getch();
	return 0;
}

void field(void)
{
	printf("ここは外フィールド\n");
}

void m_field(void)
{
	printf("ここは町フィールド\n");
}
void battle_field(void)
{
	printf("ここは戦闘画面\n");
}
void menu_field(void)
{
	printf("ここはメニュー画面\n");
}

このようにすれば、見ただけでどのシーンについて記述しているかわかりやすくなりますね。

※ちなみに、列挙体はわざわざ「mode_t」型を作成せずとも使うことができます。
例えば

enum{
	FIELD_TYPE_FIELD,
	FIELD_TYPE_M_FIELD,
	FIELD_TYPE_BATTLE,
	FIELD_TYPE_MENU
};

とだけ書けば純粋に上から0,1,2,3と数を割り当ててくれます。
同じプログラム内ならどこでも FIELD_TYPE_BATTLE は2という数字として取り扱ってくれるので、こっちの方が直感的で分かりやすいかもしれません。
これは

#define FIELD_TYPE_FIELD 0
#define FIELD_TYPE_M_FIELD 1
#define FIELD_TYPE_BATTLE 2
#define FIELD_TYPE_NMENU 3

とほとんど同様のことです。(見た目上は、ですけども)
列挙体を使うかどうかはお好みでどうぞー

おわりに

今までやってきた内容を関数で実装し、分岐も分かりやすく書く方法を伝授してきました。
ここまでくるとずいぶんたくさんの事をC言語で実装できるようになってるのではないかなと思います。
まだ今(9月6日現在)は手を付けてる人は少ないと思いますが、もし質問等あれば遠慮なくしてくださいね。
次回は何をしましょう。考えておきます。
では、お疲れ様でした。

vol.5 戦闘シーンの実装

目次

はじめに

今回は戦闘シーンの実装をしていきます。
実装する内容としては、
こうげき、かいふく、ぼうぎょ、にげる
の4つを考えていきます。
構造体を意識してプログラムを書いていきますよー。

戦闘シーン

今回構造体に導入するキャラクターデータは
name(名前)、HP(体力)、MP(魔力)、ATK(攻撃力)、def_flag(防御判定)
の5つにします。
ダメージは攻撃力に対して最大10%の増減があるものとします。
回復基本量は30で固定して、実際に回復する量には3の増減があるものとします。
回復時にはMPを4消費させます。
ぼうぎょは防御判定(1なら防御している)があるなら被ダメージを半分にします。
DEF(防御力)などを実装してもよかったのですが、計算が多くなってしまうので
今回は割愛します。
にげるについては、1/4の確率で成功することとしましょう。
0~3までの乱数で、0が出ればにげられるようにします。

#include<stdio.h>
#include<conio.h>
#include<stdlib.h>
#include<time.h>

typedef struct{
	char name[80]; //名前
	int HP; //体力
	int MP; //魔力
	int ATK; //攻撃力
	int def_flag; //防御判定
}PERSON;


//数値の増減量を計算します
int rand_bonus(int);
//逃走判定
int isescape(void);

int main(void)
{
	//seed値初期化
	srand((unsigned int)time(NULL));

	int cmd;//コマンド入力用
	int escape=0; //1なら戦闘終了、0なら戦闘継続
	int turn = 0; // 0:player[0] 1:player[1]。 2で割った余りを使っていく
	

	PERSON player[2] =
	{ 
		{ "司温", 100, 20, 35 },
		{ "魔王", 150, 8, 30 }
	};

	//戦闘開始!
	while (1){
		//最初のターンを除いて、player[0]のターンになったら画面をリセットします
		if (turn !=0 && turn % 2 == 0){
			_getch();
			system("cls");
		}
		//turn%2でどちらのプレイヤーが行動しているのか表現しています
		//適切なコマンドが入力されるまで繰り返します
		do{
			printf("%sのターン、どうする?\n", player[turn%2].name);
			printf("こうげき(1)・かいふく(2)・ぼうぎょ(3)・にげる(4) :");
		
			scanf("%d", &cmd);
		} while (!(cmd == 1 || cmd == 2 || cmd == 3 || cmd == 4));
		//コマンドの場合分け
		switch (cmd){

		//こうげき
		case 1:
			printf("%sのこうげき!\n", player[turn % 2].name);
			
			//ダメージ増減分 ダメージボーナスの略
			int db = rand_bonus(player[turn % 2].ATK / 10); 
			if (player[(turn + 1) % 2].def_flag == 1) { //防御判定があれば
				player[(turn + 1) % 2].HP -= (player[turn % 2].ATK + db) / 2; //ダメージ半減
				player[(turn + 1) % 2].def_flag = 0; //防御判定リセット
			}
			else player[(turn + 1) % 2].HP -= player[turn % 2].ATK + db;
			//HPは非負整数なので、最小値は0
			if (player[(turn + 1) % 2].HP < 0) player[(turn + 1) % 2].HP = 0; 
			printf("%sの残りHP %d\n", player[(turn + 1) % 2].name, player[(turn + 1) % 2].HP);
			if (player[(turn + 1) % 2].HP == 0){
				printf("%sは死んでしまった...\n", player[(turn + 1) % 2].name);
				escape = 1;
			}
			break;

		//かいふく
		case 2:
			printf("%sは回復魔法を唱えた!\n", player[turn % 2].name);
			
			int heal_base = 30; //基本回復量
			if (player[turn % 2].MP < 4) printf("しかしMPが足りない!\n");
			else {
				player[turn % 2].MP -= 4; //MP減少
				player[turn % 2].HP += heal_base + rand_bonus(3); //体力回復
				printf("%sの体力は%dに回復した!\n", player[turn % 2].name, player[turn % 2].HP);
				printf("残りMPは%d\n", player[turn % 2].MP);
			}
			break;
			
		//ぼうぎょ
		case 3:
			printf("%sは防御した!\n", player[turn % 2].name);
			if (player[turn % 2].def_flag != 1)
				player[turn % 2].def_flag = 1;
			break;

		//にげる
		case 4:
			printf("%sはにげだした!\n", player[turn % 2].name);
			if (isescape() == 1){
				escape = 1;
				break;
			}
			printf("しかし、回り込まれてしまった!\n");
			break;
		}

		//見づらいので横線を描画
		for (int i = 0; i < 50; i++) printf("_");
		putchar('\n');

		//戦闘終了判定
		if (escape == 1) break;
		
		//ターン経過
		turn++;
	}
	printf("%s%sに勝利した!\n", player[turn % 2].name, player[(turn + 1) % 2].name);
	
	_getch();
	return 0;
}

int rand_bonus(int value)
{
	return rand() % (1+2 * value) - value;
}

int isescape(void)
{
	if (rand() % 4 == 0) return 1; //0,1,2,3の中で0が出る確率は1/4
	else return 0; //return 1: 逃走成功 return 0: 逃走失敗
}

構造体を用いて実装する際の注意

・構造体を使って様々なことを実装できるようになるのですが、例えばゲームの武器についての情報を構造体に入れたいと思い立った時に、一気に大量に武器についての情報を入れてしまうと、あとあと後悔することになる場合があります。
プログラムを書いているうえで機能を変更していきたいケースは少なくないので、そのたびに全ての情報を変更するのはとても面倒です。
なので、最初に構造体に情報を入れるときは必要最低限の量にすることを心がけましょう。
今回で言うなら登場人物の情報として2人分しか用意していませんが、配列で準備しているので後からいくらでも登場人物を増やすことが可能です。
このように発展可能なプログラムを書くことで、より一般的な内容に対して成立するものとなるので、そのことを意識しながらプログラムを書く習慣をつけると今後の進展に拍車をかけることができるはずです。

・また、今回は戦闘シーンと言うことなのでフィールドについて言及する必要はないのですが、フィールドの情報を構造体に入れてフィールドを実装するケースも少なくありません。その時、フィールドデータはたいてい沢山の情報を持っていて、それもいちいちプログラムに全て記述していたら大変な事です。この場合、別のファイルにフィールドデータを保存しておいて、それを引用して使うということで、また新しく「ファイルポインタ」という知識が必要になってくるので今回は控えることとします。