WinForms ブロック崩しMVP チュートリアル

~シンプルな実装から MVP パターンへリファクタリングして学ぶ~

このチュートリアルでは、C# の WinForms を使用して、ブロック崩しゲームの基本機能を MVP パターンで実装する方法を解説します。
以下の機能を実装しています。

  • パドル操作: 左右キーによるパドルの移動
  • ボールの動きと反射: 壁、パドル、ブロックとの衝突判定
  • ブロック配置: 画面上部に複数のブロックを配置
  • スコア管理: ブロック破壊時にスコアを加算
  • ゲームオーバー処理: ボールが画面下端に到達した場合、ゲーム終了
  • MVP パターン: View(フォーム)と Presenter(ゲームロジック)の分離による保守性向上

1. MVP パターンの基本構成

このサンプルでは、明確な「Model」クラスは定義されていません。
通常、MVPパターンではModelはデータやビジネスロジックを担当しますが、この例ではGamePresenterがパドルやボールの位置、スコア、ブロックの状態などのゲームデータやロジックを管理しており、結果としてPresenterがModelの役割も兼ねています。

どちらのアプローチにもメリットがあります。

  • Modelを分離する場合
    • 各コンポーネント(Model, View, Presenter)の責務が明確になり、保守性やテストがしやすくなります。
    • アプリケーションが大規模になったり、ビジネスロジックが複雑になる場合、Modelを独立させておくと、ロジック部分の変更がUIに影響しにくくなります。
  • Presenterに統合する場合
    • シンプルなサンプルや小規模なプロジェクトでは、コード量を抑え、全体の理解がしやすくなるためメリットがあります。

したがって、今後機能拡張や複雑なロジックが必要になると予想される場合は、Modelを分離しておくほうが望ましいです。逆に、サンプルやプロトタイプ程度であれば、Presenter内に統合するシンプルな形でも問題ありません。

  • IGameView インターフェイス
    View(フォーム)が実装すべきイベントやメソッドを定義します。
  • GamePresenter クラス
    ゲームの状態(パドル、ボール、ブロック、スコアなど)を管理し、更新処理や衝突判定を担当します。
    また、View からの入力イベント(キー入力)を受け取り、状態を更新します。
  • Form1 クラス (View の実装)
    WinForms のフォームとして、IGameView を実装します。
    キー入力イベントを Presenter に伝播し、タイマーで定期的に Presenter の更新処理を呼び出して画面を再描画します。
  • Program クラス
    アプリケーションのエントリーポイントです。

2. サンプルコード

以下のコードは、Visual Studio の Windows Forms プロジェクトに追加してそのまま実行できる MVP パターンのサンプルです。
コード内に各処理の詳細なコメントを付与しているので、各部分の役割や流れを確認してください。

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;

// --- 1. View インターフェイス ---
// View が実装すべき基本的なイベントやメソッドを定義
public interface IGameView
{
    // キー入力イベント
    event EventHandler<KeyEventArgs> KeyDownEvent;
    event EventHandler<KeyEventArgs> KeyUpEvent;

    // 画面再描画の要求(Presenter から呼び出される)
    void RefreshView();

    // フォームのサイズ(Presenter 側でレイアウト計算に使用)
    Size ClientSize { get; }
}

// --- 2. Presenter ---
// ゲームのロジック、状態管理、入力処理、衝突判定、スコア管理を担当
public class GamePresenter
{
    private readonly IGameView view;

    // パドルの状態
    private float paddleX;
    private const float PaddleWidthConst = 100.0f;  // パドルの幅
    private const float PaddleSpeedConst = 5.0f;      // パドルの移動速度

    // ボールの状態
    private float ballX;
    private float ballY;
    private float ballVelocityX = 3.0f;
    private float ballVelocityY = -3.0f;
    private const float BallRadiusConst = 10.0f;      // ボールの半径

    // ブロックの状態
    private List<RectangleF> blocks;
    private const int BlockRows = 5;                // ブロックの行数
    private const int BlockColumns = 10;            // ブロックの列数
    private const float BlockWidth = 70;            // ブロックの幅
    private const float BlockHeight = 20;           // ブロックの高さ
    private const float BlockPadding = 5;           // ブロック間の余白
    private const float BlockTopOffset = 30;        // 上部からのオフセット

