チーム開発で学ぶ!WinFormカードゲーム基盤構築【制作編】

2025年2月19日

~GitHub Desktopを活用した4人チームのステップバイステップ実践ガイド~

この資料では、C# と WinForm を使ってシンプルなカードゲーム基盤を実装します。
基本機能(カード生成、デッキ管理、手札表示、UI操作)から始まり、拡張機能(UserControlによるカード表示、リファクタリング、応用技術)へと段階的に進みます。
また、GitHub Desktop を利用したチーム開発の手法と各担当タスクの分担も解説しています。


目次

  1. プロジェクト概要とチーム開発のポイント
  2. 基本機能実装フェーズ(ステップ1~4)
    • Step 1: Card クラスの作成
    • Step 2: Deck クラスの作成
    • Step 3: Player クラスの作成
    • Step 4: WinForm UI(MainForm)の構築
  3. 拡張機能実装フェーズ(ステップ5)
  4. リファクタリング&応用フェーズ(ステップ6~10)
  5. チーム開発:GitHub Desktop活用とタスク担当
  6. まとめと今後の課題

説明

  • Card クラス
    • カード1枚の情報(スート、数値、画像)を保持します。
    • ToString() メソッドで「スート の 数値」の文字列を返します。
  • Deck クラス
    • 複数の Card オブジェクトをリストで管理し、シャッフル(Fisher–Yates アルゴリズム)やカードを引く機能を提供します。
  • Player クラス
    • プレイヤーの名前と、手札(Card のリスト)を管理します。
    • 手札の表示には ListBox を利用します。
  • MainForm クラス
    • WinForm アプリケーションのメイン画面です。
    • Deck、Player を利用し、PictureBox でカード画像、ListBox で手札、各種 Button や Label を使って操作・表示します。
  • CardControl クラス
    • カスタムの UserControl で、カードの画像と情報表示を担当します。
    • MainForm などで再利用できるようになっています。

このクラス図をもとに、各自の担当部分や役割分担を明確にしながら、チーム開発で実装を進めると良いでしょう。

シーケンス図の流れ

  1. ユーザー操作:
    ユーザーが「カードを引く」ボタンをクリックします。
  2. MainForm の処理:
    MainForm のイベントハンドラ (btnDrawCard_Click) が呼ばれ、Deck の DrawCard() メソッドを呼び出します。
  3. Deck の動作:
    Deck は内部リストからカードを取り出し、そのカードオブジェクトを MainForm に返します。
  4. Player への追加:
    MainForm は返されたカードを Player の AddCardToHand() メソッドに渡して、手札に追加します。
  5. UI の更新:
    MainForm は残りのカード枚数を Label に表示し、PictureBox の画像を更新します。
  6. UserControl の利用(任意):
    もしカスタム UserControl を使用している場合は、MainForm が CardControl の SetCard() メソッドを呼び出し、カード情報を再表示します。

このシーケンス図を参考に、実際の実装やデバッグを行うと、各クラス間の連携がより明確になるでしょう。

1. プロジェクト概要とチーム開発のポイント

プロジェクト概要

  • 目的
    WinForm を使って、カードゲームの基盤(カード生成、デッキ管理、手札表示)を実装する。
  • 主な機能
    • 基本クラス(Card、Deck、Player)の実装
    • Fisher–Yates アルゴリズムによるシャッフル処理
    • WinForm UI によるカード操作(カードを引く、手札表示)
    • 拡張機能として、UserControl を用いたカード表示、列挙型、非同期処理、Unityでの再現例なども学習

チーム開発のポイント

  • バージョン管理
    GitHub Desktop を活用し、各自が担当ブランチで作業。定期的なコミット・プルリクエストを通じて統合とコードレビューを実施。
  • 役割分担
    各メンバーの担当タスクを以下のように平行開発できるようバランスよく割り当てます。

2. 基本機能実装フェーズ(ステップ1~4)

このフェーズでは、WinForm を用いた基本機能の実装に取り組みます。
ここまでで、カードの生成、シャッフル、プレイヤーの手札管理、UI での基本操作が完成し、動作確認が可能となります。

