学習記事一覧 · WinFormsRPG

【学習】WinFormsで作るRPG入門(3)コマンド選択 〜if で分岐〜

第2回 までで、複数の敵 に順番に攻撃し、全滅で勝利する流れが作れました。

RPG では 「いま何をするか」 を選ばせる場面がほぼ必ず出てきます。今回は 攻撃回復 を選べるようにし、if で処理を分岐する練習をします。


今回の最小ゴール

  • ラジオボタンで「攻撃」「回復」のどちらかを選ぶ
  • 決定ボタン1つで、選んだ内容に応じて処理が変わる
    • 攻撃: 第2回と同じ(ターゲットの HP を減らす・倒したら次へ)
    • 回復: 自分の HP を一定量回復する(上限 MaxHp を超えない
  • 勝利後は第2回と同様、決定ボタンを無効にする

前回からの差分

第2回 第3回
btnAttack = 常に攻撃 btnAction(決定) + ラジオでコマンド選択
Player は攻撃だけ MaxHpHeal(回復)を追加
Click に攻撃処理が直書き if / else if で「攻撃用処理」「回復用処理」に分ける

新しく出てくるキーワード: RadioButton.CheckedGroupBox(任意)、分岐MaxHp(上限付きの状態)


UI の考え方(ラジオボタン)

1つだけ選ばせたいときは ラジオボタンを使ってみよう が向いています。同じグループ内では、どれか1つだけ Checked になります。

コントロール 名前(例) 用途
GroupBox grpCommand 「コマンド」枠(なくても動くが、見やすい)
RadioButton rdoAttack 攻撃を選ぶ
RadioButton rdoHeal 回復を選ぶ
Button btnAction 決定(第2回の btnAttack の役割を兼ねる)
(第2回までの Label 類) そのまま HP 表示・敵一覧など

デザイナで rdoAttackChecked を true にしておくと、起動直後は「攻撃」が選ばれた状態になります。


Player クラス(MaxHpHeal を追加)

Enemy は第2回までと 同じ で構いません。Player だけ拡張します。

public class Player
{
    public string Name { get; set; }
    public int Hp { get; set; }
    public int MaxHp { get; set; }
    public int Attack { get; set; }

    public Player(string name, int hp, int attack)
    {
        Name = name;
        Hp = hp;
        MaxHp = hp;
        Attack = attack;
    }

    public void AttackEnemy(Enemy enemy)
    {
        enemy.Hp -= Attack;
    }

    // HP を増やす。MaxHp を超えない。
    public void Heal(int amount)
    {
        Hp += amount;
        if (Hp > MaxHp)
            Hp = MaxHp;
    }
}
  • MaxHp: 戦闘開始時の HP を上限として覚えておく例です(ポーションやレベルアップで上限が変わるゲームでは、別の持ち方にもできます)。
  • Heal: 「回復はプレイヤー自身の責務」とし、上限チェックをクラス内に閉じると、フォーム側の if がすっきりします。

Form1 のフィールド(第2回+α)

using System.Collections.Generic;
using System.Text;

// ...

Player player;
List<Enemy> enemies;
int targetIndex;

UI 上のラジオ・ボタンはデザイナがフィールドを生成するので、通常は 追加のフィールドは不要です(rdoAttack などは Form1.Designer.cs にあります)。


初期化(Form1_Load)

第2回と同じく enemies を用意し、PlayerMaxHp がコンストラクタでセットされる形にします。

private void Form1_Load(object sender, EventArgs e)
{
    player = new Player("勇者", 100, 20);

    enemies = new List<Enemy>
    {
        new Enemy("スライムA", 30),
        new Enemy("スライムB", 40),
        new Enemy("ゴブリン", 60),
    };

    targetIndex = 0;
    EnsureValidTarget();
    UpdateUI();
}

ターゲット調整(第2回と同じ)

private void EnsureValidTarget()
{
    while (targetIndex < enemies.Count && enemies[targetIndex].Hp <= 0)
    {
        targetIndex++;
    }
}

攻撃だけを切り出す(DoAttack

第2回の btnAttack_Click の中身を DoAttack メソッドにまとめます。

private void DoAttack()
{
    EnsureValidTarget();

    if (targetIndex >= enemies.Count)
        return;

    Enemy target = enemies[targetIndex];
    player.AttackEnemy(target);

    if (target.Hp < 0)
        target.Hp = 0;

    EnsureValidTarget();
}

回復(DoHeal

回復量は定数にしておくと、あとから数字を変えやすいです。

private const int HealAmount = 15;

private void DoHeal()
{
    player.Heal(HealAmount);
}

決定ボタン:if でコマンド分岐

ここが第3回の中心です。Checkedtrue のラジオに応じて、呼ぶ処理を変えます。

private void btnAction_Click(object sender, EventArgs e)
{
    if (rdoAttack.Checked)
    {
        DoAttack();
    }
    else if (rdoHeal.Checked)
    {
        DoHeal();
    }

    UpdateUI();
}

発展: コマンドが増えたら else if が続きます。さらに増える場合は switch や「コマンド番号」の列挙型に分ける手もあります(第4回以降の整理の題材になります)。


UI 更新(第2回を btnAction に合わせる)

btnAttack をやめたので、無効化するボタン名を btnAction にします。

private void UpdateUI()
{
    lblPlayerHp.Text = $"HP: {player.Hp} / {player.MaxHp}";

    var sb = new StringBuilder();
    foreach (var enemy in enemies)
    {
        sb.AppendLine($"{enemy.Name}: {enemy.Hp}");
    }
    lblEnemyList.Text = sb.ToString();

    if (targetIndex >= enemies.Count)
    {
        lblTarget.Text = "ターゲット: なし(勝利!)";
        lblEnemyHp.Text = "-";
        btnAction.Enabled = false;
        return;
    }

    Enemy current = enemies[targetIndex];
    lblTarget.Text = $"ターゲット: {current.Name}";
    lblEnemyHp.Text = $"HP: {current.Hp}";
    btnAction.Enabled = true;
}

プレイヤー HP 表示に / MaxHp を足すと、回復の効果が分かりやすくなります。


プログラムの流れ

btnAction_Click
  → rdoAttack がオン?
       はい → DoAttack(EnsureValidTarget → 攻撃 → 再調整)
  → そうでなければ rdoHeal がオン?
       はい → DoHeal(Player.Heal)
  → UpdateUI(勝利なら btnAction を無効)

Unity での当たり所

WinForms では ラジオ+1ボタンで「選んだ行動」を決めています。

Unity では次のような対応がよくあります。

WinForms(今回) Unity(イメージ)
rdoAttack.Checked UI の Toggle / ボタン群、またはメニューインデックス
btnAction_Click 内の if 同じ ifUpdate や UI イベントから呼ぶ
DoAttack / DoHeal メソッド名はそのままにして、入力だけ差し替え

分岐の中身(HP の増減・リストの更新)は、エンジンが変わっても型を合わせやすいです。Unity では「コマンド入力 → 1ターン進める」にすると、ターン制 RPG の核に近づきます。


よくあるつまずき

「ラジオを変えても攻撃しかしない」

  • Checked を確認している 名前rdoAttack / rdoHeal)が、デザイナの Name と一致しているか確認します。
  • 別の GroupBox に別グループのラジオがあると、両方オンに見える配置になることがあります。今回は 1つのグループだけにします。

「回復で HP が上限を超える」

  • 上限処理は Player.Heal の中に書くと、フォームを何枚も増やしたときに安全です(1か所にルールを集約)。

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

  • 防御コマンド(1ターンだけダメージ半減)… フラグが増えるので「状態」をどこに持つかが課題
  • BattleManager クラスDoAttack / DoHeal を移し、フォームは UI だけにする → 第4回

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


まとめ

  • コマンド選択は UI(ラジオなど)で「状態」を決め、if で処理を振り分ける形が分かりやすい
  • 回復のように ルールがある処理Player のメソッドに寄せると、フォームが読みやすくなる
  • Unity でも 同じ分岐構造を、入力と表示だけ差し替えて使える
  • 次回: クラス分割(BattleManager など)