    // キー入力状態の管理
    private bool isLeftPressed = false;
    private bool isRightPressed = false;

    // スコアとゲームオーバー状態
    private int score = 0;
    private bool isGameOver = false;

    // コンストラクタ
    public GamePresenter(IGameView view)
    {
        this.view = view;
        // ゲーム初期化
        InitializeGame();
        // View からのキー入力イベントを購読
        view.KeyDownEvent += OnKeyDown;
        view.KeyUpEvent += OnKeyUp;
    }

    // ゲーム初期化:パドル、ボール、ブロックの初期位置を設定
    private void InitializeGame()
    {
        // パドルを画面下部中央に配置
        paddleX = (view.ClientSize.Width - PaddleWidthConst) / 2;
        // ボールを画面中央に配置
        ballX = view.ClientSize.Width / 2;
        ballY = view.ClientSize.Height / 2;
        // ブロックを初期配置
        InitializeBlocks();
    }

    // ブロックの初期配置を行う
    private void InitializeBlocks()
    {
        blocks = new List<RectangleF>();
        // 全ブロックの横幅を計算し、中央配置するための開始位置を求める
        float totalWidth = BlockColumns * BlockWidth + (BlockColumns - 1) * BlockPadding;
        float startX = (view.ClientSize.Width - totalWidth) / 2;
        for (int row = 0; row < BlockRows; row++)
        {
            for (int col = 0; col < BlockColumns; col++)
            {
                float x = startX + col * (BlockWidth + BlockPadding);
                float y = BlockTopOffset + row * (BlockHeight + BlockPadding);
                blocks.Add(new RectangleF(x, y, BlockWidth, BlockHeight));
            }
        }
    }

    // --- キー入力処理 ---
    // キーが押されたときの処理
    private void OnKeyDown(object sender, KeyEventArgs e)
    {
        if (e.KeyCode == Keys.Left)
            isLeftPressed = true;
        else if (e.KeyCode == Keys.Right)
            isRightPressed = true;
    }
    // キーが離されたときの処理
    private void OnKeyUp(object sender, KeyEventArgs e)
    {
        if (e.KeyCode == Keys.Left)
            isLeftPressed = false;
        else if (e.KeyCode == Keys.Right)
            isRightPressed = false;
    }

    // --- ゲーム状態更新 ---
    // タイマーから呼び出され、ゲームの状態を更新する
    public void UpdateGame()
    {
        if (isGameOver) return;

        // パドル移動:左右キーの入力に応じて位置を更新
        if (isLeftPressed)
            paddleX -= PaddleSpeedConst;
        if (isRightPressed)
            paddleX += PaddleSpeedConst;
        // 画面外に出ないよう制限
        paddleX = Math.Max(0, Math.Min(paddleX, view.ClientSize.Width - PaddleWidthConst));

        // ボールの位置を更新
        ballX += ballVelocityX;
        ballY += ballVelocityY;

        // 壁との衝突判定
        if (ballX - BallRadiusConst < 0)
        {
            ballX = BallRadiusConst;
            ballVelocityX = -ballVelocityX;
        }
        else if (ballX + BallRadiusConst > view.ClientSize.Width)
        {
            ballX = view.ClientSize.Width - BallRadiusConst;
            ballVelocityX = -ballVelocityX;
        }
        if (ballY - BallRadiusConst < 0)
        {
            ballY = BallRadiusConst;
            ballVelocityY = -ballVelocityY;
        }
        else if (ballY - BallRadiusConst > view.ClientSize.Height)
        {
            // ボールが画面下端に到達 → ゲームオーバー
            isGameOver = true;
            return;
        }

        // 衝突判定用のボールの矩形を作成
        RectangleF ballRect = new RectangleF(ballX - BallRadiusConst, ballY - BallRadiusConst, BallRadiusConst * 2, BallRadiusConst * 2);

        // パドルとの衝突判定
        // ※ここではパドルの位置は画面下から50ピクセル上に固定
        RectangleF paddleRect = new RectangleF(paddleX, view.ClientSize.Height - 50, PaddleWidthConst, 20);
        if (paddleRect.IntersectsWith(ballRect) && ballVelocityY > 0)
        {
            // 衝突した場合、ボールの位置をパドルの上に固定し、Y 方向の速度を反転
            ballY = paddleRect.Top - BallRadiusConst;
            ballVelocityY = -Math.Abs(ballVelocityY);
            // パドル上の衝突位置に応じて X 方向の速度を調整
            float hitPos = (ballX - paddleRect.Left) / PaddleWidthConst;
            ballVelocityX = (hitPos - 0.5f) * 10;
        }

        // ブロックとの衝突判定
        for (int i = 0; i < blocks.Count; i++)
        {
            if (blocks[i].IntersectsWith(ballRect))
            {
                // 衝突したブロックを削除し、ボールの Y 方向の速度を反転
                blocks.RemoveAt(i);
                ballVelocityY = -ballVelocityY;
                // スコアを加算(例:1ブロックにつき 10 点)
                score += 10;
                // 複数のブロックとの衝突を避けるためループを抜ける
                break;
            }
        }
    }