Step 1: Card クラスの作成

目的
1枚のカードを表すクラスを作成し、カードのスート、数値、画像情報を保持する。

担当タスク

  • 担当:メンバーA
    • 「Card.cs」を作成し、XMLドキュメントコメント付きで実装

【コード例:Card.cs】

using System.Drawing;

/// <summary>
/// 1枚のカードを表します.
/// カードはスート、数値、そして画像情報を保持します.
/// </summary>
public class Card
{
    /// <summary>
    /// カードのスート(例:"ハート")を取得します.
    /// </summary>
    public string Suit { get; private set; }

    /// <summary>
    /// カードの数値(1~13)を取得します.
    /// </summary>
    public int Value { get; private set; }

    /// <summary>
    /// カードの画像を取得します.
    /// </summary>
    public Image CardImage { get; private set; }

    /// <summary>
    /// 新しい Card インスタンスを初期化します.
    /// </summary>
    /// <param name="suit">カードのスート.</param>
    /// <param name="value">カードの数値.</param>
    /// <param name="cardImage">カードの画像.</param>
    public Card(string suit, int value, Image cardImage)
    {
        Suit = suit;
        Value = value;
        CardImage = cardImage;
    }

    /// <summary>
    /// カードの内容を「スート の 数値」という形式の文字列で返します.
    /// </summary>
    /// <returns>カード情報の文字列.</returns>
    public override string ToString()
    {
        return $"{Suit} の {Value}";
    }
}

Step 2: Deck クラスの作成

目的
デッキ全体のカード管理、シャッフル、カードを引く機能を実装する。

担当タスク

  • 担当:メンバーB
    • 「Deck.cs」を作成し、Fisher–Yates アルゴリズムを用いたシャッフル処理を実装

【コード例:Deck.cs】

using System;
using System.Collections.Generic;
using System.Drawing;

/// <summary>
/// 複数のカードを管理するクラスです.
/// カードの生成、シャッフル、カードを引く機能を提供します.
/// </summary>
public class Deck
{
    private List<Card> cards;

    /// <summary>
    /// Deck を初期化し、全カード(4スート × 13 枚)を生成します.
    /// </summary>
    public Deck()
    {
        cards = new List<Card>();
        string[] suits = { "Heart", "Diamond", "Club", "Spade" };

        foreach (var suit in suits)
        {
            for (int i = 1; i <= 13; i++)
            {
                // ※画像ファイルはプロジェクト内の「images」フォルダに配置してください.
                string imagePath = $"images/{suit.ToLower()}_{i}.png";
                Image cardImage = Image.FromFile(imagePath);
                cards.Add(new Card(suit, i, cardImage));
            }
        }
    }

    /// <summary>
    /// Fisher–Yates アルゴリズムを使用してカードをシャッフルします.
    /// </summary>
    public void Shuffle()
    {
        Random rand = new Random();
        for (int i = cards.Count - 1; i > 0; i--)
        {
            int j = rand.Next(i + 1); // 0~i の乱数
            Card temp = cards[i];
            cards[i] = cards[j];
            cards[j] = temp;
        }
    }

    /// <summary>
    /// デッキから1枚カードを引き、そのカードをデッキから削除します.
    /// </summary>
    /// <returns>引かれたカード。カードがない場合は null を返します.</returns>
    public Card DrawCard()
    {
        if (cards.Count == 0)
            return null;
        var card = cards[0];
        cards.RemoveAt(0);
        return card;
    }

    /// <summary>
    /// 現在残っているカードの枚数を返します.
    /// </summary>
    /// <returns>カード枚数.</returns>
    public int CardsRemaining()
    {
        return cards.Count;
    }
}

Step 3: Player クラスの作成

目的
プレイヤーの名前と手札(カードのリスト)を管理し、UI に手札を表示する機能を実装する。

担当タスク

  • 担当:メンバーC
    • 「Player.cs」を作成し、手札管理と ListBox への表示処理を実装

【コード例:Player.cs】

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

