WinFormアプリでのカードゲーム基盤の作成とUserControlの活用

この技術資料では、WinFormアプリケーションを使って基本的なカードゲームの基盤を構築する方法を解説します。さらに、UserControlを用いてカードの表示や操作を再利用可能で管理しやすい形で実装する方法も紹介します。すべてのテキスト表示は日本語で行い、カードの画像を表示する方法も含めて説明します。

基本的な実装

1. カードクラスの実装

カードクラスは、カードのスート(ハート、ダイヤ、クラブ、スペード)と値(1〜13)を表します。また、各カードに対応する画像のパスを保持します。

public class Card
{
    public string Suit { get; private set; }
    public int Value { get; private set; }
    public string ImagePath { get; private set; }

    public Card(string suit, int value, string imagePath)
    {
        Suit = suit;
        Value = value;
        ImagePath = imagePath;
    }

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

2. デッキクラスの実装

デッキクラスは、カードの山を管理します。このクラスは、カードをシャッフルしたり、カードを引く機能を提供します。カードのスートは日本語で表現され、カード画像はimagesフォルダに保存されていると仮定します。

public class Deck
{
    private List<Card> cards;

    public Deck()
    {
        cards = new List<Card>();
        string[] suits = { "ハート", "ダイヤ", "クラブ", "スペード" };

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

    public void Shuffle()
    {
        Random rand = new Random();
        cards = cards.OrderBy(c => rand.Next()).ToList();
    }

    public Card DrawCard()
    {
        if (cards.Count == 0)
            return null;

        var card = cards[0];
        cards.RemoveAt(0);
        return card;
    }

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

3. プレイヤークラスの実装

プレイヤークラスは、プレイヤーの名前と手札を管理します。手札のカードを表示する機能も提供します。

public class Player
{
    public string Name { get; private set; }
    private List<Card> hand;

    public Player(string name)
    {
        Name = name;
        hand = new List<Card>();
    }

    public void AddCardToHand(Card card)
    {
        if (card != null)
        {
            hand.Add(card);
        }
    }

    public void ShowHand(ListBox listBox)
    {
        listBox.Items.Clear();
        foreach (var card in hand)
        {
            listBox.Items.Add(card.ToString());
        }
    }
}

4. WinFormアプリケーションでのフォームデザイン

フォーム上に配置する要素は以下の通りです。

  • PictureBox (pictureBoxCard): 引いたカードの画像を表示。
  • ListBox (listBoxHand): プレイヤーの手札を表示。
  • Button (btnDrawCard): 「カードを引く」ボタン。
  • Button (btnShowHand): 「手札を表示」ボタン。
  • Label (lblCardsRemaining): 残りのカードの枚数を表示するラベル。

5. PictureBoxを使用したカード画像の表示

PictureBoxを使用して、引いたカードの画像を表示します。以下のコードでは、カードを引くたびにPictureBoxにカードの画像が表示され、残りのカード枚数が日本語で表示されます。

public partial class MainForm : Form
{
    private Deck deck;
    private Player player;

    public MainForm()
    {
        InitializeComponent();
        deck = new Deck();
        player = new Player("プレイヤー1");
        deck.Shuffle();
    }

    private void btnDrawCard_Click(object sender, EventArgs e)
    {
        var card = deck.DrawCard();
        if (card != null)
        {
            player.AddCardToHand(card);
            lblCardsRemaining.Text = $"残りのカード枚数: {deck.CardsRemaining()}";

            // PictureBoxにカードの画像を表示
            pictureBoxCard.Image = Image.FromFile(card.ImagePath);
        }
        else
        {
            lblCardsRemaining.Text = "カードがもうありません";
        }
    }

    private void btnShowHand_Click(object sender, EventArgs e)
    {
        player.ShowHand(listBoxHand);
    }
}

6. UserControlを使用したカード表示の改善

UserControlを使用することで、カードの表示や操作をより再利用可能で管理しやすい形に改善できます。ここでは、カードを表示するためのUserControlを作成し、フォームで使用する方法を説明します。

6.1 CardControlの作成

CardControlは、カードの画像とテキストを表示するカスタムコントロールです。

public partial class CardControl : UserControl
{
    public CardControl()
    {
        InitializeComponent();
    }

    public void SetCard(Card card)
    {
        if (card != null)
        {
            pictureBoxCard.Image = Image.FromFile(card.ImagePath);
            labelCardText.Text = $"{card.Suit} の {card.Value}";
        }
        else
        {
            pictureBoxCard.Image = null;
            labelCardText.Text = string.Empty;
        }
    }
}

6.2 フォームでのCardControlの使用

フォームにCardControlを配置し、カードの表示に使用します。

public partial class MainForm : Form
{
    private Deck deck;
    private Player player;

    public MainForm()
    {
        InitializeComponent();
        deck = new Deck();
        player = new Player("プレイヤー1");
        deck.Shuffle();
    }

    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.SetCard(null);  // カードがない場合、表示をクリア
        }
    }