    // --- Presenter から View に公開するプロパティ ---
    public float PaddleX => paddleX;
    public float PaddleWidth => PaddleWidthConst;
    public float BallX => ballX;
    public float BallY => ballY;
    public float BallRadius => BallRadiusConst;
    public int Score => score;
    public bool IsGameOver => isGameOver;
    public IReadOnlyList<RectangleF> Blocks => blocks.AsReadOnly();
}

// --- 3. View 実装 (WinForms フォーム) ---
// IGameView インターフェイスを実装し、ユーザー入力と描画を担当
public partial class Form1 : Form, IGameView
{
    // IGameView のイベント実装
    public event EventHandler<KeyEventArgs> KeyDownEvent;
    public event EventHandler<KeyEventArgs> KeyUpEvent;

    private GamePresenter presenter;
    private Timer gameTimer;

    public Form1()
    {
        InitializeComponent();
        // ダブルバッファリング有効化で描画のちらつきを防止
        this.DoubleBuffered = true;
        this.ClientSize = new Size(800, 600);
        this.Text = "Breakout MVP Example";

        // フォームのキー入力イベントを発行し、Presenter に伝播
        this.KeyDown += (s, e) => KeyDownEvent?.Invoke(s, e);
        this.KeyUp += (s, e) => KeyUpEvent?.Invoke(s, e);

        // Presenter の生成(View を渡す)
        presenter = new GamePresenter(this);

        // タイマーを設定して約60FPS(16ms間隔)でゲーム状態を更新
        gameTimer = new Timer { Interval = 16 };
        gameTimer.Tick += GameTimer_Tick;
        gameTimer.Start();
    }

    // タイマーの Tick イベントで Presenter の更新処理を呼び出す
    private void GameTimer_Tick(object sender, EventArgs e)
    {
        presenter.UpdateGame();
        this.Invalidate(); // OnPaint を呼び出すための再描画要求
    }

    // IGameView インターフェイスの RefreshView 実装
    public void RefreshView()
    {
        Invalidate();
    }

