
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.c | 25x25マップの雛形・描画・敵リスポーン |
| 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とそれ以外で何が違うのか」まで含めて、しっかり解説していきます。
