WinFormsで「クラス分けしないバトル」アプリを作ろう

(チーム開発[リーダー+メンバー3名]+Git/Designerドラッグ&ドロップ前提)

目的

  • C# / WinForms でシンプルなバトルを作成
  • GitHub Desktop を使った順番作業で、初めてでも安全にチーム開発を体験
  • UI(Designer)変更は担当者だけが行い、*.Designer.cs の競合を回避

誰が Designer を触るか(早見表)

ステップDesigner変更実施者
STEP0lblPlayer, lblEnemy, txtLog, btnAttack を配置リーダーのみ
STEP0Mなし(Clone & Run)全メンバー
STEP1btnHeal を追加担当Aのみ
STEP2なし(コードのみ)担当B
STEP3txtLog を削除し lstLog を追加担当Cのみ

ルール:作業前 Pull → 作業 → Commit → Push。複数人が同時にDesignerを触らない。


STEP0(リーダー・必須):初期リポジトリと雛形作成

  1. Visual Studio で Windows フォーム アプリ(.NET) を新規作成
    この時点で最初のGit初期化を行なっておきます
  2. ツールボックスからドラッグ&ドロップで以下を配置し、Name を設定
    • Label:lblPlayer(プレイヤーHP)
    • Label:lblEnemy(敵HP)
    • TextBox:txtLog(Multiline=true, ReadOnly=true, ScrollBars=Vertical)
    • Button:btnAttack(攻撃)
  3. 下記の Form1.cs(STEP0版) を貼り付け
  4. GitHub へ初回 Push(以降は順番作業)

Initial commit

(リーダー) デザイナーで基本のコントロールを配置
(リーダー) Form1クラスの実装

(担当A) 攻撃ボタンの追加
(担当A) 攻撃クリックイベントを実装
(担当A) 回復ボタンの追加
(担当A) 回復処理を実装

(担当B) 敵キャラクターのクリティカル攻撃(20%)を実装

(担当C) 戦闘ログをListBoxに履歴化

(リーダー) ビジュアル調整(結果表示エリアを拡大)


コミット履歴の説明

1. Initial commit

最初のリポジトリ作成時のコミットです。

  • 空のプロジェクトファイルやソリューション構成がここで登録されます。
  • 実際の開発作業のスタート地点になります。

2. (リーダー) デザイナーで基本のコントロールを配置

リーダーの役割:共通の土台を整える

  • Visual Studio のデザイナーを使い、ボタン・ラベル・ListBoxなど、全員が共通で使うUI部品を配置。
  • この段階で「どの画面に何を置くか」が決まり、他のメンバーはここから分担作業に入れる。

3. (リーダー) Form1クラスの実装

リーダーが フォームの基本クラス を実装。

  • Form1 にイベントハンドラの枠(攻撃ボタンクリック・回復ボタンクリックなど)を用意。
  • 開発チームが迷わないように、共通のコード構造を最初に用意しておく。

4. (担当A) 攻撃ボタンの追加

担当Aが UIの一部(攻撃ボタン) を追加。

  • デザイナー上に「攻撃」ボタンを設置。
  • 見た目だけで、まだ処理は書かれていない段階。

5. (担当A) 攻撃クリックイベントを実装

攻撃ボタンに 処理を実装

  • プレイヤーが攻撃する処理を Click イベントに追加。
  • 例: 敵のHPを減らす処理。

6. (担当A) 回復ボタンの追加

同様に、回復用のボタン を配置。

  • UI要素を追加する段階。
  • ボタンが押せる見た目になる。

7. (担当A) 回復処理を実装

回復ボタンのクリックイベントに 処理を実装

  • プレイヤーのHPを回復する処理を追加。
  • 「UI追加」と「処理実装」を分けてコミットしている点が教材としてわかりやすい。

8. (担当B) 敵キャラクターのクリティカル攻撃(20%)を実装