    private void btnShowHand_Click(object sender, EventArgs e)
    {
        player.ShowHand(listBoxHand);
    }
}

7. サンプルコードのまとめ

この資料では、WinFormアプリでカードゲームの基盤を作成するための基本的な手順と、UserControlを使用した改善方法を説明しました。UserControlを使うことで、アプリケーションの再利用性とメンテナンス性を高めることができ、よりモジュール化された、拡張性の高いアプリケーションを構築することが可能になります。


以上で、WinFormアプリケーションにおけるカードゲーム基盤の作成方法と、UserControlの活用方法についての技術資料を終わります。この手法を基に、さらに複雑なゲームシステムやUIを追加し、フル機能のカードゲームを作成していくことができます。

Cardクラスの画像情報をstring型からImage型にしてみると

Cardクラスの画像をファイルパスではなく、Imageクラスで直接保持するようにすると、次のようなメリットがあります。

メリット:

  1. 効率的な画像管理: ファイルパスを使う代わりにImageオブジェクトを直接保持することで、画像の読み込みやキャッシュが効率的に行えます。
  2. 柔軟な操作Imageクラスを使用することで、画像のサイズ変更や変換など、より柔軟な操作が可能になります。

以下は、CardクラスにImageクラスを使用した例です。

1. カードクラスの修正

Cardクラスの画像プロパティをImage型に変更します。また、カードの画像を読み込む部分もImage.FromFileで直接読み込むようにします。

public class Card
{
    public string Suit { get; private set; }
    public int Value { get; private set; }
    public Image CardImage { get; private set; }

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

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

2. デッキクラスの修正

デッキを作成する際に、各カードに対応する画像を読み込み、CardクラスのImageプロパティに設定します。

public class Deck
{
    private List<Card> cards;

    public Deck()
    {
        cards = new List<Card>();
        string[] suits = { "ハート", "ダイヤ", "クラブ", "スペード" };

        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));
            }
        }
    }

    public void Shuffle()
    {
        Random rand = new Random();
        cards = cards.OrderBy(c => rand.Next()).ToList();
    }

    public Card DrawCard()
    {
        if (cards.Count == 0)
            return null;

        var card = cards[0];
        cards.RemoveAt(0);
        return card;
    }

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

3. CardControlの修正

CardControlで画像を設定する際に、Imageプロパティを使用して画像を表示します。

public partial class CardControl : UserControl
{
    public CardControl()
    {
        InitializeComponent();
    }

    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;
        }
    }
}

4. フォームの修正

DeckクラスやCardControlの修正に伴い、フォーム側のコードに変更は必要ありません。これまで通りにCardオブジェクトをCardControlに渡すだけで、画像が正しく表示されます。


結論

CardクラスにImageプロパティを使用することで、画像の扱いがより柔軟かつ効率的になります。これにより、画像の管理が簡単になり、UIのパフォーマンス向上が期待できます。この変更により、カードゲームの基盤がさらに強化され、開発者にとっても扱いやすい構造となります。

Suitは、列挙型にしたらどうでしょうか?

Suitを列挙型に変更することで、コードの安全性と可読性が向上します。列挙型を使用すると、スートの値が制約され、無効なスートが使用されることを防ぐことができます。また、スートに対する操作がより簡単に行えるようになります。

以下に、Suitを列挙型にした場合の例を示します。

1. Suit列挙型の定義

まず、Suit列挙型を定義します。これにより、スート(ハート、ダイヤ、クラブ、スペード)を列挙型として管理します。

public enum Suit
{
    Hearts,   // ハート
    Diamonds, // ダイヤ
    Clubs,    // クラブ
    Spades    // スペード
}

2. Cardクラスの修正

次に、Cardクラスでスートを列挙型として扱うように修正します。

public class Card
{
    public Suit Suit { get; private set; }
    public int Value { get; private set; }
    public Image CardImage { get; private set; }

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

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

    private string GetSuitName(Suit suit)
    {
        switch (suit)
        {
            case Suit.Hearts: return "ハート";
            case Suit.Diamonds: return "ダイヤ";
            case Suit.Clubs: return "クラブ";
            case Suit.Spades: return "スペード";
            default: return "不明";
        }
    }
}

3. Deckクラスの修正

Deckクラスでも、列挙型Suitを使用してカードを生成します。

public class Deck
{
    private List<Card> cards;

