STEP01:rpg1.cを機能別に分割する設計とプロジェクト構成(main.cと各モジュール)

このSTEPでやること

このバトルRPGは、もともと1ファイル(rpg1.c)に全部入りだったものを、役割ごとに10個の.cと10個の.hに分けて整理しています。
STEP01では「どの機能をどのファイルへ移したのか」「main.cは何を担当するのか」「各モジュールがどうつながるのか」を、全体像として掴みます。

分割のゴールはシンプルで、次の3つです。

  • 見通しが良くなる(探しやすい、直しやすい)
  • 責務が明確になる(この処理はここ、が決まる)
  • 段階的に記事化しやすい(STEP02以降でモジュール単位に解説できる)

1ファイル実装の「困りごと」と分割のメリット

1ファイル(rpg1.c)だと起きがちなこと

困りごと具体例
目的の処理が見つけにくいバトル処理を直したいのに、マップやセーブ処理も同じ場所に混ざる。
変更の影響範囲が読みにくいちょっと直しただけなのに別機能が壊れた…が起きやすい。
説明しづらい記事で章ごとに説明するのが大変(毎回長い)

分割するとこうラクになる

分割のメリット具体例
役割が1つにまとまるbattle.cは戦闘だけ、map.cは地形だけ、のように整理される。
変更が局所化するアイテム処理を直すなら items.c を見ればOK
学習順に記事が作れるSTEP02から順に「土台→入力→UI→マップ→…」と積み上げ可能

全体のプロジェクト構成(どのファイルが何を担当する?)

あなたが提示してくれた分割構成は、かなり理想的な「層+機能」分割になっています。

ソースファイル(実装)

ファイル役割(ざっくり)
main.cゲーム全体の流れ(初期化→探索ループ→イベント分岐→戦闘呼び出し)
platform.c画面クリア・効果音など、環境依存の土台
input.c安全な入力(行入力・数値変換)
ui.cタイトル・エンディング・ステータス表示
map.c25x25マップの雛形・描画・敵リスポーン
services.c宿・教会・セーブ・ロード
items.cインベントリ表示・消費・装備・宝箱固定配置
characters.c味方キャラ初期化・成長・レベルアップ・味方魔法
enemies.c敵テンプレ・ランダム生成・敵AI・ラストボス生成
battle.cターン制戦闘(行動選択、勝利処理、復帰)

ヘッダーファイル(公開インタフェース)

ファイル役割
common.h全体共通の定義(構造体、enum、定数、色コードなど)
platform.h など各.h各モジュールが外部へ公開する関数の宣言(API)

分割設計のコツ:.hと.cの責務を分ける

基本ルール

種類置くものポイント
.h構造体・enum・関数宣言・extern変数宣言他ファイルから使われるものだけ
.c関数の中身(実装)・staticな内部関数・staticな内部データそのファイル内に隠したいものはstatic

“隠す”ためのstaticの意味(めちゃ大事)

C言語のstaticは場面によって意味が変わりますが、ここで効くのは 内部リンケージ(その.c内だけで見える) です。

書き方意味
static void helper(void);helperはその.cファイル内だけで使える(外に漏れない)
static const TreasureSpot g_treasures[] = {...};宝箱テーブルをitems.c内部に隠せる

このおかげで、外からは「open_treasure_fixedだけ呼べばいい」というスッキリした設計になります。

common.hは「全員が共有するルールブック」

common.hには、ゲーム全体が共通で使う定義が入っています。
ここがブレると全ファイルが困るので、共通化するのは大正解です。

common.hの中身(要点)

  • 標準ライブラリのinclude(stdio.h, stdlib.h, string.h, time.h)
  • Windows用の条件付きinclude(_WIN32ならwindows.h)
  • ANSIカラー定義(RESET, RED, GREEN …)
  • 定数(MAP_H, MAP_W, PARTY_SIZE, ITEM_COUNT など)
  • enum(Job, ItemID, SpellType, EnemySpellKind)
  • struct(Spell, Character, EnemySpell, Enemy, GameState, ItemDef)
include guard(多重include防止)

common.hや各.hにあるこれ、地味だけど重要です。

#ifndef COMMON_H
#define COMMON_H
/* ... */
#endif

これがあると、たとえばmain.cが複数の.hをincludeして結果的にcommon.hが何度も読み込まれても、再定義エラーを防げます。

