vol.14 敵の移動・戦闘(ひとまず終了)

目次

はじめに

前回からの続きで、まずは敵がプレイヤーに近づく機構を作成していきたいと思います。
近づけたら敵と戦闘を行えるようにしていきます。細かい仕様等は各々記述していきますね。

敵の移動(2)

プレイヤーを中心とした7*7マスの中に敵がいた場合、敵はプレイヤーに近づこうとします。移動方向は縦か横かしかないので、どの方向を優先的に取るのかを決める必要があります。今回は、縦・横のより大きな成分の方向に移動することにします。成分が同じ場合は横方向を優先することにしましょう。
正直この定義だと敵AIとしては不成立と言ってもいいくらいですが、まあ今は目を瞑りましょう。

以下、enemyAct関数

void enemyAct(int *enemyX, int *enemyY, int enemyCount, int cursorX, int cursorY)
{
	int x ,y;
	int flag = 0;

	flag = 0; //フラグ初期化
	for (y = cursorY - 3; y <= cursorY + 3; y++) {
		for (x = cursorX - 3; x <= cursorX + 3; x++) {
			//フィールドの範囲外探索を回避
			if (x < 0 || y < 0 || x >= FIELD_WIDTH || y >= FIELD_HEIGHT)
				continue;

			//範囲内に敵がいれば
			if (x == *enemyX && y == *enemyY && flag != 1) {
				flag = 1;
				//プレイヤーに近づく処理をここに記述//
				int gapX = cursorX - *enemyX; //敵から見たプレイヤーの相対x座標
				int gapY = cursorY - *enemyY; //敵から見たプレイヤーの相対y座標

				//上へ
				if (gapY < 0 && abs(gapY) > abs(gapX)) {
					//壁・階段・プレイヤーには侵入しない
					if (CELL_TYPE_WALL <= field[*enemyY - 1][*enemyX] && field[*enemyY - 1][*enemyX] <= CELL_TYPE_PLAYER) break;

					field[*enemyY - 1][*enemyX] = 5; //移動先を敵アイコン(cellSymbolの5番目の配列)に変更
					field[*enemyY][*enemyX] = CELL_TYPE_NONE; //敵が元いた場所は空気に変更
					*enemyY -= 1; //敵は1つ上へ
				}
				//下へ
				else if (gapY > 0 && abs(gapY) > abs(gapX)) {
					//壁・階段・プレイヤーには侵入しない
					if (CELL_TYPE_WALL <= field[*enemyY + 1][*enemyX] && field[*enemyY + 1][*enemyX] <= CELL_TYPE_PLAYER) break;

					field[*enemyY + 1][*enemyX] = 5; //移動先を敵アイコン(cellSymbolの5番目の配列)に変更
					field[*enemyY][*enemyX] = CELL_TYPE_NONE; //敵が元いた場所は空気に変更
					*enemyY += 1; //敵は1つ下へ
				}
				//左へ
				else if (gapX < 0 && abs(gapY) <= abs(gapX)) {
					//壁・階段・プレイヤーには侵入しない
					if (CELL_TYPE_WALL <= field[*enemyY][*enemyX - 1] && field[*enemyY][*enemyX - 1] <= CELL_TYPE_PLAYER) break;

					field[*enemyY][*enemyX - 1] = 5; //移動先を敵アイコン(cellSymbolの5番目の配列)に変更
					field[*enemyY][*enemyX] = CELL_TYPE_NONE; //敵が元いた場所は空気に変更
					*enemyX -= 1; //敵は1つ左へ
				}
				//右へ
				else {
					//壁・階段・プレイヤーには侵入しない
					if (CELL_TYPE_WALL <= field[*enemyY][*enemyX + 1] && field[*enemyY][*enemyX + 1] <= CELL_TYPE_PLAYER) break;

					field[*enemyY][*enemyX + 1] = 5; //移動先を敵アイコン(cellSymbolの5番目の配列)に変更
					field[*enemyY][*enemyX] = CELL_TYPE_NONE; //敵が元いた場所は空気に変更
					*enemyX += 1; //敵は1つ右へ
				}
				
			}
		}
	}
		
	//範囲内に敵がいなければ
	if (flag == 0) { ... }
}

敵AIの構成はゲームの質を大きく変化させる要素なので、もっと研究していくべきでしょう。(まあここではやりませんが。)

敵のステータス追加

さて、敵と戦闘を行いたいのですが、そもそも敵にステータスが無ければ話になりませんね。敵の種類に応じてステータスを割り振るenemyStatusQuota関数を作成します。

