はじめての C# ミニ RPG ― クラス × ループで動かすターン制バトル
「RPGのキャラクターを例にしたオブジェクト指向の説明と実装」で解説されている Character クラス (名前・HP などのプロパティと Attack() / Heal() メソッド)の考え方を踏まえつつ、初学者向けに 配列・while ループ・if だけで完結する 超シンプルなターン制バトルゲーム を “まるごと” 再掲します。
0. Visual Studioでプロジェクトを作成する
コードを入力する前に、Visual Studioでコンソールアプリを作成します。授業中に画面やファイル名をそろえられるよう、次の名前で統一してください。
| 項目 | 設定する内容 |
|---|---|
| テンプレート | コンソール アプリ |
| ソリューション名 | TurnBasedGame |
| プロジェクト名 | TurnBasedGame |
| フレームワーク | 授業で使用している .NET(講師の指定に合わせる) |
授業で取り組む場合:質問や画面確認をしやすくするため、全員が上記のソリューション名・プロジェクト名で作成してください。

TurnBasedGame に統一します。1. ゲーム概要
| 項目 | 内容 |
|---|---|
| ジャンル | 1 対 1 のターン制バトル |
| 勝利条件 | 敵スライムの HP を 0 にする |
| 敗北条件 | 自分の HP が 0 になる |
| 操作 | プレイヤーのターンに ①攻撃 または ②回復 を数字入力 |
| 使う C# 機能 | クラス / メソッド / 配列 / while ループ / if |