「モジュール間のつながり」全体図(データの中心はGameState)

このゲームの核は GameState です。
探索・イベント・戦闘・セーブロード、ほぼ全部がGameStateを介してつながります。

データ中心設計(イメージ)

  • GameState
    • 現在座標 px, py
    • map[25][25]
    • party[3](Character)
    • inv[8](アイテム所持数)
    • game_clear

呼び出し関係(ざっくり)

  • main.c が全体の司令塔
    • ui(show_title / show_ending)
    • map(load_map_template / draw_map / respawn_enemy)
    • services(inn_rest / church_revive / save_game / load_game)
    • items(open_treasure_fixed)
    • enemies(make_random_enemy / make_boss_enemy)
    • battle(battle)
    • platform・inputは各所で利用(下支え)

main.cは「ゲーム進行の台本」だけを担当する

ここからがSTEP01の主役です。main.cは ゲームの進行管理 に徹します。
逆に言うと、main.cに「宝箱の中身決定」や「敵AIの判断」みたいな細かい処理は置かないのがポイントです。

main.cでやっていること(役割)

処理具体例
初期化srand、GameState初期化、タイトル表示、マップ読み込み、キャラ初期化
探索ループ画面描画→入力→移動→マス判定
イベント分岐T/I/C/E/B に応じて各モジュール関数を呼ぶ
再開処理ゲームオーバー時に初期化し直す
終了処理qで抜ける、ボス撃破でエンディング

掲載:main.c(分割後のメイン)

/* main.c */
#include "common.h"
#include "platform.h"
#include "input.h"
#include "ui.h"
#include "map.h"
#include "services.h"
#include "items.h"
#include "characters.h"
#include "enemies.h"
#include "battle.h"

int main(void) {
    srand((unsigned)time(NULL));

    GameState gs;
    memset(&gs, 0, sizeof(gs));

    show_title();

    load_map_template(gs.map);
    gs.px = 1; gs.py = 1;
    gs.game_clear = 0;
    init_characters(gs.party);

    gs.inv[ITEM_POTION] = 2;
    gs.inv[ITEM_ETHER] = 1;

    while (1) {
        clear_screen();
        draw_map(&gs);

        printf("\nコマンド: 4/6/8/2=移動  s=セーブ  l=ロード  q=終了\n");
        printf("入力: ");

        char buf[64];
        read_line(buf, sizeof(buf));
        if (buf[0] == '\0') continue;

        if (buf[0] == 'q') {
            printf("終了します。\n");
            break;
        }

        if (buf[0] == 's') {
            if (save_game(&gs)) {
                sfx_beep(0);
                printf(GREEN "セーブしました。(%s)\n" RESET, SAVE_FILE);
            }
            else {
                sfx_beep(4);
                printf(RED "セーブに失敗しました。\n" RESET);
            }
            printf("Enterで続行...");
            read_line(buf, sizeof(buf));
            continue;
        }

        if (buf[0] == 'l') {
            GameState tmp;
            if (load_game(&tmp)) {
                gs = tmp;
                sfx_beep(0);
                printf(GREEN "ロードしました。(%s)\n" RESET, SAVE_FILE);
            }
            else {
                sfx_beep(4);
                printf(RED "ロードに失敗しました。(セーブデータがない可能性)\n" RESET);
            }
            printf("Enterで続行...");
            read_line(buf, sizeof(buf));
            continue;
        }

        int key = atoi(buf);
        int nx = gs.px, ny = gs.py;

        if (key == 4) ny--;
        else if (key == 6) ny++;
        else if (key == 8) nx--;
        else if (key == 2) nx++;
        else continue;

        if (nx < 0 || nx >= MAP_H || ny < 0 || ny >= MAP_W) continue;

        if (gs.map[nx][ny] != '#') {
            gs.px = nx; gs.py = ny;
        }

        char cell = gs.map[gs.px][gs.py];

        if (cell == 'T') {
            clear_screen();
            open_treasure_fixed(&gs, gs.px, gs.py);
            gs.map[gs.px][gs.py] = ' ';
            printf("Enterで続行...");
            read_line(buf, sizeof(buf));
        }
        else if (cell == 'I') {
            clear_screen();
            inn_rest(&gs);
            printf("Enterで続行...");
            read_line(buf, sizeof(buf));
        }
        else if (cell == 'C') {
            clear_screen();
            church_revive(&gs);
            printf("Enterで続行...");
            read_line(buf, sizeof(buf));
        }
        else if (cell == 'E') {
            Enemy e = make_random_enemy();

            int win = battle(&gs, e);
            if (win) {
                gs.map[gs.px][gs.py] = ' ';
                respawn_enemy(&gs);
            }
            else {
                clear_screen();
                printf(RED "ゲームオーバー…\n" RESET);
                printf("Enterでタイトルへ...");
                read_line(buf, sizeof(buf));

                show_title();
                load_map_template(gs.map);
                gs.px = 1; gs.py = 1;
                init_characters(gs.party);
                memset(gs.inv, 0, sizeof(gs.inv));
                gs.inv[ITEM_POTION] = 2;
                gs.inv[ITEM_ETHER] = 1;
            }
        }
        else if (cell == 'B') {
            Enemy boss = make_boss_enemy();

            int win = battle(&gs, boss);
            if (win) {
                gs.game_clear = 1;
                show_ending();
                break;
            }
            else {
                clear_screen();
                printf(RED "マスタードラゴンに敗北した…\n" RESET);
                printf("Enterでタイトルへ...");
                read_line(buf, sizeof(buf));

                show_title();
                load_map_template(gs.map);
                gs.px = 1; gs.py = 1;
                init_characters(gs.party);
                memset(gs.inv, 0, sizeof(gs.inv));
                gs.inv[ITEM_POTION] = 2;
                gs.inv[ITEM_ETHER] = 1;
            }
        }
    }

    return 0;
}