    // 描画処理:Presenter から状態情報を取得して各オブジェクトを描画する
    protected override void OnPaint(PaintEventArgs e)
    {
        base.OnPaint(e);
        Graphics g = e.Graphics;
        // 背景を白でクリア
        g.Clear(Color.White);

        // パドル描画(画面下から50px上部に固定)
        g.FillRectangle(Brushes.Blue, presenter.PaddleX, this.ClientSize.Height - 50, presenter.PaddleWidth, 20);

        // ボール描画
        g.FillEllipse(Brushes.Red,
            presenter.BallX - presenter.BallRadius,
            presenter.BallY - presenter.BallRadius,
            presenter.BallRadius * 2,
            presenter.BallRadius * 2);

        // ブロック描画:各ブロックの矩形を描画
        foreach (var block in presenter.Blocks)
        {
            g.FillRectangle(Brushes.Green, block);
            g.DrawRectangle(Pens.Black, block.X, block.Y, block.Width, block.Height);
        }

        // スコア描画(画面左上に表示)
        using (Font font = new Font("Arial", 14))
        {
            g.DrawString("Score: " + presenter.Score, font, Brushes.Black, 10, 10);
        }

        // ゲームオーバー時の表示
        if (presenter.IsGameOver)
        {
            string gameOverText = "Game Over";
            using (Font font = new Font("Arial", 24))
            {
                SizeF textSize = g.MeasureString(gameOverText, font);
                g.DrawString(gameOverText, font, Brushes.Red,
                    (this.ClientSize.Width - textSize.Width) / 2,
                    (this.ClientSize.Height - textSize.Height) / 2);
            }
        }
    }
}

// --- 4. アプリケーション エントリーポイント ---
public static class Program
{
    [STAThread]
    public static void Main()
    {
        // Windows Forms アプリケーションの初期化
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        // Form1 を起動
        Application.Run(new Form1());
    }
}

3. チュートリアルのまとめ

  1. MVP パターンの概要
    • View(Form1)はユーザーインターフェイスの描画や入力受け取りを担当し、IGameView インターフェイスを実装します。
    • Presenter(GamePresenter)はゲームのロジック(パドル、ボール、ブロックの状態更新、衝突判定、スコア管理、ゲームオーバー処理など)を管理し、View からの入力に応じた処理を行います。
    • この分離により、コードの保守性やテスト容易性が向上します。
  2. 実装の流れ
    • まず、GamePresenter 内で初期状態(パドル、ボール、ブロックの配置)を設定し、View のキー入力イベントにより状態を更新します。
    • タイマーにより約60FPSで UpdateGame() を呼び出し、ゲーム状態を更新してから View に再描画を依頼します。
    • OnPaint 内で Presenter から取得した状態を元に、パドル、ボール、ブロック、スコア、ゲームオーバーのメッセージを描画します。
  3. 学習のポイント
    • 最初はシンプルなコードビハインド実装から始め、その動作を確認した後に MVP パターンにリファクタリングすることで、アーキテクチャ設計の考え方を学べます。
    • 本サンプルは基本的な枠組みとなるため、さらに Model クラスの分離や複雑なロジックの実装、ユニットテストの導入など、プロジェクトの拡張に応じた改善が可能です。

このサンプルコードとチュートリアルを参考に、MVP パターンを用いたブロック崩しゲームの実装方法を学び、自分なりにアレンジや機能拡張に挑戦してみてください。

アンチエイリアス設定の追加方法

現状のコードにはアンチエイリアスの設定は含まれていません。アンチエイリアスを有効にすることで、描画がより滑らかになり、パドルやボール、ブロックのエッジがきれいに表示されます。以下は、OnPaint メソッド内で Graphics.SmoothingMode を設定する例です。

protected override void OnPaint(PaintEventArgs e)
{
    base.OnPaint(e);
    Graphics g = e.Graphics;

    // アンチエイリアスを有効化
    g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;

    // 背景を白でクリア
    g.Clear(Color.White);

    // ここにパドル、ボール、ブロックなどの描画処理を記述
}

このように設定することで、描画の品質が向上し、エッジが滑らかになります。

UML図

1. クラス図

このクラス図は、MVP アーキテクチャに基づく主要なクラスやインターフェイスの関係を示しています。

  • IGameView
    ゲーム画面の基本インターフェイス。
  • GamePresenter
    ゲームロジック、状態管理、衝突判定などを担当。
  • Form1
    Windows Forms の実装で、IGameView を実装し、ユーザー入力や描画を担う。
  • Program
    アプリケーションのエントリーポイント。

2. シーケンス図:キー入力処理

このシーケンス図は、ユーザーがキーを押したときに Form1 のイベントハンドラから GamePresenter の処理へ伝播する流れを示しています。

3. シーケンス図:ゲームループ(タイマー処理)

