6日でできる 新C言語入門|C言語でRPG風バトルゲームを作る

「6日でできる 新C言語入門」で学んだ文法を、遊べるプログラムに変えてみよう。
構造体、関数、乱数、分岐、ループを組み合わせて、C言語でRPG風バトルゲームを作ります。

C言語の基礎を学んできたら、次はそれらを組み合わせて1つのまとまったプログラムを作ってみましょう。

今回作るのは、コンソール上で動くRPG風バトルゲームです。プレイヤーは勇者を操作し、敵と順番に戦います。通常攻撃、スキル攻撃、回復を選びながら、複数の敵を倒すことを目指します。

この演習では、C言語で学んできた多くの要素を使います。

変数、if文、switch文、while文、for文、配列、構造体、関数、ポインタ、乱数、標準入出力などが1つのプログラムの中でつながります。単体で学んだ文法が、ゲームの処理としてどのように使われるのかを確認できる総合演習です。

特に大切なのは、データと処理を分けて考えることです。

キャラクターの名前、HP、MP、攻撃力、防御力などは構造体にまとめます。攻撃、スキル、回復、敵の行動、ステータス表示などは関数に分けます。こうすることで、プログラム全体が読みやすくなり、後から機能を追加しやすくなります。

この記事では、RPG風バトルゲームを作る手順、全体設計、サンプルプログラム、各関数の役割、改造のアイデアまで順番に解説します。

RPG風バトルゲームで使うC言語の要素

今回のゲームでは、これまで学んできたC言語の基本を幅広く使います。

使う要素ゲーム内での役割
構造体勇者や敵のステータスをまとめる
配列複数の敵データを管理する
関数攻撃、回復、表示などの処理を分ける
ポインタ関数内でキャラクターのHPやMPを変更する
if文HPが0以下か、MPが足りるかなどを判定する
switch文プレイヤーの選択に応じて処理を分ける
while文戦闘が続く間、処理を繰り返す
for文敵リストや表示処理で繰り返しを行う
乱数ダメージに少し変化をつける
標準入出力メニュー表示や選択入力を行う

ゲームは、複数の文法が自然に組み合わさる題材です。
単なる計算プログラムよりも、データの変化や処理の流れをイメージしやすいのが特徴です。

ゲームの基本ルール

今回作るRPG風バトルゲームは、次のようなルールにします。

項目内容
プレイヤー勇者を操作する
複数の敵が順番に登場する
行動通常攻撃、スキル攻撃、回復から選ぶ
勝利条件すべての敵を倒す
敗北条件勇者のHPが0以下になる
ダメージ攻撃力、防御力、乱数から計算する
スキルMPを消費して強めの攻撃を行う
回復MPを消費してHPを回復する

このルールをもとに、プログラムを組み立てていきます。

ゲームとしてはシンプルですが、C言語の総合演習としては多くのポイントを含んでいます。

まずはデータ設計を考える

ゲームを作るときは、いきなり処理を書き始めるより、どのようなデータが必要かを考えると整理しやすくなります。

今回のキャラクターには、次の情報を持たせます。

データ内容
nameキャラクター名
hp現在のHP
maxHp最大HP
mp現在のMP
maxMp最大MP
attack攻撃力
defense防御力
speed素早さ

これらは、勇者にも敵にも共通する情報です。
そのため、Characterという構造体にまとめます。

構造体にすることで、勇者と敵を同じ形のデータとして扱えるようになります。

図:RPG風バトルゲームのデータ設計

この図から分かること

この図から分かるのは、勇者と敵を同じCharacter構造体で管理できるということです。

名前、HP、MP、攻撃力、防御力などを別々の変数にすると、データの対応関係を自分で管理しなければなりません。構造体にまとめることで、1人分または1体分のキャラクター情報として扱いやすくなります。

処理は関数に分けて考える

ゲームでは、いろいろな処理が登場します。

すべてをmain関数の中に書くと、プログラムが長くなり、どこで何をしているのか分かりにくくなります。

そこで、処理ごとに関数へ分けます。