//敵の種類に応じたステータス割り振り
void enemyStatusQuota(int enemyID, PERSON_t *enemy)
{
	switch (enemyID) {
	case CELL_TYPE_ENEMY__SLIME:
		(enemy->ATK) = 1, (enemy->DEF) = 1, (enemy->EXP) = 1, (enemy->GOLD) = 2, (enemy->HP) = 5, (enemy->LV) = 1, (enemy->MHP) = 5;
		break;
	case CELL_TYPE_ENEMY__DRAKEE:
		(enemy->ATK) = 2, (enemy->DEF) = 2, (enemy->EXP) = 2, (enemy->GOLD) = 4, (enemy->HP) = 8, (enemy->LV) = 1, (enemy->MHP) = 8;
		break;
	case CELL_TYPE_ENEMY__GHOST:
		(enemy->ATK) = 3, (enemy->DEF) = 4, (enemy->EXP) = 4, (enemy->GOLD) = 10, (enemy->HP) = 10, (enemy->LV) = 1, (enemy->MHP) = 10;
		break;
	case CELL_TYPE_ENEMY__MONSTER_TOALSTOOL:
		(enemy->ATK) = 5, (enemy->DEF) = 3, (enemy->EXP) = 10, (enemy->GOLD) = 15, (enemy->HP) = 12, (enemy->LV) = 1, (enemy->MHP) = 12;
		break;
	default: break;
	}
}

まあ、このあたりの数値は適当です。w
この関数をmain関数の、敵を生成するときに起動してやります。
main関数内:

//敵の初期生成
int enemyCount = random(5, 7);
ITEM_t enemy[FIELD_ITEM_COUNT]; //敵の識別
PERSON_t enemyInfo[FIELD_ITEM_COUNT]; //敵のステータス

for (i = 0; i < enemyCount; i++) {
	enemy[i].ID = random(CELL_TYPE_ENEMY__SLIME, CELL_TYPE_ENEMY_END - 1);
	setRandomIcon(enemy[i].ID, &enemy[i].x, &enemy[i].y);

	enemyStatusQuota(enemy[i].ID, &enemyInfo[i]);
}

64ターン経過の際にも敵は生成されるので忘れず起動しましょう。

//フィールドの描画
while (1) {
	//ターン経過
	turnNum++;
	//敵追加
	if (turnNum == 64 && enemyCount < FIELD_ENEMY_COUNT) {
		enemy[enemyCount].ID = random(CELL_TYPE_ENEMY__SLIME, CELL_TYPE_ENEMY_END - 1);
		setRandomIcon(enemy[enemyCount].ID, &enemy[enemyCount].x, &enemy[enemyCount].y);
		enemyStatusQuota(enemy[enemyCount].ID, &enemyInfo[enemyCount]);
		enemyCount++;
		turnNum = 0; //ターン数初期化
        }
        ...省略...
}

戦闘の概念

プレイヤーが移動したりメニュー画面を開いたりする箇所(main関数のswitch文)に機能を追加します。o(オー)キー(Offensive:攻撃)を入力することで隣接する敵に攻撃を仕掛ける機構を実装しましょう。
本来であればプレイヤーを中心とした全てのマスに存在する敵に対して個別に攻撃できるようにするところなのですが、分岐等が少々面倒なので「斜め攻撃なし」「プレイヤーの攻撃は全体攻撃(回し切り)」ということにしておきましょう。
また、ちゃんと戦闘終了処理等もここで行っていきます。敵を倒せたら経験値とゴールドを取得しフィールドから存在を消去し、プレイヤーは敗北すればゲームを終了するようにしましょう。
以下、main関数 case 'o':