このシーケンス図は、タイマーの Tick イベントからゲームの状態更新および描画処理へ流れる処理の流れを示しています。

4. アクティビティ図:ゲーム更新サイクル

このアクティビティ図は、タイマーによるゲーム更新の流れを示しています。
タイマーが Tick するごとに、GamePresenter の UpdateGame() が呼ばれ、各種衝突判定や状態更新が行われ、その後再描画要求が発行されます。

参考(Modelクラスも分割実装したサンプル)

Modelを分離するケースでは、ゲームの状態(パドル、ボール、ブロック、スコア、ゲームオーバーの状態など)の管理と、ロジックの実装を専用のModelクラスに移し、PresenterはViewとの連携やModelの操作に専念する形になります。これにより、以下のメリットが得られます。

  • 責務の分離
    Modelはデータやビジネスロジックを管理し、PresenterはViewとの仲介に専念するため、各コンポーネントが明確な責務を持ちます。
  • テストの容易さ
    Modelが独立していると、ユニットテストでゲームロジックの検証がしやすくなり、UIの影響を受けずに動作確認が可能です。
  • 保守性と拡張性
    プロジェクトが大きくなった場合や、ゲームロジックが複雑になる場合、ModelとPresenterが明確に分かれていると変更が局所化され、メンテナンスが容易になります。

以下は、Modelを分離した例です。


GameModel クラスの例

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
// --- 2. Model ---
// ゲームのデータとロジックを管理するクラス
public class GameModel
{
    // 定数(ゲーム内で使用するパラメータ)
    public const float PaddleWidth = 100.0f;   // パドルの幅
    public const float PaddleSpeed = 5.0f;       // パドルの移動速度
    public const float BallRadius = 10.0f;       // ボールの半径
    public const int BlockRows = 5;              // ブロックの行数
    public const int BlockColumns = 10;          // ブロックの列数
    public const float BlockWidth = 70;          // ブロックの幅
    public const float BlockHeight = 20;         // ブロックの高さ
    public const float BlockPadding = 5;         // ブロック間の余白
    public const float BlockTopOffset = 30;      // ブロック上部からのオフセット

    // ゲーム状態
    public float PaddleX { get; set; }
    public float BallX { get; set; }
    public float BallY { get; set; }
    public float BallVelocityX { get; set; } = 3.0f;
    public float BallVelocityY { get; set; } = -3.0f;
    public List<RectangleF> Blocks { get; private set; }
    public int Score { get; set; }
    public bool IsGameOver { get; set; }

    // ゲーム初期化:パドル、ボール、ブロックの初期位置を設定
    public void Initialize(Size clientSize)
    {
        // パドルを画面下部中央に配置
        PaddleX = (clientSize.Width - PaddleWidth) / 2;
        // ボールを画面中央に配置
        BallX = clientSize.Width / 2;
        BallY = clientSize.Height / 2;
        Score = 0;
        IsGameOver = false;
        // ブロックの初期配置
        InitializeBlocks(clientSize);
    }

    // ブロックの初期配置を行う
    private void InitializeBlocks(Size clientSize)
    {
        Blocks = new List<RectangleF>();
        // 全ブロックの横幅を計算し、中央配置するための開始位置を求める
        float totalWidth = BlockColumns * BlockWidth + (BlockColumns - 1) * BlockPadding;
        float startX = (clientSize.Width - totalWidth) / 2;
        for (int row = 0; row < BlockRows; row++)
        {
            for (int col = 0; col < BlockColumns; col++)
            {
                float x = startX + col * (BlockWidth + BlockPadding);
                float y = BlockTopOffset + row * (BlockHeight + BlockPadding);
                Blocks.Add(new RectangleF(x, y, BlockWidth, BlockHeight));
            }
        }
    }