関数役割
showStatus勇者と敵の状態を表示する
showMenu行動メニューを表示する
inputActionプレイヤーの入力を受け取る
calcDamageダメージを計算する
normalAttack通常攻撃を行う
skillAttackMPを使ってスキル攻撃を行う
healMPを使ってHPを回復する
enemyTurn敵の行動を処理する
isAliveキャラクターが生きているか判定する

関数に分けると、1つ1つの処理が読みやすくなります。

また、後からゲームを改造するときも、変更したい関数を探しやすくなります。

ゲーム全体の流れ

ゲーム全体の流れは、次のようになります。

手順内容
1勇者と敵データを準備する
2現在の敵を表示する
3勇者の行動を選ぶ
4勇者の行動を実行する
5敵のHPが0以下なら次の敵へ進む
6敵が生きていれば敵の行動を実行する
7勇者のHPが0以下なら敗北
8すべての敵を倒したら勝利

このように、ゲームの中心は戦闘ループです。

勇者が行動し、敵が行動し、どちらかのHPが0以下になるまで繰り返します。
複数の敵を倒すために、敵の配列を使って順番に登場させます。

RPG風バトルゲームのサンプルプログラム

次のプログラムは、コンソール上で動くRPG風バトルゲームです。

勇者は、通常攻撃、スキル攻撃、回復を選べます。
敵は自動で攻撃してきます。
すべての敵を倒すと勝利です。

プロジェクト/ファイル名: Lesson73/main.c

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define NAME_LENGTH 32
#define ENEMY_COUNT 3
#define SKILL_MP 5
#define HEAL_MP 4

typedef struct {
    char name[NAME_LENGTH];
    int hp;
    int maxHp;
    int mp;
    int maxMp;
    int attack;
    int defense;
    int speed;
} Character;

void showStatus(const Character* hero, const Character* enemy);
void showMenu(void);
int inputAction(void);
int calcDamage(int attack, int defense, int bonus);
void normalAttack(Character* attacker, Character* defender);
void skillAttack(Character* attacker, Character* defender);
void heal(Character* hero);
void enemyTurn(Character* hero, Character* enemy);
int isAlive(const Character* character);
void showResult(const Character* hero, int cleared);

int main(void) {
    Character hero = {
        "勇者",
        120,
        120,
        25,
        25,
        18,
        8,
        10
    };

    Character enemies[ENEMY_COUNT] = {
        { "スライム", 45, 45, 0, 0, 10, 3, 5 },
        { "ゴブリン", 70, 70, 0, 0, 14, 5, 8 },
        { "ドラゴン", 110, 110, 0, 0, 20, 8, 6 }
    };

    int stage = 0;
    int action;
    int turn = 1;

    srand((unsigned)time(NULL));

    printf("=== C言語 RPG風バトルゲーム ===\n");
    printf("%sは冒険に出発した!\n", hero.name);

    while (isAlive(&hero) && stage < ENEMY_COUNT) {
        Character* enemy = &enemies[stage];

        printf("\n------------------------------\n");
        printf("第%d戦: %sが現れた!\n", stage + 1, enemy->name);

        while (isAlive(&hero) && isAlive(enemy)) {
            printf("\n【ターン%d】\n", turn);

            showStatus(&hero, enemy);
            showMenu();

            action = inputAction();

            switch (action) {
                case 1:
                    normalAttack(&hero, enemy);
                    break;

                case 2:
                    skillAttack(&hero, enemy);
                    break;

                case 3:
                    heal(&hero);
                    break;

                default:
                    printf("入力が正しくないため、様子を見ることにしました。\n");
                    break;
            }

            if (!isAlive(enemy)) {
                printf("%sを倒した!\n", enemy->name);
                stage++;
                break;
            }

            enemyTurn(&hero, enemy);

            turn++;
        }
    }

    showResult(&hero, stage >= ENEMY_COUNT);

    return 0;
}

