
デッキ構築ゲームの中心にあるのが、「山札から引いて、使ったら捨て札へ、山札がなくなったら捨て札を混ぜて戻す」というカードの流れです。今回は、この山札・手札・捨て札の3つの置き場を、Listで作ってみたいと思います!ターン終了ボタンを押すたびに手札が入れかわり、山札がなくなると捨て札がシャッフルされて戻ってくるところまで作ります。
- シリーズの目次:デッキ構築カードゲームの作り方
- 前回:#3 カードの見た目をプレハブにしよう
今回作るもの
- 最初のデッキ(定規でペシッ×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回ドラッグしなくて済む
- 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でそのカードの見た目にする- プレハブを作る方法は、7-3 スクリプトからプレハブを生成で紹介しています
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」


Deckを置いて、つなごう
- Hierarchyで右クリック>Create Emptyで「Deck」を作り、Deckスクリプトを付ける
- Inspectorで次のように設定する
| 項目 | 値 |
|---|---|
| Starting Deck | +で3行にして、Ruler:5、PencilBoard:4、Bandage:1 |
| Card Prefab | Prefabs>Card |
| Hand Area | HandArea |
| Draw Pile Text | DrawPileの子のText (TMP) |
| Discard Pile Text | DiscardPileの子のText (TMP) |
| 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


このボタンのつなぎ方は仮のものです。#9でターンの流れを作るときに、スクリプトから呼ぶ形に変えます。
動かして確認!
再生して、ターン終了ボタンを何回か押してみましょう。


| 押した回数 | 山札 | 手札 | 捨て札 | 起きること |
|---|---|---|---|---|
| 0(再生した直後) | 5 | 5 | 0 | 10枚をシャッフルして5枚引く |
| 1 | 0 | 5 | 5 | 手札5枚が捨て札へ、残りの5枚を引く |
| 2 | 5 | 5 | 0 | 手札5枚が捨て札へ。山札が0なので、捨て札10枚をシャッフルして戻してから5枚引く |


#4の動作確認
- 再生すると、5枚のカードが横に並ぶ。山札5・捨て札0
- ターン終了ボタンを押すたびに、手札が入れかわる
- 2回目に押したとき、Consoleに「捨て札10枚をシャッフルして山札に戻した」が出る
- 何回押しても、山札+手札+捨て札はいつも10枚
- Random Seedを1にすると、何度再生しても同じ5枚から始まる
カードの枚数は、どこかで増えたり消えたりしていないかを「合計が10枚のまま」で確かめられます。3つの置き場のあいだでカードを移すだけにしておくと、合計がずれないので、バグに気づきやすくなります。
あとがき
今回は、山札・手札・捨て札の3つのListでカードを回す、デッキ構築ゲームの心臓部分を作りました。見た目はまだ横一列ですが、ボタンを押すたびにカードが入れかわるだけでも、ちょっとワクワクしてきますね。次回は、手札を扇形に並べて、1枚ずつ配る動きと、マウスを乗せたときにふわっと持ち上がる動きを付けたいと思います!
コメント
コメント一覧 (1件)
[…] 前回:#4 山札・手札・捨て札をListで作ろう […]