    // ゲーム状態の更新処理
    public void Update(Size clientSize, bool isLeftPressed, bool isRightPressed)
    {
        if (IsGameOver)
            return;

        // パドル移動:左右キーの入力に応じて位置を更新
        if (isLeftPressed)
            PaddleX -= PaddleSpeed;
        if (isRightPressed)
            PaddleX += PaddleSpeed;
        // 画面外に出ないように制限
        PaddleX = Math.Max(0, Math.Min(PaddleX, clientSize.Width - PaddleWidth));

        // ボールの位置更新
        BallX += BallVelocityX;
        BallY += BallVelocityY;

        // 壁との衝突判定
        if (BallX - BallRadius < 0)
        {
            BallX = BallRadius;
            BallVelocityX = -BallVelocityX;
        }
        else if (BallX + BallRadius > clientSize.Width)
        {
            BallX = clientSize.Width - BallRadius;
            BallVelocityX = -BallVelocityX;
        }
        if (BallY - BallRadius < 0)
        {
            BallY = BallRadius;
            BallVelocityY = -BallVelocityY;
        }
        else if (BallY - BallRadius > clientSize.Height)
        {
            // ボールが画面下端に到達 → ゲームオーバー
            IsGameOver = true;
            return;
        }

        // 衝突判定用のボールの矩形を作成
        RectangleF ballRect = new RectangleF(BallX - BallRadius, BallY - BallRadius, BallRadius * 2, BallRadius * 2);

        // パドルとの衝突判定(パドルは画面下から50px上に配置)
        RectangleF paddleRect = new RectangleF(PaddleX, clientSize.Height - 50, PaddleWidth, 20);
        if (paddleRect.IntersectsWith(ballRect) && BallVelocityY > 0)
        {
            // 衝突時、ボールをパドル上に固定し、Y方向の速度を反転
            BallY = paddleRect.Top - BallRadius;
            BallVelocityY = -Math.Abs(BallVelocityY);
            // パドル上の衝突位置に応じてX方向の速度を調整
            float hitPos = (BallX - paddleRect.Left) / PaddleWidth;
            BallVelocityX = (hitPos - 0.5f) * 10;
        }

        // ブロックとの衝突判定
        for (int i = 0; i < Blocks.Count; i++)
        {
            if (Blocks[i].IntersectsWith(ballRect))
            {
                Blocks.RemoveAt(i);
                BallVelocityY = -BallVelocityY;
                Score += 10;
                break;
            }
        }
    }
}

GamePresenter の更新例

Presenterは、Viewからの入力を受け取りModelの状態更新を呼び出す役割に集中します。

using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
// --- 3. Presenter ---
// Viewからの入力を受け、Modelの状態を更新し、Viewに結果を反映するクラス
public class GamePresenter
{
    private readonly IGameView view;
    private readonly GameModel model;
    // キー入力状態の管理
    private bool isLeftPressed = false;
    private bool isRightPressed = false;

    // コンストラクタ:Viewを受け取り、Modelの初期化およびイベント購読を行う
    public GamePresenter(IGameView view)
    {
        this.view = view;
        model = new GameModel();
        model.Initialize(view.ClientSize);

        // Viewのキー入力イベントを購読
        view.KeyDownEvent += OnKeyDown;
        view.KeyUpEvent += OnKeyUp;
    }

    // キーが押されたときの処理
    private void OnKeyDown(object sender, KeyEventArgs e)
    {
        if (e.KeyCode == Keys.Left)
            isLeftPressed = true;
        else if (e.KeyCode == Keys.Right)
            isRightPressed = true;
    }

    // キーが離されたときの処理
    private void OnKeyUp(object sender, KeyEventArgs e)
    {
        if (e.KeyCode == Keys.Left)
            isLeftPressed = false;
        else if (e.KeyCode == Keys.Right)
            isRightPressed = false;
    }

    // ゲーム状態の更新:ModelのUpdateメソッドを呼び出す
    public void UpdateGame()
    {
        model.Update(view.ClientSize, isLeftPressed, isRightPressed);
    }

    // Viewに公開するプロパティ(Modelの状態を反映)
    public float PaddleX => model.PaddleX;
    public float PaddleWidth => GameModel.PaddleWidth;
    public float BallX => model.BallX;
    public float BallY => model.BallY;
    public float BallRadius => GameModel.BallRadius;
    public int Score => model.Score;
    public bool IsGameOver => model.IsGameOver;
    public IReadOnlyList<RectangleF> Blocks => model.Blocks.AsReadOnly();
}

