
STEP10:enemies層の実装(敵テンプレート・ランダム生成・敵専用魔法)
STEP10では、RPGの戦闘を「毎回同じ敵」から卒業させるために、敵テンプレート(図鑑)を用意し、そこからランダム生成で敵を作り、さらに敵が使う敵専用魔法を実装します。
この層がしっかりすると、マップで敵(E)に当たったときに「エリアに合った敵が出る」「敵がたまに魔法を使う」ようになって、戦闘が一気にゲームらしくなります。
enemies層の役割(このSTEPで決める設計)
enemies層は「敵に関するデータと生成」を担当します。
battle層は「戦闘進行(ターン、勝敗、報酬)」を担当します。
| 項目 | enemies層 | battle層 |
|---|---|---|
| 敵の種類データ(図鑑) | ✅ | |
| エリアに応じた敵生成 | ✅ | |
| 敵の初期化(HP/MP/ステータス) | ✅ | |
| 敵の魔法データ | ✅ | |
| 敵の行動決定(通常攻撃 or 魔法) | ✅ (今回はここまで) | |
| ダメージ計算・適用(最終処理) | ✅ |
※「敵AIの意思決定」まで enemies でやると battle が読みやすくなります(おすすめ)。
追加するデータ型と定数
EnemyTemplate(敵テンプレート=敵図鑑)
テンプレートは「生成元」なので Enemy の材料です。
| フィールド | 意味 |
|---|---|
| name | 敵名 |
| min_level / max_level | 出現レベル帯 |
| max_hp / max_mp | 最大HP/MP |
| atk / def / mag | 攻撃・防御・魔力 |
| exp / gold | 報酬 |
| spells[] | 敵専用魔法 |
既存データ型の想定
STEP09で Spell を作っている前提で、敵魔法も同じ Spell を使います。
- Spell は name / type / mp_cost / power を持つ
- SpellType は SPELL_DMG / SPELL_HEAL / SPELL_BUFF を持つ
(もし名前が違うなら、あなたのプロジェクト側に合わせて読み替えてOKです)
enemies.h を実装する
enemies.h は「敵生成」と「敵行動」の入口だけを公開します。
#ifndef ENEMIES_H
#define ENEMIES_H
#include "common.h"
/* 敵テンプレート(敵図鑑) */
typedef struct {
const char* name;
int min_level;
int max_level;
int max_hp;
int max_mp;
int atk;
int def;
int mag;
int exp;
int gold;
Spell spells[4];
} EnemyTemplate;
/* エリアレベルに応じた敵を1体生成 */
Enemy enemies_spawn(int area_level);
/* 敵の行動:0=通常攻撃、1=魔法 */
int enemies_will_cast_spell(const Enemy* e);
/* 敵が魔法を使う(成功=1 / 不発=0) */
int enemies_cast_spell(Enemy* e, Character party[PARTY_SIZE]);
#endifenemies.c:敵テンプレート(図鑑)を作る
ポイントは2つです。
- 「出現レベル帯」で候補を絞れるようにする。
- 魔法枠は固定数(ここでは4枠)で扱いを単純化する。
#include "enemies.h"
#include <stdlib.h>
#include <string.h>
#define ENEMY_DB_COUNT 6
#define ENEMY_SPELLS 4
static EnemyTemplate g_enemy_db[ENEMY_DB_COUNT] = {
/* 1) スライム */
{
"Slime", 1, 3,
35, 8,
6, 2, 3,
12, 8,
{
{"Sticky Shot", SPELL_DMG, 3, 2},
{"Jelly Heal", SPELL_HEAL, 4, 15},
{"Brace", SPELL_BUFF, 3, 1},
{"-", SPELL_DMG, 0, 0}
}
},
/* 2) ゴブリン */
{
"Goblin", 2, 5,
60, 10,
10, 4, 4,
22, 15,
{
{"Throw Knife", SPELL_DMG, 3, 2},
{"Brace", SPELL_BUFF, 3, 1},
{"-", SPELL_DMG, 0, 0},
{"-", SPELL_DMG, 0, 0}
}
},
/* 3) コウモリ */
{
"Bat", 3, 6,
55, 14,
8, 3, 6,
26, 18,
{
{"Sonic", SPELL_DMG, 4, 2},
{"Night Heal", SPELL_HEAL, 5, 18},
{"-", SPELL_DMG, 0, 0},
{"-", SPELL_DMG, 0, 0}
}
},
/* 4) スケルトン */
{
"Skeleton", 5, 9,
95, 16,
14, 7, 6,
40, 30,
{
{"Dark Slash", SPELL_DMG, 5, 2},
{"Iron Guard", SPELL_BUFF, 4, 1},
{"-", SPELL_DMG, 0, 0},
{"-", SPELL_DMG, 0, 0}
}
},
/* 5) ウォーロック */
{
"Warlock", 7, 12,
110, 30,
10, 6, 12,
60, 45,
{
{"Shadow Bolt", SPELL_DMG, 6, 3},
{"Recover", SPELL_HEAL, 6, 20},
{"Barrier", SPELL_BUFF, 5, 1},
{"-", SPELL_DMG, 0, 0}
}
},
/* 6) ボス枠 */
{
"Master Dragon", 12, 99,
260, 60,
22, 12, 18,
220, 180,
{
{"Breath", SPELL_DMG, 10, 4},
{"Regenerate", SPELL_HEAL, 10, 40},
{"Dragon Guard",SPELL_BUFF, 8, 1},
{"-", SPELL_DMG, 0, 0}
}
}
};敵をランダム生成する(enemies_spawn)
候補抽出 → ランダム選択
- area_level がレベル帯に入るテンプレートだけ候補にする。
- 候補から1つ選ぶ。
- Enemy を初期化して返す。
static int in_range(int v, int minv, int maxv) {
return (v >= minv && v <= maxv);
}
Enemy enemies_spawn(int area_level) {
int idxs[ENEMY_DB_COUNT];
int n = 0;
for (int i = 0; i < ENEMY_DB_COUNT; i++) {
if (in_range(area_level, g_enemy_db[i].min_level, g_enemy_db[i].max_level)) {
idxs[n++] = i;
}
}
/* 候補が0なら最弱(安全策) */
int pick = 0;
if (n > 0) pick = idxs[rand() % n];
EnemyTemplate* t = &g_enemy_db[pick];
Enemy e;
memset(&e, 0, sizeof(Enemy));
strncpy(e.name, t->name, sizeof(e.name) - 1);
e.max_hp = t->max_hp;
e.hp = t->max_hp;
e.max_mp = t->max_mp;
e.mp = t->max_mp;
e.base_atk = t->atk;
e.base_def = t->def;
e.base_mag = t->mag;
e.exp = t->exp;
e.gold = t->gold;
for (int s = 0; s < ENEMY_SPELLS; s++) {
e.spells[s] = t->spells[s];
}
e.defending = 0;
e.alive = 1;
return e;
}area_level をどう決める?
おすすめは次のどれかです。
| 方法 | area_level の決め方 | 特徴 |
|---|---|---|
| A | 主人公レベル | 実装が簡単 |
| B | パーティ平均レベル | 安定して強さが追従 |
| C | マップ深度/フロア | ダンジョン感が出る |
STEP10では A or B が一番楽です。
敵専用魔法の発動(enemies_cast_spell)
敵魔法の特徴は「対象入力なし」でAIが決める点です。
対象選択ルール(シンプル版)
- 攻撃魔法:生存している味方からランダム1人
- 回復魔法:敵自身を回復
- 防御魔法:敵自身に defending を付与
static int pick_random_alive(Character party[PARTY_SIZE]) {
int list[PARTY_SIZE];
int n = 0;
for (int i = 0; i < PARTY_SIZE; i++) {
if (party[i].alive) list[n++] = i;
}
if (n == 0) return -1;
return list[rand() % n];
}発動本体
ここでは、魔法効果を次のように統一します。
| 種類 | 効果 |
|---|---|
| SPELL_DMG | base_mag × power のダメージ |
| SPELL_HEAL | power だけ回復(固定値) |
| SPELL_BUFF | defending=1 を付与 |
int enemies_cast_spell(Enemy* e, Character party[PARTY_SIZE]) {
int usable[ENEMY_SPELLS];
int n = 0;
for (int s = 0; s < ENEMY_SPELLS; s++) {
Spell sp = e->spells[s];
if (sp.mp_cost <= 0) continue; /* "-"枠 */
if (e->mp < sp.mp_cost) continue;
usable[n++] = s;
}
if (n == 0) return 0;
/* HPが減ってると回復を優先しやすくする */
int hp_pct = e->hp * 100 / e->max_hp;
int chosen = usable[rand() % n];
if (hp_pct <= 30) {
for (int i = 0; i < n; i++) {
Spell sp = e->spells[usable[i]];
if (sp.type == SPELL_HEAL) { chosen = usable[i]; break; }
}
}
Spell sp = e->spells[chosen];
e->mp -= sp.mp_cost;
if (sp.type == SPELL_DMG) {
int t = pick_random_alive(party);
if (t < 0) return 0;
int dmg = e->base_mag * sp.power;
if (party[t].defending) {
dmg /= 2;
party[t].defending = 0;
}
party[t].hp -= dmg;
if (party[t].hp < 0) party[t].hp = 0;
printf("%s casts %s! %s takes %d damage!\n", e->name, sp.name, party[t].name, dmg);
sfx_beep(1);
if (party[t].hp == 0) party[t].alive = 0;
return 1;
}
if (sp.type == SPELL_HEAL) {
int before = e->hp;
e->hp += sp.power;
if (e->hp > e->max_hp) e->hp = e->max_hp;
printf("%s casts %s! HP +%d!\n", e->name, sp.name, (e->hp - before));
sfx_beep(2);
return 1;
}
/* SPELL_BUFF */
e->defending = 1;
printf("%s casts %s! Defending!\n", e->name, sp.name);
sfx_beep(0);
return 1;
}敵が魔法を使うか決める(enemies_will_cast_spell)
「魔法使う頻度」を決める関数です。
ここがあるだけで、battle側が読みやすくなります。
int enemies_will_cast_spell(const Enemy* e) {
if (e->mp <= 0) return 0;
int hp_pct = e->hp * 100 / e->max_hp;
int r = rand() % 100;
if (hp_pct <= 30) return (r < 70); /* ピンチなら魔法多め */
return (r < 35); /* 平常時はほどほど */
}battle側での使い方(敵ターンの最小変更)
敵ターンで「魔法 or 通常攻撃」を切り替えるだけにします。
if (enemies_will_cast_spell(&enemy)) {
if (!enemies_cast_spell(&enemy, gs->party)) {
enemy_attack(&enemy, gs->party);
}
} else {
enemy_attack(&enemy, gs->party);
}「魔法が不発でも殴る」にしておくとテンポがいいです。
動作確認(ここだけは必ず見る)
| 確認 | 期待する挙動 |
|---|---|
| 候補抽出 | area_level を変えると敵の種類が変わる。 |
| 初期化 | hp=max_hp, mp=max_mp で開始する。 |
| 魔法枠 | "-" を引いても落ちない。 |
| MP不足 | MP不足なら魔法を使わず通常攻撃へ。 |
| 回復AI | HPが30%以下で回復が出やすい。 |
| defending | defending の半減→解除が正しく動く。 |
演習問題+模範解答例
演習:敵を1種類追加する(Orc)
問題
敵テンプレートに Orc を追加してください。出現レベル帯は 6〜10。
HP 120 / MP 12 / ATK 16 / DEF 8 / MAG 5 / EXP 55 / GOLD 40。
魔法は以下:
- Uppercut(SPELL_DMG, mp_cost=4, power=2)
- Brace(SPELL_BUFF, mp_cost=3, power=1)
模範解答例(テンプレート追加)
{
"Orc", 6, 10,
120, 12,
16, 8, 5,
55, 40,
{
{"Uppercut", SPELL_DMG, 4, 2},
{"Brace", SPELL_BUFF, 3, 1},
{"-", SPELL_DMG, 0, 0},
{"-", SPELL_DMG, 0, 0}
}
},STEP10のまとめ
- 敵はテンプレート(図鑑)で管理すると拡張が楽
- area_level で候補を絞ってランダム生成するとゲームらしい出現になる。
- 敵魔法は「対象入力なし」でAIが選ぶとテンポが良い。
- enemies_will_cast_spell を用意すると battle.c がスッキリする。
