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初期MAGLVUP HPLVUP MPLVUP ATKLVUP MAG
勇者1203084+18+6+2+1
戦士15010102+22+3+3+1
魔法使い9050410+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
130
260
3110
4180

※ ここはゲームバランスなので、後から調整しやすい形なら何でも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層(敵の定義・ランダム出現・強さテーブル)を実装して、
「マップで敵に当たる → 戦闘の敵が決まる」の流れを完成させていきます。