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;
}

おわりに

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