/// <summary>
/// プレイヤーを表すクラスです。プレイヤーは名前と手札を持ちます.
/// </summary>
public class Player
{
    /// <summary>
    /// プレイヤーの名前を取得します.
    /// </summary>
    public string Name { get; private set; }

    private List<Card> hand;

    /// <summary>
    /// 新しい Player インスタンスを初期化します.
    /// </summary>
    /// <param name="name">プレイヤーの名前.</param>
    public Player(string name)
    {
        Name = name;
        hand = new List<Card>();
    }

    /// <summary>
    /// 手札にカードを追加します.
    /// </summary>
    /// <param name="card">追加するカード.</param>
    public void AddCardToHand(Card card)
    {
        if (card != null)
        {
            hand.Add(card);
        }
    }

    /// <summary>
    /// ListBox に手札のカード情報を表示します.
    /// </summary>
    /// <param name="listBox">表示先の ListBox.</param>
    public void ShowHand(ListBox listBox)
    {
        listBox.Items.Clear();
        foreach (var card in hand)
        {
            listBox.Items.Add(card.ToString());
        }
    }
}

Step 4: WinForm UI の作成

目的
フォーム上に主要なコントロール(PictureBox、ListBox、Button、Label)を配置し、カードを引く、手札を表示する操作を実装する。

担当タスク

  • 担当:メンバーD
    • MainForm を作成し、WinForm デザイナーで以下のコントロールを配置:
      • PictureBox(例:pictureBoxCard):カード画像表示用
      • ListBox(例:listBoxHand):手札表示用
      • Button(例:btnDrawCard, btnShowHand):操作用
      • Label(例:lblCardsRemaining):残りカード枚数表示用
    • ボタンのクリックイベントに、Deck からカードを引く処理と ListBox への手札表示処理を実装

【コード例:MainForm.cs】

using System;
using System.Windows.Forms;
using System.Drawing;

/// <summary>
/// メインフォームです。ここでデッキからカードを引いたり、手札を表示したりします.
/// </summary>
public partial class MainForm : Form
{
    private Deck deck;
    private Player player;

    /// <summary>
    /// MainForm の新しいインスタンスを初期化します.
    /// </summary>
    public MainForm()
    {
        InitializeComponent();
        deck = new Deck();
        player = new Player("プレイヤー1");
        deck.Shuffle();
    }

    /// <summary>
    /// 「カードを引く」ボタンのクリックイベント処理です.
    /// </summary>
    private void btnDrawCard_Click(object sender, EventArgs e)
    {
        var card = deck.DrawCard();
        if (card != null)
        {
            player.AddCardToHand(card);
            lblCardsRemaining.Text = $"残りのカード枚数: {deck.CardsRemaining()}";
            pictureBoxCard.Image = card.CardImage;
        }
        else
        {
            lblCardsRemaining.Text = "カードがもうありません";
        }
    }

    /// <summary>
    /// 「手札を表示」ボタンのクリックイベント処理です.
    /// </summary>
    private void btnShowHand_Click(object sender, EventArgs e)
    {
        player.ShowHand(listBoxHand);
    }
}

3. 拡張フェーズ:UserControl を使ったカード表示

目的
カード表示処理を UserControl にまとめることで、再利用性と保守性を向上させます。(基本機能が完成した後の拡張タスク)

担当タスク

  • 担当:メンバーC
    • 「CardControl.cs」を新規作成
    • フォームデザイナーで PictureBox と Label を配置し、カード表示ロジックを実装
    • MainForm との統合テストを実施

【コード例:CardControl.cs】

using System.Windows.Forms;
using System.Drawing;

/// <summary>
/// カードの画像と情報を表示するカスタムユーザーコントロールです.
/// </summary>
public partial class CardControl : UserControl
{
    /// <summary>
    /// CardControl の新しいインスタンスを初期化します.
    /// </summary>
    public CardControl()
    {
        InitializeComponent();
    }

    /// <summary>
    /// 指定されたカードの情報を表示します.
    /// </summary>
    /// <param name="card">表示するカード。null の場合は表示をクリアします.</param>
    public void SetCard(Card card)
    {
        if (card != null)
        {
            pictureBoxCard.Image = card.CardImage;
            labelCardText.Text = $"{card.Suit} の {card.Value}";
        }
        else
        {
            pictureBoxCard.Image = null;
            labelCardText.Text = string.Empty;
        }
    }
}

