RPGゲームで構造体を学ぶ(C言語・コンソール)
ここでは「構造体」を学びます。 構造体は、複数のデータをひとかたまりとして扱える独自のデータ型で、 長くなるコードを整理整頓する手段のひとつです。
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;
}
この書き方の課題
hero_hp, hero_atk, ... と個数が増えて煩雑。主な書き方は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 より前) に置きます。Character)で進めます。
strcpy() 関数char name[16];)にあとから文字列を入れるときは strcpy を使う。strcpy(あて先配列, "文字列");#include <string.h>最小例
#include <stdio.h>
#include <string.h>
int main(void) {
char s[16];
strcpy(s, "ゆうしゃ");
printf("%s\n", s);
return 0;
}配列への=代入は宣言時のみ可能。宣言後はstrcpyなどを使う。
まず Character 型を定義し、変数を1つ作って値を入れてみる。
変数名.メンバ名hero.hp、hero.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;
}ファイル名: 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
ファイル名: 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)またはポインタ渡しを使います。
ファイル名: 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 を変えても呼び出し元に直接は反映されません。
そこで変更済みの defender を return し、呼び出し元で slime = attack(hero, slime); と受け取ることで更新します。
ファイル名: 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))を用意。 |
| コメント整備 | 各関数の目的・引数・戻り値、構造体フィールドの意味を短く明記。 |