
STEP08:characters層の実装(職業・成長・レベルアップ・基礎ステータス)
STEP08では、ゲームの「キャラクターそのもの」を設計・実装する characters層 を作ります。
ここが入ると、以降の battle / map / items が「キャラの強さ」を共通ルールで扱えるようになり、拡張が一気にラクになります。
このSTEPのゴールは以下です。
- 職業(勇者・戦士・魔法使い)ごとの 基礎ステータス を定義できる。
- 経験値とレベルアップの仕組みを持てる。
- レベルアップ時に HP/MP/ATK/MAG が 職業に応じて成長 する。
- 「現在ステータス」と「最大値」を整合させる(回復や上限の丸めも安定)
characters層が担当する範囲
| 役割 | 具体例 |
|---|---|
| 職業定義 | JOB_HERO / JOB_WARRIOR / JOB_MAGE |
| 初期ステータス | LV1のHP/MP/ATK/MAG |
| 成長(レベルアップ) | HP/MP/ATK/MAG の増加量 |
| 経験値管理 | exp加算、次レベル必要expの計算 |
| キャラ初期化 | パーティを作る、各キャラを初期化する。 |
※ 入力のやりとり(誰を選ぶ等)は input/ui 側でやります。
characters層は「キャラのルール」と「計算」を提供する層です。
まずはデータ構造を確認(common.h)
すでに Character が common.h にある前提で進めます(STEP04~07の流れ)。
概念としてはこういう情報を持っているはずです。
- name, job
- level, exp
- hp/max_hp, mp/max_mp
- base_atk, base_mag
- 装備補正:weapon_atk, staff_magic(STEP07)
以降の実装では「最終的な強さ」は base + 装備 の形で扱いやすくします。
characters.h(公開API)
characters層のAPIは次のようにしておくと運用がキレイです。
#ifndef CHARACTERS_H
#define CHARACTERS_H
#include "common.h"
/* 代表的な初期化 */
void init_party_default(GameState* gs);
/* 個別キャラ初期化 */
void init_character(Character* c, Job job, const char* name);
/* 経験値とレベルアップ */
int next_level_exp(int level);
int add_exp_and_levelup(Character* c, int gain_exp);
/* 最終ステータス(装備込み) */
int calc_atk(const Character* c);
int calc_mag(const Character* c);
#endif- init_party_default():ゲーム開始時のパーティをまとめて作る。
- add_exp_and_levelup():戦闘勝利時に exp を加算して、必要ならレベルアップ
- calc_atk()/calc_mag():battle側がダメージ計算で使う「最終ATK/MAG」を取れる。
職業定義と「基礎値・成長値」のテーブル化(超重要)
レベルアップ処理でいちばんやりたいのは、
「職業によって伸び方が違う」を if 文地獄にしない。
なので、ここは テーブル化 します。
成長パラメータ(JobGrowth)
typedef struct {
const char* job_name;
int hp0, mp0, atk0, mag0; /* LV1 基礎値 */
int hp_g, mp_g, atk_g, mag_g; /* レベルアップ時の増加量 */
} JobGrowth;
static const JobGrowth g_growth[JOB_COUNT] = {
/* job_name, hp0, mp0, atk0, mag0, hp_g, mp_g, atk_g, mag_g */
{"勇者", 120, 30, 8, 4, 18, 6, 2, 1},
{"戦士", 150, 10, 10, 2, 22, 3, 3, 1},
{"魔法使い", 90, 50, 4, 10, 14, 9, 1, 3},
};仕様を表で確認
| 職業 | 初期HP | 初期MP | 初期ATK | 初期MAG | LVUP HP | LVUP MP | LVUP ATK | LVUP MAG |
|---|---|---|---|---|---|---|---|---|
| 勇者 | 120 | 30 | 8 | 4 | +18 | +6 | +2 | +1 |
| 戦士 | 150 | 10 | 10 | 2 | +22 | +3 | +3 | +1 |
| 魔法使い | 90 | 50 | 4 | 10 | +14 | +9 | +1 | +3 |
このテーブルがあるだけで、以降の「職業追加」もほぼ差し替えで済みます。
キャラ初期化(init_character / init_party_default)
init_character
#include <string.h>
#include "characters.h"
void init_character(Character* c, Job job, const char* name) {
memset(c, 0, sizeof(*c));
c->job = job;
strncpy(c->name, name, sizeof(c->name) - 1);
c->level = 1;
c->exp = 0;
c->alive = 1;
const JobGrowth* g = &g_growth[job];
c->max_hp = g->hp0;
c->max_mp = g->mp0;
c->hp = c->max_hp;
c->mp = c->max_mp;
c->base_atk = g->atk0;
c->base_mag = g->mag0;
/* 装備補正(STEP07) */
c->weapon_atk = 0;
c->staff_magic = 0;
}ポイントはこれです。
- memset で初期状態を整える(未初期化事故防止)
- max_hp/max_mp を先に決めてから hp/mp を合わせる。
- 装備補正は 0 からスタート
init_party_default
ここは「ゲームの初期メンバー」を決める関数です。
void init_party_default(GameState* gs) {
init_character(&gs->party[0], HERO, "HERO");
init_character(&gs->party[1], WARRIOR, "WARRIOR");
init_character(&gs->party[2], MAGE, "MAGE");
}※ 表示名を日本語にしたければ、ここで "勇者" などにしてOKです。
次レベル必要経験値(next_level_exp)
経験値カーブは「ほどよい伸び」にしておくと遊びやすいです。
今回は例として「じわじわ増える二次関数寄り」にします。
int next_level_exp(int level) {
/* LV1→2: 30, LV2→3: 60, LV3→4: 100 ... */
return 20 + level * level * 10;
}例:必要経験値の目安
| 現在LV | 次LVに必要exp |
|---|---|
| 1 | 30 |
| 2 | 60 |
| 3 | 110 |
| 4 | 180 |
※ ここはゲームバランスなので、後から調整しやすい形なら何でもOKです。
経験値加算とレベルアップ(add_exp_and_levelup)
「経験値を足す」「閾値を超えたらレベルアップ」をまとめて面倒見ます。
static void level_up(Character* c) {
const JobGrowth* g = &g_growth[c->job];
c->level++;
c->max_hp += g->hp_g;
c->max_mp += g->mp_g;
c->base_atk += g->atk_g;
c->base_mag += g->mag_g;
/* レベルアップ時は全回復(仕様として気持ちいい) */
c->hp = c->max_hp;
c->mp = c->max_mp;
}
int add_exp_and_levelup(Character* c, int gain_exp) {
if (!c->alive) return 0;
c->exp += gain_exp;
int leveled = 0;
while (c->exp >= next_level_exp(c->level)) {
c->exp -= next_level_exp(c->level);
level_up(c);
leveled++;
}
return leveled;
}実装の意図
| 処理 | 意味 |
|---|---|
| while で回す | 一気に大量expを得たとき「複数回レベルアップ」できる。 |
| exp を減算 | “累計exp” ではなく “次レベルまでの残量管理” にしている。 |
| LVUP時に全回復 | RPGでよくある気持ちいい仕様(変更も簡単) |
最終ATK/MAG(装備込み)を返す関数
battle層から見たとき、
- 勇者の攻撃力 = base_atk + weapon_atk
- 魔法使いの魔力 = base_mag + staff_magic
という「装備込みの値」が欲しいです。
int calc_atk(const Character* c) {
return c->base_atk + c->weapon_atk;
}
int calc_mag(const Character* c) {
return c->base_mag + c->staff_magic;
}battle側の計算が簡単になるので、ここは作っておく価値が高いです。
battle.c 側の接続イメージ(勝利→経験値)
勝利時に、各生存キャラへ経験値を配ります。
int gain = 25; /* 敵の強さに応じて変えてOK */
for (int i = 0; i < PARTY_SIZE; i++) {
Character* c = &gs->party[i];
if (!c->alive) continue;
int up = add_exp_and_levelup(c, gain);
if (up > 0) {
printf(YELLOW "%s がレベルアップ! Lv.%d\n" RESET, c->name, c->level);
sfx_beep(3);
}
}ここで battle は「経験値を渡す」だけ。
「どう成長するか」は characters層に閉じ込められているので設計がキレイです。
STEP08のまとめ
- 職業ごとの「初期値」と「成長値」をテーブル化して、実装がスッキリした。
- キャラ初期化で max と current を揃え、未初期化事故を防いだ。
- 経験値→レベルアップを characters層に閉じ込め、battle側を簡単にした。
- ATK/MAG の最終値を関数化して、装備システムと自然に接続できた。
次のSTEP09では、enemies層(敵の定義・ランダム出現・強さテーブル)を実装して、
「マップで敵に当たる → 戦闘の敵が決まる」の流れを完成させていきます。
