ドラクエ風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) デザイナでの配置(最小手順)
- CharacterPanel を追加
- プロジェクトに UserControl(Windows フォーム) を新規追加 → CharacterPanel
- デザイナで PictureBox、Label×2、ProgressBar を配置
- PictureBox.SizeMode = Zoom
- ProgressBar.Minimum = 0, Maximum = 100
- メインフォームに CharacterPanel を2つ置く(playerPanel, enemyPanel という Name)
- 既存の「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回
ディスカッション
コメント一覧
まだ、コメントがありません