学習記事一覧 · WinFormsRPG

【学習】WinFormsで作るRPG入門(4)クラス分割 〜BattleManager にまとめる〜

第3回 までで、Form1戦闘のルール(ターゲットの更新・攻撃・回復)が集まってきました。

コードが長くなるほど、「画面のコード」と「ゲームのルール」が混ざると読みにくくなります。今回は 戦闘まわりだけを BattleManager クラスに移し、フォームは 表示と入力に専念する形に整理します。


今回の最小ゴール

  • BattleManagerPlayerList<Enemy>targetIndex・攻撃/回復の処理を持つ
  • Form1BattleManager を1つ持ち、btnAction_Click では「コマンドを渡す → UpdateUI」が中心になる
  • 見た目の動きは第3回と同じ(挙動を変えず、置き場所だけ分ける)

前回からの差分

第3回 第4回
DoAttack / DoHeal / EnsureValidTargetForm1 のメソッド 上記は 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 に敵を追加したい」

  • いまは 読み取り専用として渡している前提です。追加したくなったら、BattleManagerAddEnemy のようなメソッドを用意するか、コンストラクタの設計を変える、と決めると安全です(ルールをクラス越しにそろえる)。

「フォームから _targetIndex を直接いじりたくなった」

  • ターゲットの意味BattleManager の責務にまとめた方がバグが減ります。どうしても必要なら、マネージャに NextTarget() など 名前付きの操作を追加します。

発展アイデア(次の記事へ)

  • 敵の反撃やターン制(「プレイヤー → 敵」の順)を BattleManager に足す
  • 第5回: Unity 移植時の 対応関係まとめ

シリーズ全体の目次は WinFormsでRPG入門 〜Unityへ繋がる設計の考え方〜(固定ページ) を参照してください。


まとめ

  • 戦闘ルールBattleManager に寄せると、Form1入力と表示に集中できる
  • データは battle.Player / battle.Enemies のように窓口から読むと追いやすい
  • Unity でも 「ルール用クラス」+「画面用」 の二層にしやすい
  • 次回: Unity に移植するときの対応関係まとめ(第5回・シリーズ締め)