デッキ構築カードゲームの作り方#4|山札・手札・捨て札をListで作ろう

ターン終了ボタンを押すたびに、手札の5枚が入れかわり、左下の山札と右下の捨て札の数字が変わる

デッキ構築ゲームの中心にあるのが、「山札から引いて、使ったら捨て札へ、山札がなくなったら捨て札を混ぜて戻す」というカードの流れです。今回は、この山札・手札・捨て札の3つの置き場を、Listで作ってみたいと思います!ターン終了ボタンを押すたびに手札が入れかわり、山札がなくなると捨て札がシャッフルされて戻ってくるところまで作ります。

目次

今回作るもの

  • 最初のデッキ(定規でペシッ×5・下じきガード×4・ばんそうこう×1 の10枚)
  • 山札をシャッフルして、5枚引いて手札にする
  • ターン終了ボタンで、手札を捨て札へ送って、新しく5枚引く
  • 山札がなくなったら、捨て札をシャッフルして山札に戻す
  • 山札と捨て札の枚数の表示
スクリプト役割付ける場所
Deck山札・手札・捨て札のListと、引く・捨てる・シャッフルDeck(新しく作る空のオブジェクト)

手札は、今回は横一列に並べるだけにします。扇形に並べるのは#5です。

前回の続きから

前回の最後に、確認用に並べた3枚のカードを消しておきます。

  • HierarchyのHandAreaの下にあるCard・Card (1)・Card (2)を選んで、Deleteキーで消す
    • ProjectウインドのPrefabsにあるCardのプレハブは消さないでください(今回、スクリプトからこのプレハブを作ります)

3つの置き場を考えよう

デッキ構築ゲームのカードは、いつも3つの置き場のどこかにあります。

山札・手札・捨て札の流れ。山札から引いて手札に、手札を捨てて捨て札に、山札がなくなったら捨て札をシャッフルして山札に戻す
置き場中身数の変わり方
山札これから引くカード(裏向き)引くと減る。なくなったら捨て札から戻る
手札今使えるカード引くと増えて、捨てると減る
捨て札使った・捨てたカード捨てると増えて、山札に戻すと0になる