担当Bが 敵側の特殊行動 を実装。

  • 敵の攻撃時に 20% の確率でダメージ2倍(クリティカル)になる処理を追加。
  • プレイヤーと敵の戦闘がよりゲームらしくなる。

9. (担当C) 戦闘ログをListBoxに履歴化

担当Cが 戦闘ログ機能 を追加。

  • 攻撃や回復の結果を文字列として ListBox に追加。
  • 例: 「プレイヤーが攻撃した(20ダメージ)」「敵がクリティカル攻撃!(40ダメージ)」
  • UIに履歴が残ることで、戦闘の進行が分かりやすくなる。

10. (リーダー) ビジュアル調整(結果表示エリアを拡大)

最後にリーダーが 見た目を調整

  • 戦闘結果表示部分のエリアを大きくして、ログが見やすくなるように修正。
  • 開発終盤でUIの調整をまとめて行い、全体を仕上げている。

全体の流れ

  1. リーダーが環境を整備(コントロール配置、Form1実装)
  2. 担当ごとに機能を追加(A=攻撃・回復、B=クリティカル、C=ログ)
  3. リーダーが最終調整(UI改善)

Form1.cs(STEP0・丸ごと)

GitHub Desktopの表示で文字化けを解消する作業をしておきます

using System;
using System.Windows.Forms;

namespace BattleApp
{
    public partial class Form1 : Form
    {
        private int _playerHp = 30;
        private int _enemyHp = 20;
        private readonly Random _rand = new Random();

        public Form1()
        {
            InitializeComponent();

            UpdateStatus();
            AppendLog("バトルスタート!");
        }

        private void btnAttack_Click(object sender, EventArgs e)
        {
            if (!BattleOngoing())
            {
                return;
            }

            // プレイヤー攻撃
            int damage = _rand.Next(5, 10);
            _enemyHp = Math.Max(0, _enemyHp - damage);
            AppendLog($"プレイヤーの攻撃! {damage} のダメージ!");
            UpdateStatus();

            if (_enemyHp <= 0)
            {
                AppendLog("敵を倒した! 勝利!");
                btnAttack.Enabled = false;
                return;
            }

            // 敵の反撃(HP減算→残HP表示)
            int enemyDamage = _rand.Next(3, 8);
            _playerHp = Math.Max(0, _playerHp - enemyDamage);
            AppendLog($"敵の攻撃! {enemyDamage} のダメージ!\nプレイヤーHP: {_playerHp}");
            UpdateStatus();

            if (_playerHp <= 0)
            {
                AppendLog("プレイヤーは倒れた… 敗北…");
                btnAttack.Enabled = false;
            }
        }

        private bool BattleOngoing()
        {
            return _playerHp > 0 && _enemyHp > 0;
        }

        private void UpdateStatus()
        {
            lblPlayer.Text = $"プレイヤーHP: {_playerHp}";
            lblEnemy.Text  = $"敵HP: {_enemyHp}";
        }

        private void AppendLog(string line)
        {
            txtLog.AppendText(line + Environment.NewLine);
        }
    }
}

STEP0L(リーダー・必須):メンバーをプロジェクトに参加させる

チーム内でプロジェクトを共有するため、メンバーをコラボレータとして追加します

添付リンクの、1. プロジェクトの作成とリポジトリ初期化(リーダー担当: Member A)を進めます

STEP0M(メンバー・必須):クローン&実行確認

上記の添付リンクの、2. チームメンバーによるリポジトリのクローン(Member B, Member C)を進めます

  • GitHub Desktop で Clone → Visual Studio で ビルド&実行
  • STEP0 が動くことを確認(OKなら次の担当へ)
  • 以後は必ず 作業前 Pull/作業後 Commit→Push

STEP1(担当A):回復ボタンを追加(3回まで)

  1. Designer で Button:btnHeal を追加(テキスト「かいふく」)
  2. Click イベント(btnHeal_Click)を生成
  3. 下記 Form1.cs(STEP1版) に貼り替え
  4. Commit → Push、他メンバーは Pull

