4人チームでブロック崩しを作る(Gitチーム開発 × MVPパターン発展)
Gitチーム開発シリーズ:4人チームでクイズアプリを作る(Gitチーム開発発展)|4人チームでブロック崩しを作る(今ここ)
前提:以下の記事を読んでいるとスムーズです。
- 4人チームでクイズアプリを作る(Gitチーム開発発展)(同じGitチーム開発の流れを先に体験していること)
- WinForms ブロック崩しMVP チュートリアル(Model・View・Presenterの分け方を理解していること)
- WinFormsアプリで作るブロック崩し 目次(ブロック崩し本体の作り方)
はじめに
クイズアプリのチーム開発では、担当ごとのクラス(QuestionLoader・AnswerChecker・ScoreManager・UiUpdater)が「呼ばれたら答えを返すだけ」の独立した部品だったので、比較的スムーズに分担できました。
一方、ブロック崩しはゲームループです。Timerが一定間隔(30〜50ms)ごとにボールの位置・パドルの位置・ブロックの状態・スコアを同時に更新します。クイズアプリのようにきれいには分かれないので、役割分担には一工夫が必要です。
本記事では、MVPパターンのModel・View・Presenterをそのままチームの役割分担に対応させ、4〜5人でブロック崩しを完成させる進め方を紹介します。
このチュートリアルを通じて、以下のスキルを習得できます:
- WinForms でのゲームループの基本(Timer・Paint・KeyDown/KeyUp)
- 密結合になりやすい処理を、責務ごとのクラスに分ける設計力
- 「全員が同時に触る」状態をどう安全にチーム開発するかというGitの実践
Step 1. プロジェクト準備
リーダーが行う作業
- 新規 WinForms アプリを作成
プロジェクト名:TeamBlockBreaker
テンプレートは「Windows フォーム アプリ」を選びます(クイズアプリと同じ、.NET Framework版は選びません)。 - フォームに配置するUI
今回はコードで描画するため、フォームに配置するコントロールはありません。Form1の KeyPreview を true に、DoubleBuffered(プロパティが出ない場合はコンストラクタでthis.DoubleBuffered = true;)をチカチカ防止のため設定しておきます。 - GitHub Desktopでリポジトリを作成し、初期Push。「Keep this code private」のチェックを外し、public で作成します。
- main ブランチを保護しておきます。GitHub Rulesetsでmainブランチを保護する を参考に、次の3つを設定します。
Restrict deletions/Require a pull request before merging(Required approvals:1)/Block force pushes - メンバー全員がCloneして開発スタート
Step 2. チームでの役割分担
クラス構成
- モデル:
Ball、Paddle、Block - 判定:
CollisionChecker - スコア管理:
ScoreManager - 画面描画:
GameView - 統括制御:
Form1(Presenter役)
ブランチ命名
| ブランチ名 | 担当・内容 |
|---|---|
| feature/model-classes | Ball・Paddle・Block(担当A) |
| feature/collision-checker | 衝突判定(担当B) |
| feature/score-manager | スコア・ゲームオーバー管理(担当C) |
| feature/game-view | 描画処理(担当D) |
| feature/presenter-integration | リーダー:Form1統合 |
役割別担当
| 役割 | 担当 | 内容 | 難易度 |
|---|---|---|---|
| リーダー(L) | プロジェクト作成・統合 | Form1 でTimer制御と各クラスの接続、レビュー&統合 | 高 |
| 担当A | モデル | Ball・Paddle・Block クラスを作成、位置や状態だけを持つデータクラスを実装 | 低 |
| 担当B | 衝突判定 | CollisionChecker.cs を作成、壁・パドル・ブロックとの衝突を判定 | 高 |
| 担当C | スコア管理 | ScoreManager.cs を作成、得点・残りブロック数・クリア/ゲームオーバー判定 | 中 |
| 担当D | 描画 | GameView.cs を作成、ボール・パドル・ブロック・スコアの描画を担当 | 中 |
役割分担の目安
難易度の目安は、担当A(低)→担当C・D(中)→担当B・リーダー(高)です。クイズアプリと違い、担当Aのモデルクラスが全員の土台になるので、リーダーと担当Aは最初に取り掛かるのがおすすめです。
基本は5人チームです。
- リーダー:プロジェクト作成・Timer制御・統合レビュー
- 担当A〜D:それぞれの担当クラスを実装
4人チームでは、リーダーが担当A〜Dのいずれかを兼任します。3人チームでは、さらに1人が2つの担当を兼任します(例:担当C+担当D)。
同時作業を安全に進めるコツ
クイズアプリと大きく違うのはここです。QuestionLoaderやAnswerCheckerは他のクラスの形を知らなくても実装できましたが、ブロック崩しのCollisionCheckerやGameViewは、Ball・Paddle・Blockのプロパティ名が決まっていないと書き始められません。
- 最初にリーダー(または担当A)が、Ball・Paddle・Blockの3クラス(プロパティだけの空クラス)を先にmainへマージする
クイズアプリでいう「questions.csvの列構成を最初に決める」のと同じ役割です。ここが決まるまで、担当B・C・Dは着手できません。 - モデルクラスが揃ったら、担当B・C・Dはそれぞれのブランチで並行して作業を始めます。
- 作業を始める前に、最新のmainを自分の作業ブランチへ取り込みます(手順はStep 4で確認)。
- 自分の担当ファイル以外は触らない。同じファイルを複数人で変更すると、競合(Conflict)が発生しやすくなります。
なお、リーダーが行うForm1.csの統合作業は、各担当のPull Requestがレビューされ、mainにMergeされたあとに行います。チーム全員の担当作業がmainに反映されたことを確認してから進めましょう。
Step 3. コード実装
Ball.cs / Paddle.cs / Block.cs(担当A)
- 役割:位置・速度・状態だけを持つデータクラス。ロジックは持たせません。
- ポイント:全員がこのクラスの形に合わせて実装するので、名前とプロパティを最初に決めて共有することが最重要です。
namespace TeamBlockBreaker
{
public class Ball
{
public float X { get; set; }
public float Y { get; set; }
public float VelocityX { get; set; }
public float VelocityY { get; set; }
public int Size { get; set; } = 12;
public void Move()
{
X += VelocityX;
Y += VelocityY;
}
}
}
namespace TeamBlockBreaker
{
public class Paddle
{
public float X { get; set; }
public float Y { get; set; }
public int Width { get; set; } = 80;
public int Height { get; set; } = 12;
public int Speed { get; set; } = 8;
public void MoveLeft() => X -= Speed;
public void MoveRight() => X += Speed;
}
}
namespace TeamBlockBreaker
{
public class Block
{
public float X { get; set; }
public float Y { get; set; }
public int Width { get; set; } = 50;
public int Height { get; set; } = 20;
public bool IsDestroyed { get; set; } = false;
}
}
CollisionChecker.cs(担当B)
- 役割:壁・天井・パドル・ブロックとの衝突判定をすべて担当する。
- ポイント:判定結果に応じて速度を反転させるだけで、スコア加算やゲームオーバー処理はしません(担当Cの責務にする)。
using System.Collections.Generic;
using System.Drawing;
namespace TeamBlockBreaker
{
public class CollisionChecker
{
public void CheckWallCollision(Ball ball, int formWidth)
{
if (ball.X <= 0 || ball.X + ball.Size >= formWidth)
{
ball.VelocityX = -ball.VelocityX;
}
}
public void CheckCeilingCollision(Ball ball)
{
if (ball.Y <= 0)
{
ball.VelocityY = -ball.VelocityY;
}
}
public void CheckPaddleCollision(Ball ball, Paddle paddle)
{
var ballRect = new RectangleF(ball.X, ball.Y, ball.Size, ball.Size);
var paddleRect = new RectangleF(paddle.X, paddle.Y, paddle.Width, paddle.Height);
if (ballRect.IntersectsWith(paddleRect) && ball.VelocityY > 0)
{
ball.VelocityY = -ball.VelocityY;
}
}
public Block CheckBlockCollision(Ball ball, List blocks)
{
var ballRect = new RectangleF(ball.X, ball.Y, ball.Size, ball.Size);
foreach (var block in blocks)
{
if (block.IsDestroyed) continue;
var blockRect = new RectangleF(block.X, block.Y, block.Width, block.Height);
if (ballRect.IntersectsWith(blockRect))
{
block.IsDestroyed = true;
ball.VelocityY = -ball.VelocityY;
return block;
}
}
return null;
}
}
}
ScoreManager.cs(担当C)
- 役割:得点・残りブロック数を記録し、クリア/ゲームオーバーを判定する。
- ポイント:成績を管理する責務を一元化している(クイズアプリのScoreManagerと同じ考え方)。
namespace TeamBlockBreaker
{
public class ScoreManager
{
public int Score { get; private set; }
public int RemainingBlocks { get; private set; }
public bool IsGameOver { get; private set; }
public bool IsCleared { get; private set; }
public void Initialize(int totalBlocks)
{
RemainingBlocks = totalBlocks;
Score = 0;
IsGameOver = false;
IsCleared = false;
}
public void AddScore(int point = 100)
{
Score += point;
RemainingBlocks--;
if (RemainingBlocks <= 0) IsCleared = true;
}
public void SetGameOver()
{
IsGameOver = true;
}
public string GetStatusText()
{
return $"スコア: {Score} 残りブロック: {RemainingBlocks}";
}
}
}
GameView.cs(担当D)
- 役割:ボール・パドル・ブロック・スコア表示の描画をまとめる。
- ポイント:描画だけに専念し、位置の計算や判定はしません(Formのpaintイベントから呼ばれるだけ)。
using System.Collections.Generic;
using System.Drawing;
namespace TeamBlockBreaker
{
public class GameView
{
public void DrawBall(Graphics g, Ball ball)
{
g.FillEllipse(Brushes.OrangeRed, ball.X, ball.Y, ball.Size, ball.Size);
}
public void DrawPaddle(Graphics g, Paddle paddle)
{
g.FillRectangle(Brushes.SteelBlue, paddle.X, paddle.Y, paddle.Width, paddle.Height);
}
public void DrawBlocks(Graphics g, List blocks)
{
foreach (var block in blocks)
{
if (block.IsDestroyed) continue;
g.FillRectangle(Brushes.MediumSeaGreen, block.X, block.Y, block.Width, block.Height);
g.DrawRectangle(Pens.White, block.X, block.Y, block.Width, block.Height);
}
}
public void DrawStatus(Graphics g, string text)
{
g.DrawString(text, new Font("Meiryo", 12), Brushes.Black, 10, 10);
}
public void DrawGameOver(Graphics g, Size clientSize)
{
g.DrawString("GAME OVER", new Font("Meiryo", 24, FontStyle.Bold),
Brushes.Red, clientSize.Width / 2 - 100, clientSize.Height / 2 - 20);
}
public void DrawClear(Graphics g, Size clientSize)
{
g.DrawString("CLEAR!", new Font("Meiryo", 24, FontStyle.Bold),
Brushes.Blue, clientSize.Width / 2 - 60, clientSize.Height / 2 - 20);
}
}
}
Form1.cs(リーダー統合)
- 役割:全体の流れを制御する中核(Presenter)。
- 内容:Timerで毎フレーム、パドル移動→ボール移動→各種衝突判定→スコア反映→再描画、の順に呼び出す。
- ポイント:各部品(Model/CollisionChecker/ScoreManager/GameView)を組み合わせ、イベント駆動でゲームを動かす。
using System.Collections.Generic;
using System.Windows.Forms;
namespace TeamBlockBreaker
{
public partial class Form1 : Form
{
private Ball ball;
private Paddle paddle;
private List blocks;
private readonly CollisionChecker collision = new CollisionChecker();
private readonly ScoreManager score = new ScoreManager();
private readonly GameView view = new GameView();
private bool leftPressed = false;
private bool rightPressed = false;
public Form1()
{
InitializeComponent();
this.KeyPreview = true;
this.DoubleBuffered = true;
InitializeGame();
}
private void InitializeGame()
{
ball = new Ball { X = 200, Y = 300, VelocityX = 3, VelocityY = -3 };
paddle = new Paddle { X = 160, Y = 360 };
blocks = new List();
for (int row = 0; row < 3; row++)
{
for (int col = 0; col < 6; col++)
{
blocks.Add(new Block { X = col * 60 + 10, Y = row * 25 + 40 });
}
}
score.Initialize(blocks.Count);
}
private void gameTimer_Tick(object sender, System.EventArgs e)
{
if (score.IsGameOver || score.IsCleared) return;
if (leftPressed) paddle.MoveLeft();
if (rightPressed) paddle.MoveRight();
ball.Move();
collision.CheckWallCollision(ball, this.ClientSize.Width);
collision.CheckCeilingCollision(ball);
collision.CheckPaddleCollision(ball, paddle);
var hitBlock = collision.CheckBlockCollision(ball, blocks);
if (hitBlock != null) score.AddScore();
if (ball.Y > this.ClientSize.Height) score.SetGameOver();
this.Invalidate();
}
private void Form1_Paint(object sender, PaintEventArgs e)
{
var g = e.Graphics;
view.DrawBall(g, ball);
view.DrawPaddle(g, paddle);
view.DrawBlocks(g, blocks);
view.DrawStatus(g, score.GetStatusText());
if (score.IsGameOver) view.DrawGameOver(g, this.ClientSize);
if (score.IsCleared) view.DrawClear(g, this.ClientSize);
}
private void Form1_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Left) leftPressed = true;
if (e.KeyCode == Keys.Right) rightPressed = true;
}
private void Form1_KeyUp(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Left) leftPressed = false;
if (e.KeyCode == Keys.Right) rightPressed = false;
}
}
}
重要:フォームの Load イベントで gameTimer.Start(); を呼び出すこと、そして KeyDown/KeyUp/Paint の各イベントを、それぞれ Form1_KeyDown/Form1_KeyUp/Form1_Paint に設定しておきましょう。Timerの Tick イベントも gameTimer_Tick に設定します。
Step 4. GitHub Desktopでの運用ルール
基本の流れ:main を最新化 → 作業ブランチへ反映 → 編集 → Commit → Push → PR
特に重要なのが、誰かのPRがmainにマージされた後の同期です。クイズアプリの回と同じ手順なので、まだの人はそちらも参考にしてください。
誰かのPRがmainにマージされたら
- 全員:mainを最新の状態にする(Fetch origin → Pull origin)
- 作業中のブランチがある人:作業ブランチに戻り、「Update from main」でmainの変更を取り込む
- 新しい作業を始める人:最新にしたmainから新しい作業ブランチを作る
特に今回はBall・Paddle・Blockのモデルクラスが全員の土台になるため、モデルがmainにマージされたタイミングで、全員が①〜②を必ず行ってから作業を再開してください。ここを飛ばすと、古いモデルの形のままコードを書いてしまい、あとで大きなコンフリクトになります。
Commit前に確認すること
- クラスファイルを追加したら、すべて保存(Ctrl+Shift+S)してから Commit する
- 自分の担当クラス以外は触らない
- コミットメッセージは日本語で簡潔に(例:「CollisionChecker.csを追加(担当B)」)
Step 5. Rulesetsを試してみる(体験)
Step 1で設定したmain ブランチのRulesetsが、実際にどう働くのかをチームで体験してみましょう。クイズアプリの回で体験した人も、復習として試してみてください。
実験1:mainへ直接pushしてみる
誰か1人が、わざとmain ブランチのままファイルを1行変更し、Commit → Pushしてみます。Pushが拒否され、Pull Requestの作成を促すメッセージが表示されることを確認しましょう。
実験2:承認なしでMergeしてみる
誰かがPull Requestを作成した直後、まだ誰も承認していない状態でMergeボタンを押してみます。ボタンが押せない、または「At least 1 approving review is required」と表示されることを確認し、別のメンバーがApproveしたあとにボタンが押せるようになる様子を観察しましょう。
まとめ
- ゲームは全クラスが同じ状態を毎フレーム共有するため、クイズアプリより並行作業が難しい
- だからこそ、最初にModel(Ball・Paddle・Block)の形を決めて共有することが最優先
- Model・CollisionChecker・ScoreManager・GameViewと責務を分けることで、オブジェクト指向の理解にもつながる
- この「分けにくさ」を体験しておくと、Unityのコンポーネント指向がいかに疎結合で楽かを実感しやすくなる
参考(設計パターン)
このブロック崩しの構成では、以下の設計原則やパターンが関連しています。
単一責任の原則 (SRP)
各クラスがそれぞれ「位置と状態の保持」「衝突判定」「スコア管理」「描画」という明確な責務に集中しており、SRP に基づいて設計されています。
MVP(Model-View-Presenter)
Ball・Paddle・Block が Model、GameView が View、Form1(Timer制御)が Presenter にあたります。ブロック崩しMVPチュートリアルで1人で行ったリファクタリングを、最初からチームの役割分担として設計したものと言えます。
このアプリの元になった、1人用のWinFormsアプリで作るブロック崩し 目次もあわせてご覧ください。










ディスカッション
コメント一覧
まだ、コメントがありません