CardControl を有効にするための主な変更点は、既存のカード表示ロジックを CardControl に置き換える点にあります。具体的には、以下のような変更が必要です:

  • UI の変更
    • MainForm のデザイナー上で、従来の PictureBox や Label を使ったカード表示部分を削除または非表示にし、代わりに CardControl を配置します。
    • CardControl の位置やサイズ、レイアウトを調整して、ユーザーに正しくカード情報が表示されるようにします。
  • イベント処理の修正
    • 「カードを引く」ボタン(btnDrawCard)のクリックイベントで、カードが引かれた際に従来の pictureBoxCard への画像設定やラベルへのテキスト設定ではなく、CardControl の SetCard メソッドを呼び出すように変更します。
    • 例として、以下のような変更が考えられます:
private void btnDrawCard_Click(object sender, EventArgs e)
{
    var card = deck.DrawCard();
    if (card != null)
    {
        player.AddCardToHand(card);
        lblCardsRemaining.Text = $"残りのカード枚数: {deck.CardsRemaining()}";
        // 変更:CardControl にカード情報をセットする
        cardControl.SetCard(card);
    }
    else
    {
        lblCardsRemaining.Text = "カードがもうありません";
        // カードが無い場合は CardControl の表示をクリアする
        cardControl.SetCard(null);
    }
}
  • コードのリファクタリング
    • CardControl にカード表示の責務を集約することで、MainForm のコードから直接画像やテキストの設定処理を削除し、モジュール化・再利用性を高めます。
    • また、CardControl 内の初期化処理(InitializeComponent)が正しく実行されるように、コンストラクタやデザイナーコードの整合性を確認します。

これらの変更により、カード表示に関する処理が CardControl 内に集約され、今後の拡張や保守がしやすい設計となります。


4. リファクタリング&応用フェーズ:ステップ 6~10

(※ 応用編。各自の理解度に合わせて取り入れてください。)

  • Step 6: 画像管理の改善
    • 現状は Image 型で管理しています。将来的にリソース管理やキャッシュ方法の改善を検討。
  • Step 7: Suit(スート)の列挙型化
    担当:メンバーA
    • 「Suit.cs」を作成し、Card や Deck で利用します.

【コード例:Suit.cs】

/// <summary>
/// カードのスート(種類)を表す列挙型です.
/// </summary>
public enum Suit
{
    /// <summary>ハート</summary>
    Heart,
    /// <summary>ダイヤ</summary>
    Diamond,
    /// <summary>クラブ</summary>
    Club,
    /// <summary>スペード</summary>
    Spade
}

Suit(列挙型)を既存のコードに適用するには、以下の手順で各クラスを修正します。


1. Card クラスの修正

Card クラスで、スートの情報を string 型から Suit 型に変更します。また、ToString メソッドも修正して Suit.ToString() を利用します。

変更前(例):

public class Card
{
    public string Suit { get; private set; }
    // ... その他のプロパティ

    public Card(string suit, int value, Image cardImage)
    {
        Suit = suit;
        Value = value;
        CardImage = cardImage;
    }

    public override string ToString()
    {
        return $"{Suit} の {Value}";
    }
}

変更後:

using System.Drawing;

/// <summary>
/// 1枚のカードを表します.
/// カードはスート、数値、そして画像情報を保持します.
/// </summary>
public class Card
{
    /// <summary>
    /// カードのスートを取得します.
    /// </summary>
    public Suit Suit { get; private set; }

    /// <summary>
    /// カードの数値(1~13)を取得します.
    /// </summary>
    public int Value { get; private set; }

    /// <summary>
    /// カードの画像を取得します.
    /// </summary>
    public Image CardImage { get; private set; }