void showStatus(const Character* hero, const Character* enemy) {
    printf("\n%s HP:%d/%d MP:%d/%d\n",
        hero->name, hero->hp, hero->maxHp, hero->mp, hero->maxMp);

    printf("%s HP:%d/%d\n",
        enemy->name, enemy->hp, enemy->maxHp);
}

void showMenu(void) {
    printf("\n行動を選んでください。\n");
    printf("1: 通常攻撃\n");
    printf("2: スキル攻撃 MP%d消費\n", SKILL_MP);
    printf("3: 回復 MP%d消費\n", HEAL_MP);
    printf("番号を入力: ");
}

int inputAction(void) {
    int action;
    int ch;

    if (scanf("%d", &action) != 1) {
        while ((ch = getchar()) != 10 && ch != EOF) {
            /* 入力バッファを読み飛ばす */
        }
        return 0;
    }

    return action;
}

int calcDamage(int attack, int defense, int bonus) {
    int randomValue = rand() % 6;
    int damage = attack + bonus + randomValue - defense / 2;

    if (damage < 1) {
        damage = 1;
    }

    return damage;
}

void normalAttack(Character* attacker, Character* defender) {
    int damage;

    damage = calcDamage(attacker->attack, defender->defense, 0);
    defender->hp -= damage;

    if (defender->hp < 0) {
        defender->hp = 0;
    }

    printf("%sの通常攻撃! %sに%dダメージ!\n",
        attacker->name, defender->name, damage);
}

void skillAttack(Character* attacker, Character* defender) {
    int damage;

    if (attacker->mp < SKILL_MP) {
        printf("MPが足りない! スキル攻撃は使えません。\n");
        return;
    }

    attacker->mp -= SKILL_MP;

    damage = calcDamage(attacker->attack, defender->defense, 12);
    defender->hp -= damage;

    if (defender->hp < 0) {
        defender->hp = 0;
    }

    printf("%sは必殺技を放った! %sに%dダメージ!\n",
        attacker->name, defender->name, damage);
}

void heal(Character* hero) {
    int recover;

    if (hero->mp < HEAL_MP) {
        printf("MPが足りない! 回復は使えません。\n");
        return;
    }

    hero->mp -= HEAL_MP;

    recover = 18 + rand() % 8;
    hero->hp += recover;

    if (hero->hp > hero->maxHp) {
        hero->hp = hero->maxHp;
    }

    printf("%sは回復した! HPが%d回復!\n", hero->name, recover);
}

void enemyTurn(Character* hero, Character* enemy) {
    int damage;
    int action = rand() % 100;

    if (action < 30) {
        damage = calcDamage(enemy->attack, hero->defense, 5);
        printf("%sの強攻撃! ", enemy->name);
    } else {
        damage = calcDamage(enemy->attack, hero->defense, 0);
        printf("%sの攻撃! ", enemy->name);
    }

    hero->hp -= damage;

    if (hero->hp < 0) {
        hero->hp = 0;
    }

    printf("%sに%dダメージ!\n", hero->name, damage);
}

int isAlive(const Character* character) {
    return character->hp > 0;
}

void showResult(const Character* hero, int cleared) {
    printf("\n==============================\n");

    if (cleared) {
        printf("すべての敵を倒した!\n");
        printf("%sの勝利です!\n", hero->name);
    } else {
        printf("%sは倒れてしまった...\n", hero->name);
        printf("ゲームオーバー\n");
    }

    printf("==============================\n");
}

実行例

乱数を使っているため、ダメージや戦闘の流れは実行するたびに少し変わります。
次は実行例の一部です。

=== C言語 RPG風バトルゲーム ===
勇者は冒険に出発した!

------------------------------
第1戦: スライムが現れた!

【ターン1】

勇者 HP:120/120 MP:25/25
スライム HP:45/45

行動を選んでください。
1: 通常攻撃
2: スキル攻撃 MP5消費
3: 回復 MP4消費
番号を入力: 2
勇者は必殺技を放った! スライムに27ダメージ!
スライムの攻撃! 勇者に8ダメージ!

【ターン2】

勇者 HP:112/120 MP:20/25
スライム HP:18/45