case 'o': {
	//全ての敵について
	for (i = 0; i < FIELD_ENEMY_COUNT; i++) {
                //死んでいたら処理しない
		if (enemyInfo[i].HP <= 0) continue;
		//プレイヤーの目の前に敵がいたら
		if ((enemy[i].x - 1 == cursorX && enemy[i].y == cursorY) || (enemy[i].x + 1 == cursorX && enemy[i].y == cursorY)
		    || (enemy[i].x == cursorX && enemy[i].y - 1 == cursorY) || (enemy[i].x == cursorX && enemy[i].y + 1 == cursorY)) {
			//プレイヤーの攻撃宣言
			printf("プレイヤーの攻撃!\n");
			_getch();
			break; //攻撃宣言は一度だけ(まとめて周囲に攻撃できることにした)
		}
	}
	//全ての敵について
	for (i = 0; i < FIELD_ENEMY_COUNT; i++) {
                //死んでいたら処理しない
		if (enemyInfo[i].HP <= 0) continue;
		//プレイヤーの目の前に敵がいたら
		if ((enemy[i].x - 1 == cursorX && enemy[i].y == cursorY) || (enemy[i].x + 1 == cursorX && enemy[i].y == cursorY)
		    || (enemy[i].x == cursorX && enemy[i].y - 1 == cursorY) || (enemy[i].x == cursorX && enemy[i].y + 1 == cursorY)) {
			//プレイヤーの攻撃
			int damageEnemy = (player.ATK) - (enemyInfo[i].DEF);
			printf("プレイヤーは%s%dポイントのダメージを与えた!\n", idToName(enemy[i].ID), damageEnemy);
			enemyInfo[i].HP -= damageEnemy;

			//敵を倒したとき
			if (enemyInfo[i].HP <= 0) {
				printf("%sを倒した!\n", idToName(enemy[i].ID));
				printf("経験値%dポイント獲得!\n", enemyInfo[i].EXP);
				printf("%dゴールド入手した!\n", enemyInfo[i].GOLD);
				//経験値・ゴールドの取得
				player.EXP += enemyInfo[i].EXP;
				player.GOLD += enemyInfo[i].GOLD;
				//ifledの情報の書き換え(敵の死亡)
				field[enemy[i].y][enemy[i].x] = CELL_TYPE_NONE;
			}
		}
	}
	_getch();
	//全ての敵について
	for (i = 0; i < FIELD_ENEMY_COUNT; i++) {
		//死んでいたら処理しない
		if (enemyInfo[i].HP <= 0) continue;
		//プレイヤーの目の前に敵がいたら
		if ((enemy[i].x - 1 == cursorX && enemy[i].y == cursorY) || (enemy[i].x + 1 == cursorX && enemy[i].y == cursorY)
		    || (enemy[i].x == cursorX && enemy[i].y - 1 == cursorY) || (enemy[i].x == cursorX && enemy[i].y + 1 == cursorY)) {
			//敵の攻撃
			printf("%sの攻撃!\n", idToName(enemy[i].ID));
                        _getch();
			int damagePlayer = enemyInfo[i].ATK - player.DEF;
			printf("プレイヤーは%dポイントのダメージを受けた!\n", damagePlayer);
			player.HP -= damagePlayer;
			_getch();

			//体力は最小でも0なので
			if (player.HP < 0) {
				player.HP = 0;
				break;
			}
		}
	}
	break;
}

敵が死亡したことをfieldに変更してもenemyAct関数で再び書き換えられてしまうので、enemyAct関数を呼び出す際にそもそも敵が死亡していたらcontinueするようにすれば書き換えられることはなくなります。
main関数の下の方

//敵AI
for (i = 0; i < enemyCount; i++)
    if (enemyInfo[i].HP <= 0) continue;
    enemyAct(&enemy[i].x, &enemy[i].y, enemyCount, cursorX, cursorY);
}

そういえばidToName関数に敵の情報を追加していなかったので追加しておきましょう。まだアイテムの情報しか入ってませんからね。

char *idToName(int itemID)
{
	switch (itemID) {
	case CELL_TYPE_BREAD:
		return "パン";
	case CELL_TYPE_BIG_BREAD:
		return "大きなパン";
	case CELL_TYPE_ENEMY__SLIME:
		return "スライム";
	case CELL_TYPE_ENEMY__GHOST:
		return "ゴースト";
	case CELL_TYPE_ENEMY__DRAKEE:
		return "ドラキー";
	case CELL_TYPE_ENEMY__MONSTER_TOALSTOOL:
		return "おばけきのこ";
	default: break;
	}
}

※今回の戦闘におけるダメージ計算は、簡略化のために攻撃力から防御力を引いた値を実際のダメージとしています。他にもダメージ計算には種類があるのでよかったら別の方法でも試してみてくださいな。

おわりに

脈絡もなく今までプログラムを書いてきたので良い見通しが立っておらず、プログラムが煩雑になってしまっているのは申し訳ないです。ひとまず戦闘まで実装することができました。
もうあらかたゲームに必要なものはそろってきていると思いますので、続きはどうしようか、また考えておきます。
では、お疲れ様でした。

追伸:
とりあえずアスキーアートで表現する、C言語でのゲームプログラミングはここで終了したいと思います。何かほかにやってほしいことがあったらコメントください。即時実現・説明入れていきます。
このシリーズは終了しますが、新しく新環境でゲームプログラミングしていきたいと思います!
言語は変わらずC言語で(ひとまずは)続けようと思いますので、乞うご期待!