Form1.cs(STEP1・丸ごと)

using System;
using System.Windows.Forms;

namespace BattleApp
{
    public partial class Form1 : Form
    {
        private int _playerHp = 30;
        private int _enemyHp = 20;
        private int _healLeft = 3;
        private readonly Random _rand = new Random();

        public Form1()
        {
            InitializeComponent();

            UpdateStatus();
            UpdateHealButtonText();
            AppendLog("バトルスタート!");
        }

        private void btnAttack_Click(object sender, EventArgs e)
        {
            if (!BattleOngoing())
            {
                return;
            }

            int damage = _rand.Next(5, 10);
            _enemyHp = Math.Max(0, _enemyHp - damage);
            AppendLog($"プレイヤーの攻撃! {damage} のダメージ!");
            UpdateStatus();

            if (_enemyHp <= 0)
            {
                AppendLog("敵を倒した! 勝利!");
                SetCommandsEnabled(false);
                return;
            }

            EnemyTurnBasic();
        }

        private void btnHeal_Click(object sender, EventArgs e)
        {
            if (!BattleOngoing() || _healLeft <= 0)
            {
                return;
            }

            int heal = _rand.Next(6, 12);
            _playerHp += heal;
            _healLeft--;
            AppendLog($"プレイヤーは {heal} かいふくした!\nプレイヤーHP: {_playerHp}");
            UpdateStatus();
            UpdateHealButtonText();

            if (BattleOngoing())
            {
                EnemyTurnBasic();
            }
        }

        private void EnemyTurnBasic()
        {
            int enemyDamage = _rand.Next(3, 8);
            _playerHp = Math.Max(0, _playerHp - enemyDamage);
            AppendLog($"敵の攻撃! {enemyDamage} のダメージ!\nプレイヤーHP: {_playerHp}");
            UpdateStatus();

            if (_playerHp <= 0)
            {
                AppendLog("プレイヤーは倒れた… 敗北…");
                SetCommandsEnabled(false);
            }
        }

        private bool BattleOngoing()
        {
            return _playerHp > 0 && _enemyHp > 0;
        }

        private void UpdateStatus()
        {
            lblPlayer.Text = $"プレイヤーHP: {_playerHp}";
            lblEnemy.Text  = $"敵HP: {_enemyHp}";
        }

        private void SetCommandsEnabled(bool enabled)
        {
            btnAttack.Enabled = enabled;
            btnHeal.Enabled   = enabled && _healLeft > 0;
        }

        private void UpdateHealButtonText()
        {
            btnHeal.Text = $"かいふく({_healLeft})";
        }

        private void AppendLog(string line)
        {
            txtLog.AppendText(line + Environment.NewLine);
        }
    }
}

STEP2(担当B):敵のクリティカル(20%)を追加

  • Designer変更はなし(コードのみ)
  • クリティカル時は 10〜15 ダメージ/通常は 3〜8 ダメージ
  • どちらも HP減算→残HPを表示 の順でログ出力

Form1.cs(STEP2・丸ごと)

using System;
using System.Windows.Forms;

namespace BattleApp
{
    public partial class Form1 : Form
    {
        private int _playerHp = 30;
        private int _enemyHp = 20;
        private int _healLeft = 3;
        private readonly Random _rand = new Random();

        public Form1()
        {
            InitializeComponent();

            UpdateStatus();
            UpdateHealButtonText();
            AppendLog("バトルスタート!");
        }

        private void btnAttack_Click(object sender, EventArgs e)
        {
            if (!BattleOngoing())
            {
                return;
            }

            int damage = _rand.Next(5, 10);
            _enemyHp = Math.Max(0, _enemyHp - damage);
            AppendLog($"プレイヤーの攻撃! {damage} のダメージ!");
            UpdateStatus();

            if (_enemyHp <= 0)
            {
                AppendLog("敵を倒した! 勝利!");
                SetCommandsEnabled(false);
                return;
            }

            EnemyTurnWithCritical();
        }

