28_構造体

RPGゲームで構造体を学ぶ(C言語・コンソール)

ここでは「構造体」を学びます。 構造体は、複数のデータをひとかたまりとして扱える独自のデータ型で、 長くなるコードを整理整頓する手段のひとつです。


例1:変数で実装

RPG風の各キャラクターのパラメータを、ばらばらの変数で管理した例。

ファイル名: struct_example_no_struct.c

#include <stdio.h>

int main(void) {
    /* キャラクターのパラメータ(ばらばら) */
    int hero_hp;     /* ゆうしゃHP */
    int hero_atk;    /* ゆうしゃ攻撃力 */
    int slime_hp;    /* スライムHP */
    int slime_atk;   /* スライム攻撃力(今回は未使用) */

    hero_hp  = 30;
    hero_atk = 5;
    slime_hp = 18;
    slime_atk = 2;

    printf("[ゆうしゃ] HP=%d ATK=%d\n", hero_hp, hero_atk);
    printf("[スライム] HP=%d ATK=%d\n", slime_hp, slime_atk);

    /* 単純な攻撃:ゆうしゃ→スライム */
    slime_hp = slime_hp - hero_atk;
    if (slime_hp < 0) {
        slime_hp = 0;
    }

    printf("ゆうしゃ の攻撃! スライム に %d ダメージ\n", hero_atk);
    printf("[スライム] HP=%d ATK=%d\n", slime_hp, slime_atk);

    return 0;
}

この書き方の課題


構造体定義の書き方とルール

主な書き方は2通り

方式特徴変数宣言の書き方
タグ方式 毎回 struct を付ける struct Character hero;
typedef 方式 新しい型名を作り、以後は struct 不要 Character hero;

構文(ひな型)

/* タグ方式 */
struct 構造体名 {
    型 メンバ名;
    /* ... */
};  /* ← 最後にセミコロン */
/* typedef 方式 */
typedef struct {
    型 メンバ名;
    /* ... */
} 型名;  /* ← ここにもセミコロン */

コード例

1. タグ方式

struct Character {
    char name[16];
    int  hp;
    int  atk;
};

int main(void) {
    struct Character hero;  /* 宣言 */
    return 0;
}

2. typedef 方式(新しい型名を作り、以後は struct 不要)

#include <stdio.h>

typedef struct {
    char name[16];
    int  hp;
    int  atk;
} Character;  /* 例:Character という型名を作る */

int main(void) {
    Character hero;  /* 宣言 */
    return 0;
}

※ タグ名も残したい場合は typedef struct Character { ... } Character; と書けます。