この例では、

  • GameModel はゲームデータやロジックを管理するModelの役割を担い、
  • GamePresenter はModelとViewの橋渡しを行い、入力イベントに応じてModelを更新、
  • Form1 はWinFormsのViewとして描画と入力の伝播を担当しています。

ModelとPresenterを分離することで、各コンポーネントの責務が明確になり、テストやメンテナンスがしやすくなります。


分離する場合のポイント

  • ModelとPresenterの役割が明確になる
    Modelはゲームロジックとデータ管理、Presenterは入力のハンドリングとModelとViewの仲介に専念します。
  • 再利用性とテストの向上
    Modelが独立しているため、UIに依存せずにロジック部分だけをテストしやすくなります。また、同じModelを別のUI(例えばWPFなど)でも使い回すことが可能です。
  • メンテナンスのしやすさ
    それぞれの変更が局所的になるため、将来的な機能追加やバグ修正が容易になります。

このように、Modelを分離した構成にすると、アプリケーション全体の設計がより堅牢で拡張しやすくなるため、プロジェクトの規模や複雑さに応じて採用することが推奨されます。

Model追加時のUML図

1. クラス図

各クラスとインターフェイスの関係を示すクラス図です。

  • IGameView インターフェイスを Form1 が実装
  • GamePresenter は IGameView と GameModel を利用
  • Program は Form1 を起動します

2. シーケンス図

ゲーム更新の流れを表すシーケンス図です。

  • ユーザーがキー入力
  • Form1(View)が入力イベントを発行
  • タイマーイベントで Form1 が GamePresenter.UpdateGame() を呼び出し、内部で GameModel.Update()を実行
  • その結果、状態が更新され、再描画が行われる流れを示します

3. コンポーネント図

各層の責務を大まかに示すコンポーネント図です。

  • Presentation パッケージ:IGameViewGamePresenterForm1
  • Domain パッケージ:GameModel
  • Program が Form1 を起動する構成になっています

4. オブジェクト図

オブジェクト図の説明

  • Form1 (form1)
    • 役割Form1 は、WinForms の具体的な View クラスであり、IGameView インターフェイスを実装しています。
    • 状態: 図中では、ClientSize や Title が表示され、画面の大きさやウィンドウタイトルを確認できます。
    • 関係Form1 は、自身が実装する View インターフェイスを介して、GamePresenter に渡されます。矢印のラベル「view」により、この関係が示されています。
  • GamePresenter (presenter)
    • 役割GamePresenter は、View と Model の仲介役として、ユーザー入力(キーイベントなど)を受け取り、Model の状態更新を行います。また、View への描画情報の提供も担当します。
    • 状態: 図中では、左右キーの入力状態 (isLeftPressed や isRightPressed) のサンプル値が示されています。
    • 関係GamePresenter は、Form1 から View の参照を受け取り、内部で GameModel のインスタンスを利用しています。矢印「uses」により、GamePresenter が Model を利用していることが表現されています。
  • GameModel (model)
    • 役割GameModel は、ゲームのロジックと状態(パドルの位置、ボールの位置、スコア、ブロックの一覧など)を管理する Model コンポーネントです。ここでは、ゲームの初期化や更新ロジックが実装されています。
    • 状態: 図中では、PaddleXBallXBallYScoreIsGameOver、およびブロックの状態がサンプル値として記載されています。

全体の関係

  • View と Presenter の連携
    • Form1 は IGameView の実装として、ユーザーからのキー入力などのイベントをキャプチャし、そのイベントを GamePresenter に伝達します。これにより、Presenter は View の状態変化に応じた処理を実行します。
  • Presenter と Model の連携
    • GamePresenter は、View から受け取った入力情報を元に GameModel の状態更新処理を呼び出します。Model はゲームのロジックを担い、更新された状態を Presenter 経由で View に反映させます。

このオブジェクト図は、MVPパターンで Model を明確に分離した場合の、実行時における各コンポーネントのインスタンスとその相互関係を視覚的に理解するためのものです。