        private void btnHeal_Click(object sender, EventArgs e)
        {
            if (!BattleOngoing() || _healLeft <= 0)
            {
                return;
            }

            int heal = _rand.Next(6, 12);
            _playerHp += heal;
            _healLeft--;
            AppendLog($"プレイヤーは {heal} かいふくした!\nプレイヤーHP: {_playerHp}");
            UpdateStatus();
            UpdateHealButtonText();

            if (BattleOngoing())
            {
                EnemyTurnWithCritical();
            }
        }

        private void EnemyTurnWithCritical()
        {
            bool isCritical = (_rand.Next(0, 5) == 0); // 20%
            int enemyDamage = isCritical ? _rand.Next(10, 15) : _rand.Next(3, 8);

            _playerHp = Math.Max(0, _playerHp - enemyDamage);

            if (isCritical)
            {
                AppendLog($"敵のクリティカルヒット! {enemyDamage} の大ダメージ!\nプレイヤーHP: {_playerHp}");
            }
            else
            {
                AppendLog($"敵の攻撃! {enemyDamage} のダメージ!\nプレイヤーHP: {_playerHp}");
            }

            UpdateStatus();

            if (_playerHp <= 0)
            {
                AppendLog("プレイヤーは倒れた… 敗北…");
                SetCommandsEnabled(false);
            }
        }

        private bool BattleOngoing()
        {
            return _playerHp > 0 && _enemyHp > 0;
        }

        private void UpdateStatus()
        {
            lblPlayer.Text = $"プレイヤーHP: {_playerHp}";
            lblEnemy.Text  = $"敵HP: {_enemyHp}";
        }

        private void SetCommandsEnabled(bool enabled)
        {
            btnAttack.Enabled = enabled;
            btnHeal.Enabled   = enabled && _healLeft > 0;
        }

        private void UpdateHealButtonText()
        {
            btnHeal.Text = $"かいふく({_healLeft})";
        }

        private void AppendLog(string line)
        {
            txtLog.AppendText(line + Environment.NewLine);
        }
    }
}

STEP3(担当C):ログを履歴化(ListBoxへ)

  1. Designer で txtLog を削除し、ListBox:lstLog を追加
  2. 下記の Form1.cs(STEP3版) に貼り替え
  3. Commit → Push、他メンバーは Pull

STEP3は「ログを1行=1イベントとして扱えるUIへ差し替える」練習で、学習上とチーム開発上の狙いがあります。

  • 可読性・操作性の向上:TextBoxだと長文が流れますが、ListBoxなら「1行=1イベント」で視認・選択・コピーがしやすい(スクロールも最新行へ追従)。記事でも「txtLog を削除→ lstLog を追加」という明確な置換手順になっています。  
  • UIリファクタリングの体験:AppendLog を経由する設計にしているため、表示部品を差し替えても呼び出し側をほとんど直さずに済む=抽象化の効果を体感できます。実際にSTEP3のコードではListBox版の AppendLog に置き換えています。  
  • チーム開発でのDesigner変更の責任分担:Designerは競合しやすいので、担当CだけがUI変更を行う段取りにして、他メンバーはPullで確認のみ→.Designer.csの競合回避を学ぶ意図があります(早見表でもSTEP3は担当Cのみと明記)。  
  • 拡張の布石:将来、フィルタ(ダメージだけ表示)/重要度別表示/ログ保存などをやりやすい。ListBoxなら選択行からメタ情報にアクセスしやすく、DataBindingへの拡張も自然です。
  • 教材の一貫性:STEP0でtxtLogのプロパティはDesignerで設定する方針を明示しており(Multiline/ReadOnly/ScrollBars)、STEP3でUIコンポーネントを安全に差し替える流れに繋がります。  

