【学習】WinFormsで作るRPG入門(4)クラス分割 〜BattleManager にまとめる〜
第3回 までで、Form1 に 戦闘のルール(ターゲットの更新・攻撃・回復)が集まってきました。
コードが長くなるほど、「画面のコード」と「ゲームのルール」が混ざると読みにくくなります。今回は 戦闘まわりだけを BattleManager クラスに移し、フォームは 表示と入力に専念する形に整理します。
今回の最小ゴール
BattleManagerがPlayer・List<Enemy>・targetIndex・攻撃/回復の処理を持つForm1はBattleManagerを1つ持ち、btnAction_Clickでは「コマンドを渡す →UpdateUI」が中心になる- 見た目の動きは第3回と同じ(挙動を変えず、置き場所だけ分ける)
前回からの差分
| 第3回 | 第4回 |
|---|---|
DoAttack / DoHeal / EnsureValidTarget が Form1 のメソッド |
上記は BattleManager のメソッド |
player / enemies / targetIndex がフォームのフィールド |
BattleManager の内部(フォームからは battle.Player などで参照) |
| フォーム=画面+ルール | フォーム=画面+入力、ルール=マネージャ |
新しく出てくるキーワード: 責務の分離、ファサード(窓口)のような マネージャークラス
なぜ分けるのか(短く)
- テストや再利用: 戦闘ロジックだけ別クラスにすると、フォームなしで考えやすい
- Unity に近い感覚: 画面用スクリプトと「ルール用」の型を分ける発想に繋がる
- 読みやすさ:
Form1を開いたときに「イベントと表示更新」だけ追えばよくなる
Player / Enemy
第3回と 同じ で構いません(変更しなくてよいです)。
BattleManager クラス(新規)
戦闘に必要なデータと、DoAttack / DoHeal / ターゲット調整を 1か所にまとめます。
using System.Collections.Generic;
public class BattleManager
{
private readonly List<Enemy> _enemies;
private int _targetIndex;
public const int HealAmount = 15;
public Player Player { get; }
// フォームの一覧表示用(中身は読むだけ想定)
public IReadOnlyList<Enemy> Enemies => _enemies;
public int TargetIndex => _targetIndex;
// 敵が全滅したあと(攻撃対象がもういない)
public bool IsVictory => _targetIndex >= _enemies.Count;
public BattleManager(Player player, List<Enemy> enemies)
{
Player = player;
_enemies = enemies;
_targetIndex = 0;
EnsureValidTarget();
}
private void EnsureValidTarget()
{
while (_targetIndex < _enemies.Count && _enemies[_targetIndex].Hp <= 0)
{
_targetIndex++;
}
}
public void DoAttack()
{
EnsureValidTarget();
if (_targetIndex >= _enemies.Count)
return;
Enemy target = _enemies[_targetIndex];
Player.AttackEnemy(target);
if (target.Hp < 0)
target.Hp = 0;
EnsureValidTarget();
}
public void DoHeal()
{
Player.Heal(HealAmount);
}
}IReadOnlyList<Enemy>: フォーム側でforeachして一覧を出せます。外からAddされないようにすると、リストの責任の所在がはっきりします(教材では「まず読み取り専用で公開」と覚えてもよいです)。HealAmount: 第3回のconstをマネージャ側に移した例です。
Form1 のフィールド
player / enemies / targetIndex はやめて、マネージャ1つにします。
BattleManager battle;List<Enemy> は Form1_Load で生成してコンストラクタに渡す形にすると、BattleManager が「リストの所有者」だと分かりやすいです。
初期化(Form1_Load)
Form1.cs の先頭に using System.Collections.Generic; がある前提です(List<Enemy> 用)。
private void Form1_Load(object sender, EventArgs e)
{
var player = new Player("勇者", 100, 20);
var enemies = new List<Enemy>
{
new Enemy("スライムA", 30),
new Enemy("スライムB", 40),
new Enemy("ゴブリン", 60),
};
battle = new BattleManager(player, enemies);
UpdateUI();
}new BattleManager(...) の直後に、内部で EnsureValidTarget が走ります(第3回の Load と同じタイミング)。
決定ボタン(btnAction_Click)
戦闘の中身は battle に任せるだけにします。
private void btnAction_Click(object sender, EventArgs e)
{
if (rdoAttack.Checked)
{
battle.DoAttack();
}
else if (rdoHeal.Checked)
{
battle.DoHeal();
}
UpdateUI();
}UI 更新(UpdateUI)
表示に使うデータは battle 経由で取ります。
private void UpdateUI()
{
lblPlayerHp.Text = $"HP: {battle.Player.Hp} / {battle.Player.MaxHp}";
var sb = new StringBuilder();
foreach (var enemy in battle.Enemies)
{
sb.AppendLine($"{enemy.Name}: {enemy.Hp}");
}
lblEnemyList.Text = sb.ToString();
if (battle.IsVictory)
{
lblTarget.Text = "ターゲット: なし(勝利!)";
lblEnemyHp.Text = "-";
btnAction.Enabled = false;
return;
}
Enemy current = battle.Enemies[battle.TargetIndex];
lblTarget.Text = $"ターゲット: {current.Name}";
lblEnemyHp.Text = $"HP: {current.Hp}";
btnAction.Enabled = true;
}using System.Text;(StringBuilder 用)は第3回と同様に必要です。
プログラムの流れ
Form1_Load
→ Player / List<Enemy> を new
→ BattleManager に渡す(内部で EnsureValidTarget)
→ UpdateUI
btnAction_Click
→ ラジオに応じて battle.DoAttack / battle.DoHeal
→ UpdateUI(battle.IsVictory でボタン無効など)Unity での当たり所
| WinForms(今回) | Unity(イメージ) |
|---|---|
Form1 |
MonoBehaviour(UI バインド・ボタンイベント) |
BattleManager |
Unity 非依存の C# クラスとして同じ構造を置く例が多い |
battle.DoAttack() |
ボタンやコマンド確定から 同じメソッドを呼ぶ |
見た目や入力は Unity 側に寄せつつ、HP やターゲットをどう動かすかは BattleManager に近いクラスに閉じる、という分け方はそのまま応用しやすいです。
よくあるつまずき
「Enemies に敵を追加したい」
- いまは 読み取り専用として渡している前提です。追加したくなったら、
BattleManagerにAddEnemyのようなメソッドを用意するか、コンストラクタの設計を変える、と決めると安全です(ルールをクラス越しにそろえる)。
「フォームから _targetIndex を直接いじりたくなった」
- ターゲットの意味は
BattleManagerの責務にまとめた方がバグが減ります。どうしても必要なら、マネージャにNextTarget()など 名前付きの操作を追加します。
発展アイデア(次の記事へ)
- 敵の反撃やターン制(「プレイヤー → 敵」の順)を
BattleManagerに足す - 第5回: Unity 移植時の 対応関係まとめ
シリーズ全体の目次は WinFormsでRPG入門 〜Unityへ繋がる設計の考え方〜(固定ページ) を参照してください。
まとめ
- 戦闘ルールを
BattleManagerに寄せると、Form1は 入力と表示に集中できる - データは
battle.Player/battle.Enemiesのように窓口から読むと追いやすい - Unity でも 「ルール用クラス」+「画面用」 の二層にしやすい
- 次回: Unity に移植するときの対応関係まとめ(第5回・シリーズ締め)