学習ポイント
- 配列だけで手番管理(Character[] participants)
- 未習トピック(List, enum, LINQ など)を排除しているので、配列・ループ・条件分岐まで学んだ直後でも動かせる
- 1ファイルのコンソールアプリなので Visual Studio で貼り替えてすぐ試せる
2. 完全ソースコード
using System;
namespace TurnBasedGame
{
// 1. キャラクター基底クラス
class Character
{
public string Name; // 名前
public int HP; // 現在HP
public int MaxHP; // 最大HP
private static Random _rand = new Random();
// 生存判定
public bool IsAlive => HP > 0;
// 攻撃(与えたダメージを返す)
public int Attack()
{
int damage = _rand.Next(10, 21); // 10〜20
Console.WriteLine($"{Name} の攻撃! {damage} ダメージ!");
return damage;
}
// 被ダメージ処理
public void TakeDamage(int damage)
{
HP = Math.Max(HP - damage, 0);
}
// 回復(5〜15)
public void Heal()
{
int heal = _rand.Next(5, 16);
HP = Math.Min(HP + heal, MaxHP);
Console.WriteLine($"{Name} は回復魔法! {heal} 回復(HP: {HP}/{MaxHP})");
}
}
// 2. メインプログラム
class Program
{
static void Main()
{
// 2-1. キャラクター生成
Character player = new Character { Name = "ヒロト", HP = 100, MaxHP = 100 };
Character enemy = new Character { Name = "スライム", HP = 60, MaxHP = 60 };
// 配列で順番を管理(0:プレイヤー 1:敵)
Character[] participants = { player, enemy };
int turn = 0; // 先攻はプレイヤー
// 2-2. バトルループ
while (player.IsAlive && enemy.IsAlive)
{
Character current = participants[turn];
Character target = participants[1 - turn];
if (current == player)
{
// --- プレイヤーターン ---
Console.WriteLine("\n=== あなたのターン ===");
Console.WriteLine($"敵 HP: {target.HP}/{target.MaxHP}");
Console.Write("1)攻撃 2)回復 > ");
string input = Console.ReadLine();
if (input == "2")
current.Heal();
else
target.TakeDamage(current.Attack());
}
else
{
// --- 敵ターン(常に攻撃)---
Console.WriteLine("\n--- 敵のターン ---");
target.TakeDamage(current.Attack());
}
turn = 1 - turn; // 手番交代
}
// 2-3. 勝敗表示
Console.WriteLine(player.IsAlive ? "\n★ 勝利!" : "\n★ 敗北…");
}
}
}
全体の流れ
- キャラクタークラスを作って、名前・HP・攻撃・回復の機能をまとめる
- プレイヤーと敵を作って、ターン制で交互に行動させる
- HPが0になったらバトル終了、勝敗を表示する
1. キャラクタークラス Character
役割
ゲームに登場する「キャラクター(プレイヤーも敵も)」が共通で持つデータと動きをまとめたクラスです。
主なメンバー
| 項目 | 説明 |
|---|---|
| Name | キャラクターの名前です。(例:ヒロト、スライム) |
| HP / MaxHP | 現在のHPと最大HPを表す整数です。 |
| IsAlive | HPが0より大きいかを調べて、生きているかを判定します。 |
| Attack() | ランダムで10〜20のダメージを発生させ、攻撃したことを表示してそのダメージを返します。 |
| TakeDamage(int damage) | ダメージを受けてHPを減らします(HPがマイナスにならないように調整)。 |
| Heal() | 5〜15の範囲で回復します(HPは最大HPを超えないようにします)。 |
IsAliveを詳しく学びたい方へ:
【C#入門】public bool IsAlive => HP > 0; を読み解く|式形式プロパティと生存判定では、bool、式形式プロパティ、HPが0になる境界値を図解付きで解説しています。
ポイント
- Random はランダムな数を作るために使っています。
- Math.Max は「値がマイナスにならないように最低0にする」ために使っています。
- Math.Min は「HPが最大HPを超えないようにする」ために使っています。
2. メインプログラム Program
役割
ゲームを動かす「シナリオ」部分です。
流れ
2-1. キャラクター生成
Character player = new Character { Name = "ヒロト", HP = 100, MaxHP = 100 };
Character enemy = new Character { Name = "スライム", HP = 60, MaxHP = 60 };
- player と enemy を作成します。
- Name、HP、MaxHP を設定しています。
2-2. バトルループ
while (player.IsAlive && enemy.IsAlive)
{
// ...
}
- どちらかのHPが0になるまで繰り返します。
- turn という変数で「どっちのターンか」を切り替えます。
プレイヤーターン
- コンソールに 1)攻撃 2)回復 を表示
- 入力を Console.ReadLine() で読み取る
- 入力が “2" なら Heal()、それ以外なら Attack() して相手に TakeDamage() します。
敵ターン
- 敵は常に攻撃します。入力は不要。
- Attack() してプレイヤーに TakeDamage() します。
2-3. 勝敗表示
Console.WriteLine(player.IsAlive ? "\n★ 勝利!" : "\n★ 敗北…");
- プレイヤーが生きていれば勝利、そうでなければ敗北です。
- ? は条件演算子と呼ばれ、「もし条件が true なら左、false なら右」と分岐します。
3. まとめ
このコードで学べるポイント:
✅ クラスの作り方
✅ フィールド(データ)の使い方
✅ メソッド(行動)の使い方
✅ while 文での繰り返し処理
✅ if 文での条件分岐
✅ 配列で参加者を管理する方法
✅ Random でランダムな数値を作る方法
4. アドバイス
- 「自分のHPがどう変わっているか」を Console.WriteLine() で何度も表示して確認してみると理解が深まります。
- クラスを作ると、同じ機能を複数のキャラクターに使い回せるのが便利です。
- 改造して敵を複数にしたり、攻撃の威力をキャラごとに変えたりしてみましょう!
完成コードを自分で組み立て直してみよう
完成コードを上から順番に写すだけでなく、クラス・メソッド・バトルループなどの大きな骨格を先に作り、中身を後から埋めると、プログラム全体の構造を理解しやすくなります。
具体的な進め方は、【C#学習法】完成コードを上から写さない|ターン制バトルを大きなブロックから組み立てる写経術で段階的に解説しています。
3. 実行結果サンプル
※ ダメージと回復量は乱数で毎回変わります。
=== あなたのターン ===
敵 HP: 60/60
1)攻撃 2)回復 > 1
ヒロト の攻撃! 17 ダメージ!
--- 敵のターン ---
スライム の攻撃! 12 ダメージ!
=== あなたのターン ===
敵 HP: 43/60
1)攻撃 2)回復 > 2
ヒロト は回復魔法! 10 回復(HP: 98/100)
--- 敵のターン ---
スライム の攻撃! 14 ダメージ!
=== あなたのターン ===
敵 HP: 43/60
1)攻撃 2)回復 > 1
ヒロト の攻撃! 19 ダメージ!
--- 敵のターン ---
スライム の攻撃! 11 ダメージ!
=== あなたのターン ===
敵 HP: 24/60
1)攻撃 2)回復 > 1
ヒロト の攻撃! 20 ダメージ!
--- 敵のターン ---
スライム の攻撃! 13 ダメージ!
=== あなたのターン ===
敵 HP: 4/60
1)攻撃 2)回復 > 1
ヒロト の攻撃! 15 ダメージ!
★ 勝利!
4. コードの読みどころ
| 重点ポイント | 解説 |
|---|---|
| 状態とふるまいの一体化 | Character に「データ(Name/HP)」と「行動(Attack/Heal)」をまとめ、オブジェクト指向の“モノらしさ”を体験できる。 |
| 配列でターン管理 | Character[] participants + turn = 1 – turn で 0↔1 を交互に切替。わずか2要素の固定長なら配列で十分。 |
| 未習機能は使わない | List や enum はあえて未使用。授業進度に合わせて乱数や三項演算子を削るなど調整も容易。 |
5. さらに拡張してみよう(次の学習課題)
| 学習テーマ | 追加アイデア例 |
|---|---|
| 多次元配列 | 味方3人 vs 敵3体を Character[2,3] で管理 |
| 条件分岐強化 | 敵 AI を「HP が少ないと回復」「偶数ターンは強攻撃」などに変更 |
| 戻り値パターン比較 | Attack() を void に書き換え、ダメージ適用を内部処理に |
| enum 活用 | 行動種別(Attack / Heal / Guard …)を列挙型で管理し switch で分岐 |
サンプル回答(クリックして確認)
まずは自分で改造してみましょう。行き詰まった場合や、別の書き方と比較したい場合にサンプル回答を開いてください。プログラムには複数の実現方法があるため、ここで紹介するコードは回答例の一つです。
考え方:Character[2, 3] の第1添字をチーム、第2添字をメンバー番号として使います。配列の扱いに集中するため、この例では全員が自動で攻撃します。
using System;
class Character
{
public string Name;
public int HP;
public int MaxHP;
private static Random _rand = new Random();
public bool IsAlive => HP > 0;
public int Attack()
{
int damage = _rand.Next(10, 21);
Console.WriteLine($"{Name} の攻撃! {damage} ダメージ!");
return damage;
}
public void TakeDamage(int damage)
{
HP = Math.Max(HP - damage, 0);
}
}
class Program
{
static void Main()
{
Character[,] teams =
{
{
new Character { Name = "勇者", HP = 100, MaxHP = 100 },
new Character { Name = "魔法使い", HP = 70, MaxHP = 70 },
new Character { Name = "僧侶", HP = 80, MaxHP = 80 }
},
{
new Character { Name = "スライム", HP = 50, MaxHP = 50 },
new Character { Name = "ゴブリン", HP = 70, MaxHP = 70 },
new Character { Name = "オーク", HP = 100, MaxHP = 100 }
}
};
int team = 0;
int member = 0;
while (HasAlive(teams, 0) && HasAlive(teams, 1))
{
Character current = teams[team, member];
if (current.IsAlive)
{
Character target = FindFirstAlive(teams, 1 - team);
target.TakeDamage(current.Attack());
Console.WriteLine($"{target.Name} の残りHP: {target.HP}/{target.MaxHP}");
}
member++;
if (member == teams.GetLength(1))
{
member = 0;
team = 1 - team;
Console.WriteLine();
}
}
Console.WriteLine(HasAlive(teams, 0)
? "★ 味方チームの勝利!"
: "★ 敵チームの勝利!");
}
static bool HasAlive(Character[,] teams, int team)
{
for (int i = 0; i < teams.GetLength(1); i++)
{
if (teams[team, i].IsAlive)
return true;
}
return false;
}
static Character FindFirstAlive(Character[,] teams, int team)
{
for (int i = 0; i < teams.GetLength(1); i++)
{
if (teams[team, i].IsAlive)
return teams[team, i];
}
return null;
}
}
ポイント:GetLength(1) で1チームの人数を取得し、倒れたキャラクターは飛ばします。FindFirstAlive() は相手チームから最初に生きている対象を探します。
考え方:敵のHPと敵ターン数を調べ、行動を3種類に分けます。
- HPが最大値の3分の1以下なら回復
- 偶数回目の敵ターンなら強攻撃
- それ以外は通常攻撃
// Character クラスに追加
public int StrongAttack()
{
int damage = _rand.Next(20, 31);
Console.WriteLine($"{Name} の強攻撃! {damage} ダメージ!");
return damage;
}
// while文の前に追加
int enemyTurnCount = 0;
// 敵ターンを置き換える
else
{
enemyTurnCount++;
Console.WriteLine("\n--- 敵のターン ---");
if (current.HP <= current.MaxHP / 3)
{
current.Heal();
}
else if (enemyTurnCount % 2 == 0)
{
target.TakeDamage(current.StrongAttack());
}
else
{
target.TakeDamage(current.Attack());
}
}
ポイント:条件の優先順位が重要です。この例では、HPが少ない場合の回復を最優先にしています。
考え方:Attack() がダメージを返す代わりに、攻撃相手を受け取ってダメージ適用まで行います。
// 変更前
public int Attack()
{
int damage = _rand.Next(10, 21);
Console.WriteLine($"{Name} の攻撃! {damage} ダメージ!");
return damage;
}
// 変更後:攻撃相手を引数で受け取る
public void Attack(Character target)
{
int damage = _rand.Next(10, 21);
Console.WriteLine($"{Name} の攻撃! {damage} ダメージ!");
target.TakeDamage(damage);
}
// 呼び出し側
current.Attack(target);
比較:戻り値版は「計算」と「適用」を分けられるため柔軟です。void版は呼び出しが短くなりますが、攻撃メソッドが相手への適用まで担当します。どちらが絶対に正しいというものではありません。
考え方:数字をそのまま比較せず、行動の種類をActionTypeとして名前で表します。防御中は次に受けるダメージを半分にします。
enum ActionType
{
Attack = 1,
Heal = 2,
Guard = 3
}
// Character クラスに追加
public bool IsGuarding;
public void Guard()
{
IsGuarding = true;
Console.WriteLine($"{Name} は身を守っている!");
}
// TakeDamage() を変更
public void TakeDamage(int damage)
{
if (IsGuarding)
{
damage /= 2;
IsGuarding = false;
Console.WriteLine("ガードでダメージを半分にした!");
}
HP = Math.Max(HP - damage, 0);
}
// プレイヤーターン
Console.Write("1)攻撃 2)回復 3)防御 > ");
string input = Console.ReadLine();
if (int.TryParse(input, out int value) &&
Enum.IsDefined(typeof(ActionType), value))
{
ActionType action = (ActionType)value;
switch (action)
{
case ActionType.Attack:
target.TakeDamage(current.Attack());
break;
case ActionType.Heal:
current.Heal();
break;
case ActionType.Guard:
current.Guard();
break;
}
}
else
{
Console.WriteLine("1〜3を入力してください。");
}
ポイント:enumを使うと、1や2の意味がコード上で明確になります。行動を増やす場合もswitchへ追加できます。











ディスカッション
コメント一覧
まだ、コメントがありません