カードの枚数は置き場ごとに変わるので、3つともListで持ちます。Listの基本は6-13 数が変わるデータを扱おう:Listで紹介しています。

  • 3つともList<CardData>(#2で作ったカードのデータのリスト)
  • カードを「移す」ときは、元のListからRemoveして、先のListにAddする
  • 同じカード(定規でペシッ)を5枚入れたいときは、同じCardDataを5回Addすればいい

Deckスクリプトを作ろう

  • DeckBuilding/Scriptsに「Deck」スクリプトを作る
using System.Collections.Generic;
using TMPro;
using UnityEngine;

// 山札・手札・捨て札を List で持って、カードを引いたり捨てたりする
public class Deck : MonoBehaviour
{
    // 最初のデッキの1行分(どのカードを何枚入れるか)
    [System.Serializable]
    public class DeckEntry
    {
        public CardData card;
        [Min(1)] public int count = 1;
    }

    [Header("最初のデッキ")]
    [SerializeField] private List<DeckEntry> startingDeck = new List<DeckEntry>();

    [Header("表示")]
    [SerializeField] private CardView cardPrefab;               // 手札に作るカード(#3のプレハブ)
    [SerializeField] private RectTransform handArea;            // 手札を並べる場所
    [SerializeField] private TextMeshProUGUI drawPileText;      // 山札の枚数
    [SerializeField] private TextMeshProUGUI discardPileText;   // 捨て札の枚数
    [SerializeField] private float cardSpacing = 230f;          // 手札のカードの間隔

    [Header("確かめるとき用")]
    [SerializeField] private int randomSeed = 0; // 0以外にすると、毎回同じ順番に混ざる

    private List<CardData> drawPile = new List<CardData>();    // 山札
    private List<CardData> hand = new List<CardData>();        // 手札
    private List<CardData> discardPile = new List<CardData>(); // 捨て札
    private List<CardView> handViews = new List<CardView>();   // 手札のカードの見た目

    private void Start()
    {
        if (randomSeed != 0)
        {
            Random.InitState(randomSeed);
        }

        // 最初のデッキを、枚数ぶん山札に入れる
        foreach (DeckEntry entry in startingDeck)
        {
            for (int i = 0; i < entry.count; i++)
            {
                drawPile.Add(entry.card);
            }
        }
        Shuffle(drawPile);
        Draw(5);
    }

    // 山札から count 枚引いて、手札にする
    public void Draw(int count)
    {
        for (int i = 0; i < count; i++)
        {
            if (drawPile.Count == 0)
            {
                if (discardPile.Count == 0)
                {
                    break; // 山札にも捨て札にもカードが無い
                }
                RefillFromDiscard();
            }

            // 山札の一番うしろ(上)から1枚取る
            int last = drawPile.Count - 1;
            CardData card = drawPile[last];
            drawPile.RemoveAt(last);
            hand.Add(card);

            CardView view = Instantiate(cardPrefab, handArea);
            view.Setup(card);
            handViews.Add(view);
        }
        LayoutHand();
        UpdateCounts();
    }

    // 手札を全部、捨て札へ送る
    public void DiscardHand()
    {
        discardPile.AddRange(hand);
        hand.Clear();
        foreach (CardView view in handViews)
        {
            Destroy(view.gameObject);
        }
        handViews.Clear();
        UpdateCounts();
    }

    // 捨て札をシャッフルして、山札に戻す
    private void RefillFromDiscard()
    {
        drawPile.AddRange(discardPile);
        discardPile.Clear();
        Shuffle(drawPile);
        Debug.Log($"山札がなくなったので、捨て札{drawPile.Count}枚をシャッフルして山札に戻した");
    }

    // リストの順番をランダムに入れかえる(Fisher-Yates のシャッフル)
    private void Shuffle(List<CardData> cards)
    {
        for (int i = cards.Count - 1; i > 0; i--)
        {
            int j = Random.Range(0, i + 1);
            CardData temp = cards[i];
            cards[i] = cards[j];
            cards[j] = temp;
        }
    }

    // 手札を横一列に並べる(#5で扇形にする)
    private void LayoutHand()
    {
        float left = -(handViews.Count - 1) * cardSpacing / 2f;
        for (int i = 0; i < handViews.Count; i++)
        {
            RectTransform rt = (RectTransform)handViews[i].transform;
            rt.anchoredPosition = new Vector2(left + i * cardSpacing, 0f);
        }
    }

    // 山札と捨て札の枚数を表示する
    private void UpdateCounts()
    {
        drawPileText.text = $"山札\n{drawPile.Count}"; // \n は改行
        discardPileText.text = $"捨て札\n{discardPile.Count}";
    }
}

少し長いので、部分ごとに見ていきましょう。

最初のデッキ(DeckEntry)

  • [System.Serializable]のクラス
    • 「カード」と「枚数」を1組にした、最初のデッキの1行分。Inspectorで行ごとに設定できる
    • 定規でペシッを5枚入れるのに、同じものを5回ドラッグしなくて済む

6-13の記事ではusing System;を書いて[Serializable]としていましたが、今回は[System.Serializable]と書いています。using System;を書くと、C#のSystem.RandomとUnityのRandomの2つが見えるようになり、Random.Rangeがどちらのものか分からないというエラーになるためです。

  • Start
    • 1行ずつ、枚数の回数だけ山札にAddして、シャッフルしてから5枚引く

シャッフル

トランプを混ぜるように、リストの順番をランダムに入れかえます。

  • 一番うしろ(i)から順に、「0〜iのどれか(j)」と入れかえていく
    • Random.Range(0, i + 1)は、0からiまでの整数(int版のRandom.Rangeは、うしろの数を含まない)
    • 入れかえは、tempにいったんよけておいてから行う
  • この混ぜ方はFisher-Yates(フィッシャー・イェーツ)のシャッフルと呼ばれていて、どの並びも同じ確率で出てきます

「適当な2枚を入れかえるのを何回かくり返す」混ぜ方だと、混ざり方にかたよりが出ることがあります。カードゲームのように何百回も混ぜるものは、Fisher-Yatesを使っておくと安心です。

引く(Draw)

  • 山札の一番うしろを「山札の上」として取る
    • リストの一番うしろは、RemoveAtしてもほかのカードの番号がずれないので、手軽に取り出せます
  • 引いたカードはhandにAddして、#3のプレハブをInstantiateで作り、Setupでそのカードの見た目にする
  • Instantiate(cardPrefab, handArea)のように2つ目に親を渡すと、HandAreaの子として作られる

捨て札と、山札がなくなったとき

  • DiscardHand
    • 手札を全部捨て札にAddRangeして、手札を空にする。カードの見た目もDestroyで消す
  • RefillFromDiscard
    • 引こうとしたときに山札が0枚なら、捨て札を全部山札に戻して、シャッフルし直す
    • 山札にも捨て札にも1枚も無いときは、それ以上引かずにやめる(breakでforを抜ける。これが無いと、無いカードを取ろうとしてエラーになります)
スライム

山札がなくなったら、ちゃんと混ぜ直してくれるんですね・・・

フェアリー

そう、これがデッキ構築ゲームの「デッキが回る」感じ!新しく手に入れたカードも、このサイクルで手札に入ってくるよ

確かめるとき用のシード

  • Random.InitState(randomSeed)
    • 乱数の「種(シード)」を決めると、そのあとのRandom.Rangeが毎回同じ順番で出てくる
    • Random Seedを0以外(たとえば1)にすると、何度再生しても同じ順番にシャッフルされるので、同じ条件で何度も確かめられます
    • ふだんは0のまま(毎回ちがう順番)にしておきます

山札と捨て札の表示を置こう

枚数を出す場所を、手札の場所の左右に作ります。Hudの子に作るので、カードに隠れません。

  • Hudの子にDrawPile(UI (Canvas)>Image)
    • アンカー:bottom-left、Pos X:130、Pos Y:160、Width:130、Height:170
    • Source Image:UISprite、Image Type:Sliced、Color:#6B4E3D(カードの裏のような茶色)
  • DrawPileの子に Text – TextMeshPro
    • アンカー:stretch-stretch、Left・Top・Right・Bottom:0
    • テキスト:「山札」と入力して改行し、2行目に「10」。Font Size:30、Alignment:上下左右とも中央、Color:白
    • 数字はスクリプトが書きかえるので、ここは仮の文字です
  • DrawPileを複製してDiscardPileにする
    • アンカー:bottom-right、Pos X:-130、Pos Y:160
    • Color:#9A9A9A(灰色)、テキスト:「捨て札」と改行して「0」
Hudの下にDrawPileとDiscardPile。Gameビューの左下に茶色の山札10、右下に灰色の捨て札0

Deckを置いて、つなごう

  • Hierarchyで右クリック>Create Emptyで「Deck」を作り、Deckスクリプトを付ける
  • Inspectorで次のように設定する
項目値
Starting Deck+で3行にして、Ruler:5、PencilBoard:4、Bandage:1
Card PrefabPrefabs>Card
Hand AreaHandArea
Draw Pile TextDrawPileの子のText (TMP)
Discard Pile TextDiscardPileの子のText (TMP)
Card Spacing230
Random Seed0
DeckのInspector。Starting DeckにRuler・5、PencilBoard・4、Bandage・1の3行、Card PrefabにCard、Hand AreaにHandArea、Card Spacingが230、Random Seedが0

最後に、ターン終了ボタンに「手札を捨てて、5枚引く」をつなぎます。ボタンの基本は9-5 入力系UIで紹介しています。

  • HierarchyでEndTurnButtonを選び、ButtonのOn Click ()の+を2回押して、欄を2つにする
  • 1つ目:Deckをドラッグして、Deck > DiscardHand ()
  • 2つ目:Deckをドラッグして、Deck > Draw (int)、数字の欄に5
EndTurnButtonのOn Click ()。1つ目がDeck.DiscardHand、2つ目がDeck.Drawで5

On Click ()の欄は、上から順に実行されます。DiscardHandを先に、Drawをあとにしておかないと、引いたばかりのカードまで捨ててしまいます。

このボタンのつなぎ方は仮のものです。#9でターンの流れを作るときに、スクリプトから呼ぶ形に変えます。

動かして確認!

再生して、ターン終了ボタンを何回か押してみましょう。

再生した画面。手札に5枚、左下に山札5、右下に捨て札0
押した回数山札手札捨て札起きること
0(再生した直後)55010枚をシャッフルして5枚引く
1055手札5枚が捨て札へ、残りの5枚を引く
2550手札5枚が捨て札へ。山札が0なので、捨て札10枚をシャッフルして戻してから5枚引く
Consoleに「山札がなくなったので、捨て札10枚をシャッフルして山札に戻した」

#4の動作確認

  • 再生すると、5枚のカードが横に並ぶ。山札5・捨て札0
  • ターン終了ボタンを押すたびに、手札が入れかわる
  • 2回目に押したとき、Consoleに「捨て札10枚をシャッフルして山札に戻した」が出る
  • 何回押しても、山札+手札+捨て札はいつも10枚
  • Random Seedを1にすると、何度再生しても同じ5枚から始まる

カードの枚数は、どこかで増えたり消えたりしていないかを「合計が10枚のまま」で確かめられます。3つの置き場のあいだでカードを移すだけにしておくと、合計がずれないので、バグに気づきやすくなります。

あとがき

今回は、山札・手札・捨て札の3つのListでカードを回す、デッキ構築ゲームの心臓部分を作りました。見た目はまだ横一列ですが、ボタンを押すたびにカードが入れかわるだけでも、ちょっとワクワクしてきますね。次回は、手札を扇形に並べて、1枚ずつ配る動きと、マウスを乗せたときにふわっと持ち上がる動きを付けたいと思います!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメント一覧 (1件)

コメントする

目次