記述位置: 型定義は ファイル先頭(#include の下、main より前) に置きます。
この資料では以後のコードはすべて typedef 方式(型名は Character)で進めます。

文字列の扱い:strcpy() 関数

最小例

#include <stdio.h>
#include <string.h>

int main(void) {
    char s[16];
    strcpy(s, "ゆうしゃ");
    printf("%s\n", s);
    return 0;
}
配列への = 代入は宣言時のみ可能。宣言後は strcpy などを使う。

例2:構造体という仕組みで、関連データをひとかたまりにする

まず Character 型を定義し、変数を1つ作って値を入れてみる。

Character hero

char[16]name
inthp
intatk
→ メンバへのアクセスは 変数名.メンバ名
例:hero.hphero.atk
#include <stdio.h>
#include <string.h>

typedef struct {
    char name[16];
    int  hp;
    int  atk;
} Character;

int main(void) {
    Character hero;

    strcpy(hero.name, "ゆうしゃ");
    hero.hp  = 30;
    hero.atk = 5;

    printf("%s HP=%d ATK=%d\n", hero.name, hero.hp, hero.atk);
    return 0;
}

手順 A 最小の構造体で値を入れて表示

ファイル名: struct_stepA_min.c

#include <stdio.h>
#include <string.h>

/* 型定義 */
typedef struct {
    char name[16];
    int  hp;
    int  atk;
} Character;

int main(void) {
    Character hero;  /* 関数先頭で宣言 */

    strcpy(hero.name, "ゆうしゃ");
    hero.hp  = 30;
    hero.atk = 5;

    printf("名前=%s HP=%d ATK=%d\n", hero.name, hero.hp, hero.atk);
    return 0;
}

実行例

名前=ゆうしゃ HP=30 ATK=5

手順 B 表示処理を関数に分ける(値渡し)

ファイル名: struct_stepB_print.c

#include <stdio.h>
#include <string.h>

typedef struct {
    char name[16];
    int  hp;
    int  atk;
} Character;

/* 値渡し:引数の c は呼び出し元の変数のコピー */
void print_status(Character c) {
    printf("[%s] HP=%d ATK=%d\n", c.name, c.hp, c.atk);
}

int main(void) {
    Character hero;

    strcpy(hero.name, "ゆうしゃ");
    hero.hp  = 30;
    hero.atk = 5;

    print_status(hero);  /* ← 値渡し */
    return 0;
}
構造体の値渡し: 構造体を関数に渡すと、すべてのメンバが丸ごとコピーされます。 関数内で c.hp = 0; などと書いても、呼び出し元の hero.hp は変わりません。 呼び出し元の値を変えたい場合は、戻り値(手順C)またはポインタ渡しを使います。

手順 C 攻撃処理を関数化(戻り値で更新)

ファイル名: struct_stepC_attack.c

#include <stdio.h>
#include <string.h>

typedef struct {
    char name[16];
    int  hp;
    int  atk;
} Character;

void print_status(Character c) {
    printf("[%s] HP=%d ATK=%d\n", c.name, c.hp, c.atk);
}

/* attacker の攻撃で defender を更新し、更新後の defender を返す */
Character attack(Character attacker, Character defender) {
    int dmg;
    dmg = attacker.atk;              /* まずは固定ダメージ */
    defender.hp = defender.hp - dmg;
    if (defender.hp < 0) {
        defender.hp = 0;
    }
    printf("%s の攻撃! %s に %d ダメージ\n", attacker.name, defender.name, dmg);
    return defender;  /* 更新済みの defender を返す */
}

int main(void) {
    Character hero;
    Character slime;

    strcpy(hero.name,  "ゆうしゃ");
    strcpy(slime.name, "スライム");
    hero.hp  = 30;  hero.atk  = 5;
    slime.hp = 18;  slime.atk = 2;

    print_status(hero);
    print_status(slime);

    slime = attack(hero, slime);   /* ← 戻り値で更新 */

    print_status(slime);
    return 0;
}

実行例

[ゆうしゃ] HP=30 ATK=5
[スライム] HP=18 ATK=2
ゆうしゃ の攻撃! スライム に 5 ダメージ
[スライム] HP=13 ATK=2
戻り値による更新のしくみ: attack() は引数を値渡しで受け取るため、関数内で defender.hp を変えても呼び出し元に直接は反映されません。 そこで変更済みの defenderreturn し、呼び出し元で slime = attack(hero, slime); と受け取ることで更新します。

手順 D 構造体の配列で敵を複数体にする

ファイル名: struct_stepD_array.c

#include <stdio.h>
#include <string.h>

typedef struct {
    char name[16];
    int  hp;
    int  atk;
} Character;

void print_status(Character c) {
    printf("[%s] HP=%d ATK=%d\n", c.name, c.hp, c.atk);
}

int main(void) {
    Character enemy[3];  /* 3体分の構造体配列 */
    int i;

    strcpy(enemy[0].name, "スライム");   enemy[0].hp = 10;  enemy[0].atk = 2;
    strcpy(enemy[1].name, "コウモリ");   enemy[1].hp = 12;  enemy[1].atk = 3;
    strcpy(enemy[2].name, "おおねずみ"); enemy[2].hp = 14;  enemy[2].atk = 2;

    for (i = 0; i < 3; i++) {
        print_status(enemy[i]);   /* 値渡し */
    }
    return 0;
}

実行例

[スライム] HP=10 ATK=2
[コウモリ] HP=12 ATK=3
[おおねずみ] HP=14 ATK=2
配列との組み合わせ: Character enemy[3] と書くと、enemy[0]enemy[1]enemy[2] がそれぞれ Character 型の変数になります。 メンバへのアクセスは enemy[0].hp のように「添字 → ドット → メンバ名」の順です。

おまけ:乱数でダメージに幅を持たせる

ファイル名: struct_damage_rand_option.c

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

typedef struct {
    char name[16];
    int  hp;
    int  atk;
} Character;

void print_status(Character c) {
    printf("[%s] HP=%d ATK=%d\n", c.name, c.hp, c.atk);
}

/* [a, b] の範囲でランダムな整数を返す */
int rand_range(int a, int b) {
    int r;
    r = rand() % (b - a + 1) + a;
    return r;
}

/* ランダムダメージ版:更新後の defender を返す */
Character attack_random(Character attacker, Character defender) {
    int dmg;
    dmg = rand_range(attacker.atk - 1, attacker.atk + 1);  /* atk±1 のばらつき */
    if (dmg < 0) { dmg = 0; }
    defender.hp = defender.hp - dmg;
    if (defender.hp < 0) { defender.hp = 0; }
    printf("%s の攻撃! %s に %d ダメージ\n", attacker.name, defender.name, dmg);
    return defender;
}

int main(void) {
    Character h;
    Character s;

    strcpy(h.name, "ゆうしゃ");  h.hp = 30;  h.atk = 5;
    strcpy(s.name, "スライム");  s.hp = 18;  s.atk = 2;

    srand((unsigned)time(NULL));  /* 乱数初期化は最初の1回だけ */

    s = attack_random(h, s);  /* 戻り値で更新 */

    print_status(s);
    return 0;
}

仕上げの工夫

以下の中から選択したり、自分なりの工夫を加えて完成版に仕上げてみてください。

テーマ内容
名前の入力対応自分の名前を入力してキャラクター名に設定。未入力時のデフォルト名も用意。
入力バリデーションHPやメニュー番号の入力が範囲外なら再入力を促す(負の数や文字を弾く)。
ダメージ演出メッセージを整形して読みやすく(区切り線・行間・ラウンド見出し)。
HPの下限処理HPは必ず0で止める(負になる表示を防ぐ)。
クリティカル一定確率でダメージ増加(例:×2)。発生時はメッセージで強調。
追加パラメータdef(防御力)や heal(回復量)を構造体に追加して活用。
複数戦・全滅判定敵を配列で複数体にし、全滅で戦闘終了→結果まとめを表示。
集計・ランキング勝ち数・総ダメージ・残HPなどをカウントして表形式で提示。
定数の明示最大HPやラウンド数は定数で管理(マジックナンバーをなくす)。
関数の分割初期化/表示/判定/1ターン進行などに分け、1関数1役を徹底。
再現テスト提出前チェック用に乱数の種を固定する版(srand(12345))を用意。
コメント整備各関数の目的・引数・戻り値、構造体フィールドの意味を短く明記。