    public Deck()
    {
        cards = new List<Card>();

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

    public void Shuffle()
    {
        Random rand = new Random();
        cards = cards.OrderBy(c => rand.Next()).ToList();
    }

    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. CardControlの修正

CardControlの設定部分は、スートの名前を取得する際にGetSuitNameメソッドを使用します。

public partial class CardControl : UserControl
{
    public CardControl()
    {
        InitializeComponent();
    }

    public void SetCard(Card card)
    {
        if (card != null)
        {
            pictureBoxCard.Image = card.CardImage;
            labelCardText.Text = $"{card.GetSuitName(card.Suit)} の {card.Value}";
        }
        else
        {
            pictureBoxCard.Image = null;
            labelCardText.Text = string.Empty;
        }
    }
}

5. メリット

  • タイプセーフティ: 列挙型を使用することで、無効なスート値が設定されるのを防げます。
  • コードの可読性向上: 列挙型は定数よりも意味が明確で、コードの可読性が向上します。
  • 管理のしやすさ: 新しいスートを追加する際も、列挙型に追加するだけで済むため、コードのメンテナンスが容易です。

結論

Suitを列挙型に変更することで、コードの安全性、可読性、およびメンテナンス性が向上します。これは、特に複雑なアプリケーションでのバグの防止や、コードの理解を助けるために非常に有効です。この変更により、カードゲームの基盤がさらに堅牢で扱いやすいものになります。

更なるリファクタリング

Suitを列挙型に変更し、Imageクラスを使用するなどのリファクタリングを行いましたが、さらにコードを改善するためのリファクタリングの候補を以下に示します。

1. カードの値を列挙型に変更

カードの値を整数(1〜13)ではなく、列挙型にすることで、カードの扱いがより明確になります。例えば、「エース」「キング」「クイーン」などの特定のカード値に名前を付けることができます。

1.1 CardValue列挙型の定義

public enum CardValue
{
    Ace = 1, // エース
    Two,
    Three,
    Four,
    Five,
    Six,
    Seven,
    Eight,
    Nine,
    Ten,
    Jack,   // ジャック
    Queen,  // クイーン
    King    // キング
}

1.2 Cardクラスの修正

public class Card
{
    public Suit Suit { get; private set; }
    public CardValue Value { get; private set; }
    public Image CardImage { get; private set; }

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

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

    private string GetSuitName(Suit suit)
    {
        switch (suit)
        {
            case Suit.Hearts: return "ハート";
            case Suit.Diamonds: return "ダイヤ";
            case Suit.Clubs: return "クラブ";
            case Suit.Spades: return "スペード";
            default: return "不明";
        }
    }

    private string GetCardValueName(CardValue value)
    {
        switch (value)
        {
            case CardValue.Ace: return "エース";
            case CardValue.Jack: return "ジャック";
            case CardValue.Queen: return "クイーン";
            case CardValue.King: return "キング";
            default: return ((int)value).ToString();
        }
    }
}

2. Deckクラスのカプセル化

Deckクラスの操作をより明確にし、内部構造を隠すために、カードのリストを公開せず、メソッドを通じてのみ操作できるようにします。

public class Deck
{
    private readonly List<Card> cards;

    public Deck()
    {
        cards = new List<Card>();

        foreach (Suit suit in Enum.GetValues(typeof(Suit)))
        {
            foreach (CardValue value in Enum.GetValues(typeof(CardValue)))
            {
                string imagePath = $"images/{suit.ToString().ToLower()}_{(int)value}.png";
                Image cardImage = Image.FromFile(imagePath);
                cards.Add(new Card(suit, value, cardImage));
            }
        }
    }

    public void Shuffle()
    {
        Random rand = new Random();
        cards = cards.OrderBy(c => rand.Next()).ToList();
    }

    public Card DrawCard()
    {
        if (cards.Count == 0)
            return null;

        var card = cards[0];
        cards.RemoveAt(0);
        return card;
    }

    public int CardsRemaining => cards.Count;
}

3. Playerクラスのシングルリスポンシビリティの原則に従った分離

Playerクラスが手札の表示や管理の責務を持っているのは少し多すぎるかもしれません。手札の表示は別のクラスに分離し、Playerクラスは手札の管理だけに集中させることができます。

3.1 Handクラスの作成

public class Hand
{
    private readonly List<Card> cards;

    public Hand()
    {
        cards = new List<Card>();
    }

    public void AddCard(Card card)
    {
        if (card != null)
        {
            cards.Add(card);
        }
    }

    public void ShowHand(ListBox listBox)
    {
        listBox.Items.Clear();
        foreach (var card in cards)
        {
            listBox.Items.Add(card.ToString());
        }
    }

    public int CardCount => cards.Count;
}

3.2 Playerクラスの修正

ublic class Player
{
    public string Name { get; private set; }
    public Hand Hand { get; private set; }

    public Player(string name)
    {
        Name = name;
        Hand = new Hand();
    }
}

4. 非同期処理の導入(パフォーマンス向上)

大量の画像を読み込む場合やシャッフル操作が重い場合は、非同期処理を導入することでUIの応答性を向上させることができます。

4.1 非同期のシャッフル

public async Task ShuffleAsync()
{
    await Task.Run(() =>
    {
        Random rand = new Random();
        cards = cards.OrderBy(c => rand.Next()).ToList();
    });
}

結論

これらのリファクタリングにより、コードの可読性、保守性、拡張性がさらに向上します。特に列挙型の活用やクラスの責務の分離は、コードが成長しても管理しやすくなる重要なステップです。また、非同期処理を導入することで、パフォーマンスを向上させることも可能です。

参考)Unityで再現

Unityで同様のカードゲーム基盤を再現することは十分に可能です。以下に、Unityでの実装手順を解説します。この手順では、UnityのスクリプトでSuit列挙型やCardクラスを使用し、カードの表示をUI.Imageコンポーネントを用いて行います。

1. Suit列挙型の定義

まず、C#スクリプトでSuit列挙型を定義します。

public enum Suit
{
    Hearts,   // ハート
    Diamonds, // ダイヤ
    Clubs,    // クラブ
    Spades    // スペード
}

2. Cardクラスの実装

次に、Cardクラスを定義します。Cardクラスでは、カードのスート、値、そして画像を管理します。

using UnityEngine;

public class Card
{
    public Suit Suit { get; private set; }
    public int Value { get; private set; }
    public Sprite CardImage { get; private set; }

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

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

3. Deckクラスの実装

カードデッキを管理するDeckクラスを作成します。このクラスでは、カードの生成、シャッフル、カードを引く機能を実装します。

using System.Collections.Generic;
using UnityEngine;

public class Deck
{
    private List<Card> cards;

