ドラクエ風WinFormsをクラス分けで整理する(Lv5 → 「各キャラHP横にイラスト表示」をOOPで拡張)

狙い:

HPなどの数値はモデル(Character)で一元管理し、表示は再利用可能なUserControl(CharacterPanel)に委譲します。フォームは「モデルとUIのひも付け」に専念。

これでキャラ追加・差し替え・見た目変更が最小改修で済む構成になります。

ドラクエ風WinFormsをクラス分けで整理する(4人開発・完成チュートリアル)のチュートリアルを終えたことを前提としています


1) ドメインモデルを導入

Character.cs

using System;
using System.Drawing;

namespace RpgSample
{
    public sealed class Character
    {
        public string Name { get; }
        public int MaxHp { get; }
        private int _hp;
        public int Hp
        {
            get => _hp;
            private set
            {
                int clamped = Math.Max(0, Math.Min(MaxHp, value));
                if (clamped != _hp)
                {
                    _hp = clamped;
                    StateChanged?.Invoke(this, EventArgs.Empty);
                }
            }
        }

        // 表示用:キャラの静止画(Resources から渡す想定)
        public Image Portrait { get; }

        public event EventHandler? StateChanged;

        public Character(string name, int maxHp, Image portrait)
        {
            Name = name;
            MaxHp = maxHp;
            _hp = maxHp;
            Portrait = portrait;
        }

        public void Heal(int amount) => Hp += Math.Max(0, amount);
        public void Damage(int amount) => Hp -= Math.Max(0, amount);
        public bool IsDead => Hp <= 0;
    }
}

2) 既存の GameController を「キャラ駆動」に差し替え

GameController.cs(抜粋:プレイヤー/敵を Character 化)

using System;

namespace RpgSample
{
    public sealed class GameController
    {
        public Character Player { get; }
        public Character Enemy  { get; }

        public event Action? StateChanged;
        public Action<string>? Log; // 既存の簡易ロガーを活かす

        public GameController(Character player, Character enemy)
        {
            Player = player;
            Enemy  = enemy;

            // どちらかの状態が変わったらコントローラの StateChanged も発火
            Player.StateChanged += (_, __) => StateChanged?.Invoke();
            Enemy.StateChanged  += (_, __) => StateChanged?.Invoke();
        }

        public void PlayerAttack()
        {
            if (Player.IsDead || Enemy.IsDead) return;
            Enemy.Damage(8);
            Log?.Invoke("プレイヤーの攻撃! 8 ダメージ");
            AfterPlayerAction();
        }

        public void PlayerHeal()
        {
            if (Player.IsDead || Enemy.IsDead) return;
            int before = Player.Hp;
            Player.Heal(8);
            Log?.Invoke($"プレイヤーは {Player.Hp - before} 回復した");
            AfterPlayerAction();
        }

        private void AfterPlayerAction()
        {
            if (!Player.IsDead && !Enemy.IsDead)
            {
                EnemyTurn();
            }
            StateChanged?.Invoke();
        }

        private void EnemyTurn()
        {
            Player.Damage(6);
            Log?.Invoke("敵の攻撃! 6 ダメージ");
        }
    }
}

3) UI を再利用可能な UserControl に分離

CharacterPanel.cs(UserControl)

  • 責務:画像(PictureBox)、名前、HP/最大HP、HPバーをまとめて描画。
  • バインド:Bind(Character c) でモデルと接続。Character.StateChanged を購読してUI更新。
using System;
using System.Windows.Forms;

namespace RpgSample
{
    public partial class CharacterPanel : UserControl
    {
        private Character? _character;

        public CharacterPanel()
        {
            InitializeComponent();
            // Designerで下記コントロールを配置しておく:
            // pictureBoxPortrait (SizeMode = Zoom)
            // labelName
            // labelHp
            // progressBarHp (Minimum = 0, Maximum = 100)
        }

        public void Bind(Character character)
        {
            if (_character != null)
            {
                _character.StateChanged -= OnCharacterStateChanged;
            }
            _character = character;
            _character.StateChanged += OnCharacterStateChanged;

            // 初回反映
            pictureBoxPortrait.Image = _character.Portrait;
            labelName.Text = _character.Name;
            RefreshView();
        }

        private void OnCharacterStateChanged(object? sender, EventArgs e) => RefreshView();

        private void RefreshView()
        {
            if (_character == null) return;

            labelHp.Text = $"{_character.Hp} / {_character.MaxHp}";
            progressBarHp.Value = _character.MaxHp == 0
                ? 0
                : Math.Min(progressBarHp.Maximum,
                           (int)Math.Round(100.0 * _character.Hp / _character.MaxHp));
        }
    }
}

レイアウト目安
左:PictureBox(128×128 など)/ 右:Nameラベル、HPラベル、ProgressBar。
既存の「HP表示のラベル」の位置にこの CharacterPanel を置き換えるイメージ。


4) フォームで「バインド」するだけにする

Form1.cs(抜粋:初期化とバインド)

using System;
using System.Windows.Forms;

namespace RpgSample
{
    public partial class Form1 : Form
    {
        private GameController _gc;