行動を選んでください。
1: 通常攻撃
2: スキル攻撃 MP5消費
3: 回復 MP4消費
番号を入力: 1
勇者の通常攻撃! スライムに18ダメージ!
スライムを倒した!

入力する行動や乱数によって、勝敗や途中経過は変わります。
この変化が、ゲームらしさにつながっています。

Character構造体の役割

このプログラムの中心になるのがCharacter構造体です。

typedef struct {
    char name[NAME_LENGTH];
    int hp;
    int maxHp;
    int mp;
    int maxMp;
    int attack;
    int defense;
    int speed;
} Character;

Character構造体には、キャラクター1体分の情報をまとめています。

メンバ内容
name名前
hp現在のHP
maxHp最大HP
mp現在のMP
maxMp最大MP
attack攻撃力
defense防御力
speed素早さ

今回のプログラムでは、勇者も敵もCharacter型で表しています。

Character hero = {
    "勇者",
    120,
    120,
    25,
    25,
    18,
    8,
    10
};

敵は配列で管理しています。

Character enemies[ENEMY_COUNT] = {
    { "スライム", 45, 45, 0, 0, 10, 3, 5 },
    { "ゴブリン", 70, 70, 0, 0, 14, 5, 8 },
    { "ドラゴン", 110, 110, 0, 0, 20, 8, 6 }
};

構造体配列にすることで、複数の敵を順番に登場させられます。

戦闘ループの仕組み

このゲームでは、while文を使って戦闘を続けています。

外側のwhile文は、勇者が生きていて、まだ敵が残っている間だけ続きます。

while (isAlive(&hero) && stage < ENEMY_COUNT) {
    ...
}

内側のwhile文は、勇者と現在の敵がどちらも生きている間だけ続きます。

while (isAlive(&hero) && isAlive(enemy)) {
    ...
}

つまり、外側はゲーム全体の進行、内側は1体の敵との戦闘を表しています。

ループ役割
外側のwhile文敵を順番に出す
内側のwhile文現在の敵との戦闘を続ける

この2重のループにより、複数の敵と順番に戦えるようになっています。

図:戦闘ループの流れ

この図から分かること

この図から分かるのは、RPG風バトルゲームの中心が繰り返し処理でできているということです。

勇者の行動、敵のHP判定、敵の行動、勇者のHP判定を1ターンとして繰り返します。敵を倒せば次の敵へ進み、勇者のHPが0になるとゲームオーバーになります。ゲームの進行は、条件分岐とループを組み合わせて作られています。

showStatus関数で現在の状態を表示する

showStatus関数は、勇者と敵の現在のHPやMPを表示します。

void showStatus(const Character* hero, const Character* enemy)

引数はCharacter型のポインタです。

この関数は表示だけを行い、データを変更しません。
そのため、constを付けています。

const Character* hero
const Character* enemy

constを付けることで、関数内でキャラクター情報を変更しないという意図が分かりやすくなります。

構造体ポインタからメンバにアクセスするので、->を使います。

hero->hp
enemy->name

inputAction関数で入力を受け取る

inputAction関数では、プレイヤーが選んだ番号を受け取ります。

int inputAction(void)

scanfで整数を読み取り、入力に失敗した場合は0を返します。

if (scanf("%d", &action) != 1) {
    ...
    return 0;
}

入力が正しくない場合、残った入力を読み飛ばしています。

while ((ch = getchar()) != 10 && ch != EOF) {
    /* 入力バッファを読み飛ばす */
}

10は改行コードを表しています。
数字以外が入力された場合に、次回の入力へ悪影響が出にくくなるようにしています。

switch文で行動を分ける

プレイヤーの行動は、switch文で分けています。

switch (action) {
    case 1:
        normalAttack(&hero, enemy);
        break;

    case 2:
        skillAttack(&hero, enemy);
        break;

    case 3:
        heal(&hero);
        break;

    default:
        printf("入力が正しくないため、様子を見ることにしました。\n");
        break;
}
入力行動
1通常攻撃
2スキル攻撃
3回復
その他何もしない