    public Deck()
    {
        cards = new List<Card>();

        foreach (Suit suit in System.Enum.GetValues(typeof(Suit)))
        {
            for (int i = 1; i <= 13; i++)
            {
                string imagePath = $"Images/{suit.ToString().ToLower()}_{i}";
                Sprite cardImage = Resources.Load<Sprite>(imagePath);
                cards.Add(new Card(suit, i, cardImage));
            }
        }
    }

    public void Shuffle()
    {
        for (int i = 0; i < cards.Count; i++)
        {
            Card temp = cards[i];
            int randomIndex = Random.Range(i, cards.Count);
            cards[i] = cards[randomIndex];
            cards[randomIndex] = temp;
        }
    }

    public Card DrawCard()
    {
        if (cards.Count == 0)
            return null;

        var card = cards[0];
        cards.RemoveAt(0);
        return card;
    }

    public int CardsRemaining => cards.Count;
}

4. カードの表示 (CardDisplayスクリプト)

UnityのUI.Imageを使ってカードを表示するためのスクリプトを作成します。

using UnityEngine;
using UnityEngine.UI;

public class CardDisplay : MonoBehaviour
{
    public Image cardImage;
    public Text cardText;

    public void SetCard(Card card)
    {
        if (card != null)
        {
            cardImage.sprite = card.CardImage;
            cardText.text = card.ToString();
        }
        else
        {
            cardImage.sprite = null;
            cardText.text = "";
        }
    }
}

5. Unityシーンの設定

  1. UI作成:
    • Canvas: シーンにCanvasを追加し、UI要素の親として使用します。
    • Image: Canvas内にImageコンポーネントを持つオブジェクトを配置し、これがカードの画像を表示する場所になります。
    • Text: カードのスートと値を表示するためにTextコンポーネントを追加します。
  2. スクリプトの設定:
    • CardDisplayスクリプトをImageオブジェクトにアタッチします。
    • CardDisplaycardImageには、Imageコンポーネントを割り当て、cardTextにはTextコンポーネントを割り当てます。
  3. デッキの作成とシャッフル:
    • Deckオブジェクトを作成し、シャッフル後にカードを引いて、CardDisplayに渡します。
public class GameController : MonoBehaviour
{
    public CardDisplay cardDisplay;

    private Deck deck;

    void Start()
    {
        deck = new Deck();
        deck.Shuffle();
        DrawNextCard();
    }

    public void DrawNextCard()
    {
        Card card = deck.DrawCard();
        cardDisplay.SetCard(card);
    }
}

6. カードの画像リソース準備

Resources/Imagesフォルダを作成し、カード画像をhearts_1.pngdiamonds_2.pngなどの名前で配置します。これにより、Resources.Load<Sprite>(imagePath)を使用して画像を読み込めます。

7. シーンの動作確認

Unityエディタで再生ボタンを押してシーンを動作させ、ボタンをクリックするたびにカードが表示されることを確認します。


結論

この手順に従って、Unityで基本的なカードゲーム基盤を構築できます。Suitを列挙型として、CardクラスにSpriteを使用し、カード画像を表示することで、C#の基本的なオブジェクト指向の概念をUnityで再現できます。この基盤を拡張して、より複雑なカードゲームを作成することも可能です。