    /// <summary>
    /// 新しい Card インスタンスを初期化します.
    /// </summary>
    /// <param name="suit">カードのスート.</param>
    /// <param name="value">カードの数値.</param>
    /// <param name="cardImage">カードの画像.</param>
    public Card(Suit suit, int value, Image cardImage)
    {
        Suit = suit;
        Value = value;
        CardImage = cardImage;
    }

    /// <summary>
    /// カードの内容を「スート の 数値」という形式の文字列で返します.
    /// </summary>
    /// <returns>カード情報の文字列.</returns>
    public override string ToString()
    {
        return $"{Suit.ToString()} の {Value}";
    }
}

3. Deck クラスの修正

カード生成の際に、スート情報を列挙型 Suit で扱うようにします。従来は string[] を使っていましたが、Enum.GetValues を利用して全ての Suit を取得できます。

変更前(例):

public Deck()
{
    cards = new List<Card>();
    string[] suits = { "Heart", "Diamond", "Club", "Spade" };

    foreach (var suit in suits)
    {
        for (int i = 1; i <= 13; i++)
        {
            string imagePath = $"images/{suit.ToLower()}_{i}.png";
            Image cardImage = Image.FromFile(imagePath);
            cards.Add(new Card(suit, i, cardImage));
        }
    }
}

変更後:

using System;
using System.Collections.Generic;
using System.Drawing;

public class Deck
{
    private List<Card> cards;

    /// <summary>
    /// Deck を初期化し、全カード(各スート × 13 枚)を生成します.
    /// </summary>
    public Deck()
    {
        cards = new List<Card>();

        // Enum からすべてのスートを取得
        foreach (Suit suit in Enum.GetValues(typeof(Suit)))
        {
            for (int i = 1; i <= 13; i++)
            {
                // 画像ファイルはプロジェクト内の「images」フォルダに配置してください.
                // suit.ToString().ToLower() を使用してファイル名を生成
                string imagePath = $"images/{suit.ToString().ToLower()}_{i}.png";
                Image cardImage = Image.FromFile(imagePath);
                cards.Add(new Card(suit, i, cardImage));
            }
        }
    }

    // ※ Shuffle、DrawCard、CardsRemaining のメソッドはそのままでOK
    public void Shuffle()
    {
        Random rand = new Random();
        for (int i = cards.Count - 1; i > 0; i--)
        {
            int j = rand.Next(i + 1); // 0~i の乱数
            Card temp = cards[i];
            cards[i] = cards[j];
            cards[j] = temp;
        }
    }

    public Card DrawCard()
    {
        if (cards.Count == 0)
            return null;
        var card = cards[0];
        cards.RemoveAt(0);
        return card;
    }

    public int CardsRemaining()
    {
        return cards.Count;
    }
}

4. 他のクラスへの影響

  • Player クラス
    Card クラスの変更により、Player クラス内でカードを扱う処理はそのままで問題ありません。ToString メソッドが自動的に Suit の情報を文字列に変換するためです。
  • CardControl や MainForm
    Card の生成や表示部分において、特に Suit に依存した変更は必要ありません。ただし、画像ファイル名の生成方法が変わったため、実際の画像ファイル名が heart_1.png(すべて小文字)などとなっていることを確認してください。

まとめ

  1. Suit 列挙型の作成: Suit.cs に列挙型を定義します。
  2. Card クラスの修正: スートの型を string から Suit に変更し、コンストラクタと ToString メソッドを更新します。
  3. Deck クラスの修正: カード生成時に Enum.GetValues を用いて Suit の値を利用し、画像パスなどを生成します。

これにより、カードのスート情報が型安全になり、後の拡張やメンテナンスが容易になります。

  • Step 8: クラスの責務分離(Hand クラスの導入)
    担当:メンバーC
    • 「Hand.cs」を作成し、Player クラスの手札管理部分を移行します.
    • Hand クラスは、プレイヤーの手札(カードのリスト)の管理と表示の責務を持ちます。すでに作成済みの Hand クラスは以下のようになっています。

【コード例:Hand.cs】

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

/// <summary>
/// プレイヤーの手札(カードの集合)を管理するクラスです.
/// </summary>
public class Hand
{
    private readonly List<Card> cards;