メニューから番号を選び、その番号に応じて処理を切り替える流れは、ゲームだけでなく、設定メニューや管理ツールなどにも応用できます。

calcDamage関数でダメージを計算する

ダメージ計算はcalcDamage関数にまとめています。

int calcDamage(int attack, int defense, int bonus)

この関数では、攻撃力、防御力、ボーナス値、乱数を使ってダメージを計算します。

int randomValue = rand() % 6;
int damage = attack + bonus + randomValue - defense / 2;

randを使うことで、毎回少し違うダメージになります。
これにより、同じ攻撃でもゲームに変化が出ます。

また、ダメージが0以下にならないように、最低値を1にしています。

if (damage < 1) {
    damage = 1;
}

これにより、防御力が高い敵にも最低1ダメージは与えられます。

normalAttack関数で通常攻撃を行う

normalAttack関数は、攻撃側と防御側を受け取り、防御側のHPを減らします。

void normalAttack(Character* attacker, Character* defender)

引数はCharacter型のポインタです。
防御側のHPを変更する必要があるため、ポインタで受け取っています。

defender->hp -= damage;

HPがマイナスになった場合は、0に補正しています。

if (defender->hp < 0) {
    defender->hp = 0;
}

ゲームでは、HP表示を分かりやすくするため、0未満の値を表示しないようにすることが多いです。

skillAttack関数でMPを使った攻撃を行う

skillAttack関数は、MPを消費して強い攻撃を行います。

void skillAttack(Character* attacker, Character* defender)

最初にMPが足りるか確認しています。

if (attacker->mp < SKILL_MP) {
    printf("MPが足りない! スキル攻撃は使えません。\n");
    return;
}

MPが足りない場合は、returnで関数を終了します。
MPが足りる場合は、MPを消費してから強めのダメージを計算します。

attacker->mp -= SKILL_MP;
damage = calcDamage(attacker->attack, defender->defense, 12);

通常攻撃よりbonusが大きいため、スキル攻撃のほうが大きなダメージを与えやすくなっています。

heal関数でHPを回復する

heal関数では、勇者のHPを回復します。

void heal(Character* hero)

回復にもMPを使います。
MPが足りない場合は回復できません。

if (hero->mp < HEAL_MP) {
    printf("MPが足りない! 回復は使えません。\n");
    return;
}

回復量にも乱数を使っています。

recover = 18 + rand() % 8;

HPを回復したあと、最大HPを超えないようにしています。

if (hero->hp > hero->maxHp) {
    hero->hp = hero->maxHp;
}

この処理がないと、最大HPを超えて回復してしまいます。
ゲームのルールを守るために、上限チェックは大切です。

enemyTurn関数で敵の行動を作る

enemyTurn関数では、敵が勇者へ攻撃します。

void enemyTurn(Character* hero, Character* enemy)

敵の行動は乱数で少し変えています。

int action = rand() % 100;

actionが30未満なら強攻撃、それ以外なら通常攻撃にしています。

if (action < 30) {
    damage = calcDamage(enemy->attack, hero->defense, 5);
    printf("%sの強攻撃! ", enemy->name);
} else {
    damage = calcDamage(enemy->attack, hero->defense, 0);
    printf("%sの攻撃! ", enemy->name);
}

このようにすると、敵の行動に少しランダム性が出ます。

敵AIと呼べるほど複雑ではありませんが、ゲームらしい変化を作るには十分です。

図:関数が構造体ポインタを使ってHPとMPを更新する

この図から分かること

この図から分かるのは、関数に構造体ポインタを渡すことで、勇者や敵のHP、MPを直接変更できるということです。

通常攻撃やスキル攻撃では敵のHPを減らします。回復では勇者のHPを増やし、MPを減らします。敵の行動では勇者のHPを減らします。これらの処理は、構造体ポインタを使って元のデータを更新しています。

isAlive関数で生存判定を行う

isAlive関数は、キャラクターがまだ生きているかを判定します。