いつTextBoxのままにする?

  • ログを自由編集したい、1つの大きなテキストとして保存したい、といった用途ならTextBox継続もアリです。ただ、今回の「イベントログ」としてはListBoxがハマります。

まとめると、STEP3は“UIを安全に差し替える設計と、チームでのDesigner変更の進め方”を学ぶための意図的なステップです。

Form1.cs(STEP3・丸ごと)

using System;
using System.Windows.Forms;

namespace BattleApp
{
    public partial class Form1 : Form
    {
        private int _playerHp = 30;
        private int _enemyHp = 20;
        private int _healLeft = 3;
        private readonly Random _rand = new Random();

        public Form1()
        {
            InitializeComponent();

            UpdateStatus();
            UpdateHealButtonText();
            AppendLog("バトルスタート!");
        }

        private void btnAttack_Click(object sender, EventArgs e)
        {
            if (!BattleOngoing())
            {
                return;
            }

            int damage = _rand.Next(5, 10);
            _enemyHp = Math.Max(0, _enemyHp - damage);
            AppendLog($"プレイヤーの攻撃! {damage} のダメージ!");
            UpdateStatus();

            if (_enemyHp <= 0)
            {
                AppendLog("敵を倒した! 勝利!");
                SetCommandsEnabled(false);
                return;
            }

            EnemyTurnWithCritical();
        }

        private void btnHeal_Click(object sender, EventArgs e)
        {
            if (!BattleOngoing() || _healLeft <= 0)
            {
                return;
            }

            int heal = _rand.Next(6, 12);
            _playerHp += heal;
            _healLeft--;
            AppendLog($"プレイヤーは {heal} かいふくした!\nプレイヤーHP: {_playerHp}");
            UpdateStatus();
            UpdateHealButtonText();

            if (BattleOngoing())
            {
                EnemyTurnWithCritical();
            }
        }

        private void EnemyTurnWithCritical()
        {
            bool isCritical = (_rand.Next(0, 5) == 0); // 20%
            int enemyDamage = isCritical ? _rand.Next(10, 15) : _rand.Next(3, 8);

            _playerHp = Math.Max(0, _playerHp - enemyDamage);

            if (isCritical)
            {
                AppendLog($"敵のクリティカルヒット! {enemyDamage} の大ダメージ!\nプレイヤーHP: {_playerHp}");
            }
            else
            {
                AppendLog($"敵の攻撃! {enemyDamage} のダメージ!\nプレイヤーHP: {_playerHp}");
            }

            UpdateStatus();

            if (_playerHp <= 0)
            {
                AppendLog("プレイヤーは倒れた… 敗北…");
                SetCommandsEnabled(false);
            }
        }

        private bool BattleOngoing()
        {
            return _playerHp > 0 && _enemyHp > 0;
        }

        private void UpdateStatus()
        {
            lblPlayer.Text = $"プレイヤーHP: {_playerHp}";
            lblEnemy.Text  = $"敵HP: {_enemyHp}";
        }

        private void SetCommandsEnabled(bool enabled)
        {
            btnAttack.Enabled = enabled;
            btnHeal.Enabled   = enabled && _healLeft > 0;
        }

        private void UpdateHealButtonText()
        {
            btnHeal.Text = $"かいふく({_healLeft})";
        }

        private void AppendLog(string line)
        {
            lstLog.Items.Add(line);
            lstLog.TopIndex = lstLog.Items.Count - 1; // 常に最新へスクロール
        }
    }
}

Git/進め方のコツ

  • 順番作業:リーダー → A → B → C(各自、作業前 Pull/作業後 Commit→Push)
  • コミットは小さく・わかりやすく:「回復追加」「クリティカル実装」など
  • Designerの競合を回避:UI変更は担当者だけが行い、他メンバーはPullして確認に徹する
  • 競合が出たら:どちらの変更を残すか相談。必要ならDesignerで再配置し直す方が早い場合も

訪問数 1 回, 今日の訪問数 1回