    /// <summary>
    /// Hand の新しいインスタンスを初期化します.
    /// </summary>
    public Hand()
    {
        cards = new List<Card>();
    }

    /// <summary>
    /// 手札にカードを追加します.
    /// </summary>
    /// <param name="card">追加するカード.</param>
    public void AddCard(Card card)
    {
        if (card != null)
        {
            cards.Add(card);
        }
    }

    /// <summary>
    /// ListBox に手札のカード情報を表示します.
    /// </summary>
    /// <param name="listBox">表示先の ListBox.</param>
    public void ShowHand(ListBox listBox)
    {
        listBox.Items.Clear();
        foreach (var card in cards)
        {
            listBox.Items.Add(card.ToString());
        }
    }

    /// <summary>
    /// 手札に含まれるカードの枚数を取得します.
    /// </summary>
    public int CardCount => cards.Count;
}

Player クラスから直接手札の List\ を管理していた部分を、Hand クラスに委譲することで責務を分離できます。以下はそのリファクタリング例です。

1. Player クラスの修正

元々の Player クラスでは手札を List\ で管理していましたが、これを Hand クラスのインスタンスに置き換えます。

/// <summary>
/// プレイヤーを表すクラスです。プレイヤーは名前と手札(Hand クラス)を持ちます.
/// </summary>
public class Player
{
    /// <summary>
    /// プレイヤーの名前を取得します.
    /// </summary>
    public string Name { get; private set; }

    /// <summary>
    /// プレイヤーの手札を管理する Hand クラスのインスタンス.
    /// </summary>
    public Hand Hand { get; private set; }

    /// <summary>
    /// 新しい Player インスタンスを初期化します.
    /// </summary>
    /// <param name="name">プレイヤーの名前.</param>
    public Player(string name)
    {
        Name = name;
        Hand = new Hand();
    }

    /// <summary>
    /// 手札にカードを追加します.
    /// </summary>
    /// <param name="card">追加するカード.</param>
    public void AddCardToHand(Card card)
    {
        Hand.AddCard(card);
    }

    /// <summary>
    /// ListBox に手札のカード情報を表示します.
    /// </summary>
    /// <param name="listBox">表示先の ListBox.</param>
    public void ShowHand(ListBox listBox)
    {
        Hand.ShowHand(listBox);
    }
}

まとめ

  • Player クラスのリファクタリング: 直接 List\ を持つのではなく、Hand クラスのインスタンスをメンバーとして持つ。
  • 責務の分離: 手札管理の責務を Hand クラスに集約することで、コードの再利用性と保守性が向上する。
  • UI への影響: UI 側のコードは Player のメソッド呼び出しのままで、内部で Hand クラスを使っているため、動作に変化はなくなります。

このように、Hand クラスを適用することで、プレイヤーの手札管理がより明確に分離され、将来的な拡張や変更が容易になります。

  • Step 9: 非同期処理の導入(応用)
    担当:メンバーB
    • Deck クラスに ShuffleAsync() メソッドを追加し、非同期でシャッフル処理を実装します.

【コード例:Deck.cs 内の一部】

using System.Threading.Tasks;

// ...

/// <summary>
/// 非同期にシャッフル処理を実行します.
/// UI スレッドをブロックしないために、Task.Run を利用してバックグラウンドでシャッフルを実行します.
/// </summary>
/// <returns>非同期処理のタスク.</returns>
public async Task ShuffleAsync()
{
    await Task.Run(() =>
    {
        Random rand = new Random();
        for (int i = cards.Count - 1; i > 0; i--)
        {
            int j = rand.Next(i + 1);
            Card temp = cards[i];
            cards[i] = cards[j];
            cards[j] = temp;
        }
    });
}

非同期処理の導入(応用) における変更の必要性と、それに伴う MainForm クラスの変更前後のコード例です。

変更前のコード

/// <summary>
/// メインフォームです。ここでデッキからカードを引いたり、手札を表示したりします.
/// </summary>
public partial class MainForm : Form
{
    private Deck deck;
    private Player player;