int isAlive(const Character* character) {
    return character->hp > 0;
}

HPが0より大きければ真、0以下なら偽になります。

この関数を使うことで、while文の条件が読みやすくなります。

while (isAlive(&hero) && isAlive(enemy)) {
    ...
}

直接hero.hp > 0と書いても動きますが、isAliveという関数名にすることで、何を判定しているのかが分かりやすくなります。

showResult関数で勝敗を表示する

showResult関数では、ゲームの最終結果を表示します。

void showResult(const Character* hero, int cleared)

clearedが真なら、すべての敵を倒したことを表します。
偽なら、勇者が倒れたことを表します。

if (cleared) {
    printf("すべての敵を倒した!\n");
    printf("%sの勝利です!\n", hero->name);
} else {
    printf("%sは倒れてしまった...\n", hero->name);
    printf("ゲームオーバー\n");
}

このように、結果表示を関数に分けておくと、main関数がすっきりします。

このプログラムで学べる設計の考え方

今回のRPG風バトルゲームでは、単に動くプログラムを書くだけでなく、設計の考え方も学べます。

設計ポイント内容
データを構造体にまとめるキャラクター情報を1つの型で扱う
処理を関数に分ける攻撃、回復、表示、判定を分離する
ポインタで更新する関数内でHPやMPを変更する
ループで進行するターン制バトルを繰り返す
乱数で変化をつけるダメージや敵行動を少し変える
定数を使うMP消費量や敵数を分かりやすくする

このように、ゲームはC言語の基礎を総合的に使うよい練習になります。

改造するとさらに学習が深まる

サンプルプログラムが動いたら、少しずつ改造してみると理解が深まります。

改造案学べること
敵を増やす構造体配列の使い方
スキルを増やすswitch文と関数分割
防御コマンドを追加する状態管理とダメージ計算
逃げるコマンドを追加する乱数と条件分岐
レベルアップを追加するステータス更新
アイテムを追加する配列と所持数管理
敵ごとの行動パターンを変える関数設計と条件分岐
セーブ機能を追加するファイル操作

たとえば、防御コマンドを追加する場合、選択肢に4を追加し、そのターンだけ受けるダメージを半分にする処理を入れられます。

アイテムを追加する場合は、回復薬の個数を変数で持ち、使うたびに減らす処理を作れます。

このように、少しずつ機能を追加していくと、C言語でプログラムを設計する力が自然に身につきます。

総合演習として意識したいポイント

このRPG風バトルゲームは、これまで学んできた文法を1つにまとめる総合演習です。

単独の文法だけを見ると、何に使うのか分かりにくいものもあります。
しかし、ゲームの中で使うと役割がはっきりします。

構造体はキャラクターを表すために使います。
関数は攻撃や回復の処理を整理するために使います。
ポインタは関数内でHPやMPを変更するために使います。
while文は戦闘を続けるために使います。
switch文は行動選択を分けるために使います。
randはダメージや敵行動に変化を出すために使います。

このように、文法が実際の処理と結びつくと、C言語の理解がかなり深まります。

RPG風バトルゲームから次の学習へ

今回のプログラムは、1ファイルで動くシンプルなRPG風バトルゲームです。

ここからさらに発展させるなら、ソースコードを複数ファイルに分けることもできます。
たとえば、キャラクター処理をcharacter.c、戦闘処理をbattle.c、表示処理をscreen.cのように分けると、より本格的な設計になります。

また、敵データをファイルから読み込むようにすれば、ファイル操作の練習にもなります。
戦闘結果をログとして保存するようにすれば、テキストファイル出力の練習にもつながります。

C言語の学習では、文法を1つずつ覚えるだけでなく、それらを組み合わせて何かを作ることが大切です。
RPG風バトルゲームは、変数、配列、構造体、ポインタ、関数、ループ、条件分岐、乱数をまとめて使える題材です。

まずはこのプログラムを動かし、次に数値や敵の種類、行動メニューを少しずつ変えて、自分だけのバトルゲームへ発展させてみましょう。