main.cのロジック解説(重要ポイントだけギュッと)

➀ srandで乱数を初期化

srand((unsigned)time(NULL));
  • randを使う敵生成や宝箱(固定外の場合)にランダム性が出ます。
  • time(NULL)は現在時刻(秒)を返すので、起動するたびに違う乱数系列になりやすいです。

➁ GameStateをゼロクリアしてからゲーム開始

GameState gs;
memset(&gs, 0, sizeof(gs));
  • 構造体の中身(配列やフラグ)を一旦0で埋めて、初期状態を安定させます。
  • ゲームでは inv や game_clear みたいな値が残ると事故るので、最初にゼロ化は安心です。

➂ “入力→移動→マス判定→イベント呼び出し” の流れが綺麗

探索ループの中心はこれです。

段階main.cの仕事
入力read_lineで行入力、atoiで方向キー判定
移動nx, nyを計算し、壁 # を避けて座標更新
マス判定cell = gs.map[gs.px][gs.py]
イベントT/I/C/E/B で各モジュールの関数を呼ぶ

ここが「司令塔としてのmain.c」っぽいところですね。処理そのものは各モジュールへ逃がして、main.cは分岐と進行を担当しています。

代表的な“分割のパターン”を3つだけ紹介

パターンA:単純なユーティリティ系(platform / input)

  • platform.c:clear_screen / sfx_beep
  • input.c:read_line / read_int_safely
    どこからでも使う“土台”なので、分離すると全体が安定します。

パターンB:データを持つ機能(items / enemies)

  • items.cは g_items を持つ
  • enemies.cは g_enemy_templates を持つ
    こういう「テーブル(定数データ)」を持つ機能は、モジュール化の効果が大きいです。

ちなみに items.h はこうなっていますね。

extern const ItemDef g_items[ITEM_COUNT];
  • externは「実体は別の.cにあるよ(ここでは宣言だけ)」という意味です。
  • 実体(定義)は items.c のこれです。
const ItemDef g_items[ITEM_COUNT] = { ... };

パターンC:GameStateを受け取って仕事する機能(map / services / battle)

  • map:GameStateを見て描画、敵を置く
  • services:GameStateのpartyやセーブデータを更新
  • battle:GameStateのpartyと敵で戦闘し、結果を反映

この3つは「状態を持つゲーム」らしさの中心で、分割すると一気に読みやすくなります。

STEP01のまとめ

  • common.hに共通定義を集約して、全モジュールの前提を統一した。
  • 各.cは1つの役割に集中し、外に見せる関数だけを.hに宣言した。
  • main.cは司令塔として、探索ループとイベント分岐を担当する形に整理できた。

次はSTEP02で、platform層(画面クリア・効果音・色表現の土台)を「なぜここにまとめるのか」「Windowsとそれ以外で何が違うのか」まで含めて、しっかり解説していきます。