    /// <summary>
    /// MainForm の新しいインスタンスを初期化します.
    /// </summary>
    public MainForm()
    {
        InitializeComponent();
        deck = new Deck();
        player = new Player("プレイヤー1");
        deck.Shuffle();
    }

    /// <summary>
    /// 「カードを引く」ボタンのクリックイベント処理です.
    /// </summary>
    private void btnDrawCard_Click(object sender, EventArgs e)
    {
        var card = deck.DrawCard();
        if (card != null)
        {
            player.AddCardToHand(card);
            lblCardsRemaining.Text = $"残りのカード枚数: {deck.CardsRemaining()}";
            cardControl.SetCard(card); // UserControl を利用してカード表示
        }
        else
        {
            lblCardsRemaining.Text = "カードがもうありません";
            cardControl.SetCard(null);
        }
    }

    /// <summary>
    /// 「手札を表示」ボタンのクリックイベント処理です.
    /// </summary>
    private void btnShowHand_Click(object sender, EventArgs e)
    {
        player.ShowHand(listBoxHand);
    }
}

問題点:

  • コンストラクタ内で deck.ShuffleAsync() を呼び出していますが、非同期メソッドの呼び出しを待たずに次の処理に進むため、シャッフル処理が完了する前にフォームが表示される恐れがあります。
  • また、非同期処理は UI の応答性を保つために、適切に await で待機する必要があります。

変更後のコード

以下の変更を適用しています:

  • コンストラクタから非同期シャッフル呼び出しを削除。
  • フォームの Load イベントハンドラ内で await deck.ShuffleAsync() を呼び出し、UI スレッドがブロックされないように非同期処理を実行。
/// <summary>
/// メインフォームです。ここでデッキからカードを引いたり、手札を表示したりします.
/// 非同期処理を導入して、UI の応答性を維持します.
/// </summary>
public partial class MainForm : Form
{
    private Deck deck;
    private Player player;

    /// <summary>
    /// MainForm の新しいインスタンスを初期化します.
    /// コンストラクタ内では非同期シャッフル処理は呼び出さず、フォームのロード時に実行します.
    /// </summary>
    public MainForm()
    {
        InitializeComponent();
        deck = new Deck();
        player = new Player("プレイヤー1");
    }

    /// <summary>
    /// フォームロード時に非同期でシャッフル処理を実行します.
    /// UI スレッドをブロックせず、例外発生時にはエラーメッセージを表示します.
    /// </summary>
    /// <param name="sender">イベントの送信元オブジェクト.</param>
    /// <param name="e">イベント引数.</param>
    private async void MainForm_Load(object sender, EventArgs e)
    {
        try
        {
            await deck.ShuffleAsync();
        }
        catch (Exception ex)
        {
            MessageBox.Show($"シャッフル中にエラーが発生しました: {ex.Message}");
        }
    }

    /// <summary>
    /// 「カードを引く」ボタンのクリックイベント処理です.
    /// </summary>
    /// <param name="sender">イベントの送信元オブジェクト.</param>
    /// <param name="e">イベント引数.</param>
    private void btnDrawCard_Click(object sender, EventArgs e)
    {
        var card = deck.DrawCard();
        if (card != null)
        {
            player.AddCardToHand(card);
            lblCardsRemaining.Text = $"残りのカード枚数: {deck.CardsRemaining()}";
            cardControl.SetCard(card); // UserControl を利用してカード表示
        }
        else
        {
            lblCardsRemaining.Text = "カードがもうありません";
            cardControl.SetCard(null);
        }
    }

    /// <summary>
    /// 「手札を表示」ボタンのクリックイベント処理です.
    /// </summary>
    /// <param name="sender">イベントの送信元オブジェクト.</param>
    /// <param name="e">イベント引数.</param>
    private void btnShowHand_Click(object sender, EventArgs e)
    {
        player.ShowHand(listBoxHand);
    }
}