        public Form1()
        {
            InitializeComponent();

            // --- 画像の準備 ---
            // 画像は Properties.Resources に追加しておく(後述)
            var player = new Character("勇者", 50, Properties.Resources.PlayerPortrait);
            var slime  = new Character("スライム", 35, Properties.Resources.SlimePortrait);

            // --- Controller 構築 ---
            _gc = new GameController(player, slime);
            _gc.Log = msg => txtLog.AppendText(msg + Environment.NewLine); // 既存ログTextBoxを想定
            _gc.StateChanged += () => RefreshAll();

            // --- パネルにバインド ---
            playerPanel.Bind(_gc.Player); // CharacterPanel(プレイヤー用)
            enemyPanel.Bind(_gc.Enemy);   // CharacterPanel(敵用)

            RefreshAll();
        }

        private void RefreshAll()
        {
            // 必要に応じて他のUIも更新。
            // CharacterPanel 側はイベントで自動更新されるのでここでは最小限。
            if (_gc.Player.IsDead) txtLog.AppendText("プレイヤーは倒れた…\r\n");
            if (_gc.Enemy.IsDead)  txtLog.AppendText("敵を倒した!\r\n");
        }

        private void btnAttack_Click(object sender, EventArgs e) => _gc.PlayerAttack();
        private void btnHeal_Click(object sender, EventArgs e)   => _gc.PlayerHeal();
    }
}

5) 画像(イラスト)の取り込み方法

  • 推奨:プロジェクト > プロパティ > リソース > 画像 に PNG/JPG を追加例)PlayerPortrait.png, SlimePortrait.png→ コードから Properties.Resources.PlayerPortrait のように参照できます。
  • 代替:外部ファイル(assets/xxx.png)を実行時に Image.FromFile(path) で読み込む
    • メリット:差し替え容易
    • デメリット:配布・相対パス管理が増える
  • メモリ管理:Character.Portrait は共有参照を推奨(都度 new Bitmap しない)

6) デザイナでの配置(最小手順)

  1. CharacterPanel を追加
    • プロジェクトに UserControl(Windows フォーム) を新規追加 → CharacterPanel
    • デザイナで PictureBox、Label×2、ProgressBar を配置
      • PictureBox.SizeMode = Zoom
      • ProgressBar.Minimum = 0, Maximum = 100
  2. メインフォームに CharacterPanel を2つ置く(playerPanel, enemyPanel という Name)
  3. 既存の「HPラベル」は CharacterPanel が代替するため非表示または削除

7) 拡張しやすさ(OOP上の余白)

  • 敵AIの差し替えIEnemyBehavior(DecideAction(GameController gc))を導入すると、スライム/ドラゴンなどで行動を切替可能。
  • 状態異常・バフIStatusEffect を Character に付与して OnTurnStart/End を回す設計に拡張。
  • 立ち絵の切替Character に Image PortraitNormal/PortraitDamaged/PortraitDead を持たせ、Hp割合 で切替表示。

8) 参考:Designerコード例(CharacterPanel.Designer.cs 概略)

private System.Windows.Forms.PictureBox pictureBoxPortrait;
private System.Windows.Forms.Label labelName;
private System.Windows.Forms.Label labelHp;
private System.Windows.Forms.ProgressBar progressBarHp;

private void InitializeComponent()
{
    this.pictureBoxPortrait = new System.Windows.Forms.PictureBox();
    this.labelName = new System.Windows.Forms.Label();
    this.labelHp = new System.Windows.Forms.Label();
    this.progressBarHp = new System.Windows.Forms.ProgressBar();
    ((System.ComponentModel.ISupportInitialize)(this.pictureBoxPortrait)).BeginInit();
    this.SuspendLayout();
    // pictureBoxPortrait
    this.pictureBoxPortrait.Location = new System.Drawing.Point(8, 8);
    this.pictureBoxPortrait.Size = new System.Drawing.Size(128, 128);
    this.pictureBoxPortrait.SizeMode = System.Windows.Forms.PictureBoxSizeMode.Zoom;
    // labelName
    this.labelName.AutoSize = true;
    this.labelName.Location = new System.Drawing.Point(148, 8);
    this.labelName.Text = "Name";
    // labelHp
    this.labelHp.AutoSize = true;
    this.labelHp.Location = new System.Drawing.Point(148, 36);
    this.labelHp.Text = "HP";
    // progressBarHp
    this.progressBarHp.Location = new System.Drawing.Point(148, 64);
    this.progressBarHp.Size = new System.Drawing.Size(180, 18);
    this.progressBarHp.Minimum = 0;
    this.progressBarHp.Maximum = 100;

    // CharacterPanel
    this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
    this.Controls.Add(this.pictureBoxPortrait);
    this.Controls.Add(this.labelName);
    this.Controls.Add(this.labelHp);
    this.Controls.Add(this.progressBarHp);
    this.Size = new System.Drawing.Size(340, 148);
    ((System.ComponentModel.ISupportInitialize)(this.pictureBoxPortrait)).EndInit();
    this.ResumeLayout(false);
    this.PerformLayout();
}

9) 置き換えの最小差分まとめ

  • 追加:Character.cs、CharacterPanel.cs(UserControl)
  • 変更:GameController を int Hp 直持ち → Character 参照へ
  • フォーム:playerPanel.Bind(gc.Player)/enemyPanel.Bind(gc.Enemy) を呼ぶ
  • 画像:リソース(Properties.Resources)に追加して参照

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