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

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