変更点のまとめ(Step 9: 非同期処理の導入)

  1. UI の応答性の維持
  • 変更前はコンストラクタ内で deck.ShuffleAsync() を呼び出していたため、シャッフル処理が完了する前にフォームが表示される恐れがあり、UI の応答性に影響が出る可能性がありました。
  • 変更後はフォームの Load イベントで await deck.ShuffleAsync() を呼び出すことで、処理完了を待ちつつも UI スレッドはブロックされず、スムーズなユーザー体験を実現しています。
  1. エラーハンドリングの強化
  • 非同期処理中に例外が発生した場合、try-catch ブロックを利用して適切にエラーメッセージを表示するようにしています。
  1. 非同期処理の適切な呼び出し
  • 非同期メソッドはコンストラクタではなく、フォームのロード時に呼び出すことで、フォーム初期化後のタイミングで非同期処理を安全に実行できるようにしています。

これらの変更により、非同期処理の導入が適切に行われ、UI の応答性やユーザーエクスペリエンス、拡張性が向上します。

  • Step 10: Unity での再現例(応用編)
    担当:メンバーD
    • Unity 用の各クラス(Suit, Card, Deck, CardDisplay, GameController)のコード例を用意し、WinForm の設計思想を Unity に展開する。
      ※ Unity 用コード例は、必要に応じた別資料またはブランチで管理します。

5. チーム開発:GitHub Desktop 活用とタスク担当

GitHub Desktop の活用方法

  • リポジトリ作成
    GitHub 上でプロジェクトリポジトリを作成し、各メンバーがクローンします。
  • ブランチ運用
    各タスクごとに以下のようなブランチを作成します:
    • feature/card-class(メンバーA担当)
    • feature/deck-class(メンバーB担当)
    • feature/player-class(メンバーC担当)
    • feature/mainform-ui(メンバーD担当)
    • feature/cardcontrol(メンバーC担当)
    • feature/suit-enum(メンバーA担当)
    • feature/async-shuffle(メンバーB担当)
    • feature/hand-class(メンバーC担当)
    • feature/unity-example(メンバーD担当)
  • コミットとプルリクエスト
    各自が作業内容をこまめにコミットし、定期的にプルリクエストを発行。チーム内でコードレビューを実施し、統合前にフィードバックを共有します。

タスク担当のまとめ

  • メンバーA(基本設計担当)
  • 担当タスク:
    • Step 1: Card クラスの作成
    • Step 7: Suit 列挙型の導入
  • メンバーB(デッキ機能担当)
  • 担当タスク:
    • Step 2: Deck クラスの作成
    • Step 9: 非同期処理(ShuffleAsync)の導入
  • メンバーC(プレイヤー・拡張担当)
  • 担当タスク:
    • Step 3: Player クラスの作成
    • Step 8: Hand クラスの導入
    • Step 5: UserControl(CardControl)の作成
  • メンバーD(UI担当・応用担当)
  • 担当タスク:
    • Step 4: MainForm UI の作成
    • Step 10: Unity での再現例の作成

6. まとめと今後の課題

  • 基本実装(ステップ1~4)
    Card、Deck、Player、MainForm を実装し、WinForm 上でカードの生成、シャッフル、表示ができる状態に到達しました。
  • UserControl 拡張(ステップ5)
    カード表示の再利用性を向上させるため、UserControl を導入しました。
  • リファクタリング&応用(ステップ6~10)
    列挙型の導入、責務分離(Hand クラス)、非同期処理、さらに Unity での再現例など、より実践的な技術を学び、プロジェクトの完成度を高めます。
  • チーム開発の効果
    GitHub Desktop を活用したブランチ管理、コミット、プルリクエスト、コードレビューにより、実際の開発現場で必要なスキルを習得できます。

この資料に沿って、まずは基本機能(ステップ1~4)を各担当メンバーが平行して開発してください。次に、拡張機能(ステップ5)や応用編(ステップ6~10)に進むことで、プロジェクト全体を段階的に完成させましょう。
各タスクの進捗や課題は、GitHub の Issues や定例ミーティングで共有し、効率的に平行開発を進めてください。


この最終版資料には、XML ドキュメントコメント付きのコード例と実行時イメージを最初に掲載することで、初学者にもわかりやすい流れとなっています。チーム全体でこの流れを参考に、効率よくプロジェクトを進めましょう。