
ここまでで、ターンを回して敵と戦い、攻撃にも守りにも手応えが出るようになりました。でも、敵のHPを0にしても、スライムちゃんのHPが0になっても、戦いが終わりません・・・。今回は、敵が倒れて勝ち、HPが0で負けの判定と、勝ったときに報酬のカードを3枚から1枚選んでデッキに加えるところを作ってみたいと思います!
- シリーズの目次:デッキ構築カードゲームの作り方
- 前回:#11 守る・受ける・回復するときの演出
今回作るもの
- 敵が倒れる(しぼんで、弾けて消える)
- 敵を全部倒したら勝ち。最後の1体はヒットストップを長めに
- スライムちゃんのHPが0になったら負け
- 「勝利!」「まけてしまった・・・」の表示と、「もう一度」ボタン
- 勝ったら報酬のカードを3枚出して、選んだ1枚をデッキに加える
| スクリプト | 役割 | 付ける場所 |
|---|---|---|
| ResultView | 勝ち負けの文字と「もう一度」ボタン | ResultView(新しく作る) |
| RewardView | 報酬の3択 | RewardView(新しく作る) |
| EnemyView(足す) | 倒れる動き | - |
| BattleManager(足す) | 勝ち負けの判定と、その後の流れ | - |
| Deck(足す) | 報酬のカードを山札に加える | - |
次の戦いへ進むのは#14で作ります。今回は、勝っても負けても「もう一度」で同じ戦いをやり直す形です。
前回の続きから
前回の最後の状態から始めます。勝ったときのスライムちゃんの絵は、素材のzipを入れ直すと入ります。
- DeckBuildingAssets.zip
DeckBuilding/Sprites/Slime/win_01.png〜win_03.png(正面を向いて喜ぶスライムちゃん。01→02→03の順に見せる)- 自分で入れた場合は、Sprite Mode:Singleにして Apply
フォントのアトラスを増やせるようにする
今回は「報酬」「えらぼう」など、新しい文字がたくさん出てきます。#1でフォントアセットの Multi Atlas Textures にチェックを入れていない人は、ここで入れておきましょう(#1にもあとから手順を足しました)。
- Projectウインドで Fonts/NotoSansJP SDF を選ぶ
- Inspectorの Generation Settings にある Multi Atlas Textures にチェックを入れる

#1で作ったフォントアセットは、使った文字をその場で1枚の画像(アトラス、1024×1024)に書き足していく形です。日本語は文字の種類が多いので、回が進むと1枚に入りきらなくなり、「The character … was not found」という警告が出て、文字が空白になります。Multi Atlas Texturesにチェックを入れておくと、いっぱいになったときに2枚目の画像を作ってくれます。
敵が倒れる
HPが0になった敵が、横にふくらみながら縦にしぼんで、弾けて消えるようにします。EnemyViewに足します。
[SerializeField] private float stepOutSeconds = 0.25f; // 戻る時間
[Header("★#12 倒れる動き")]
[SerializeField] private float defeatDelay = 0.3f; // のけぞりを見せてから倒れるまでの時間
[SerializeField] private float shrinkSeconds = 0.25f; // しぼむ時間
StepOutの下に足します。
// ★#12 倒れる:横にふくらみながら縦にしぼんで、火花とともに消える
public IEnumerator Defeat()
{
bodyImage.raycastTarget = false; // もうねらえないようにする
intent.gameObject.SetActive(false);
yield return new WaitForSeconds(defeatDelay);
for (float time = 0f; time < shrinkSeconds; time += Time.deltaTime)
{
float t = time / shrinkSeconds;
transform.localScale = new Vector3(1f + t * 0.4f, 1f - t, 1f);
yield return null;
}
FindFirstObjectByType<EffectSpawner>().PlaySparks(transform.position); // 弾ける
Destroy(gameObject);
}
- まず0.3秒待つ
- 最後の一撃の反応(#10の白く光る・のけぞる・数字)を見せてから倒れます
- 横を1→1.4、縦を1→0に変える
- 「つぶれていく」感じが出ます
- 最後に#10の火花を出して、
Destroyで消す bodyImage.raycastTarget = false:倒れている途中の敵に、攻撃のカードを落とせないようにします
勝ち負けの表示を作ろう(ResultView)
「勝利!」「まけてしまった・・・」の文字と、「もう一度」ボタンを出す画面を作ります。
- Hudの子にResultView(UI (Canvas)>Image)
- Hudの子の一番下に置く
- アンカー:stretch-stretch、Left・Top・Right・Bottom:0
- Color:#000000、Alpha:170ぐらい(後ろを暗くして、文字を読みやすくする)
- ResultViewの子にTitle(Text – TextMeshPro)
- Pos Y:80、Width:1400、Height:200
- テキスト:勝利!、Font Size:120、Font Style:B、Alignment:中央
- Text Wrapping Mode:No Wrap(折り返さない)、Raycast Targetのチェックを外す
- ResultViewの子にRetryButton(UI (Canvas)>Button – TextMeshPro)
- Pos Y:-140、Width:320、Height:100
- 子のテキスト:もう一度、Font Size:44、Font Style:B
- DeckBuilding/Scriptsに「ResultView」スクリプトを作って、ResultViewに付ける
using System.Collections;
using TMPro;
using UnityEngine;
using UnityEngine.SceneManagement;
// 勝ち・負けの文字を、はずませて出す。「もう一度」ボタンで戦いをやり直す
public class ResultView : MonoBehaviour
{
[SerializeField] private TextMeshProUGUI title;
[SerializeField] private GameObject retryButton;
[SerializeField] private float popSeconds = 0.35f; // はずんで出るのにかける時間
private void Awake()
{
gameObject.SetActive(false); // 最初は隠しておく
}
// 文字をはずませて出す(出し終わるまで待てるように IEnumerator にしている)
public IEnumerator Show(string text, Color color)
{
gameObject.SetActive(true);
retryButton.SetActive(false);
title.text = text;
title.color = color;
for (float time = 0f; time < popSeconds; time += Time.deltaTime)
{
float t = time / popSeconds;
// 0 → 1.3 → 1:一度大きくふくらんでから、元の大きさにおさまる
float scale = t < 0.5f ? Mathf.Lerp(0f, 1.3f, t / 0.5f) : Mathf.Lerp(1.3f, 1f, (t - 0.5f) / 0.5f);
title.transform.localScale = Vector3.one * scale;
yield return null;
}
title.transform.localScale = Vector3.one;
}
public void Hide()
{
gameObject.SetActive(false);
}
// 「もう一度」ボタンを出す
public void ShowRetry()
{
gameObject.SetActive(true);
retryButton.SetActive(true);
}
// 「もう一度」ボタンから呼ぶ:今のシーンを読み込み直す
public void Retry()
{
SceneManager.LoadScene(SceneManager.GetActiveScene().name);
}
}
Awakeで自分を非表示にする- Hierarchyでは表示したままにしておきます。最初から非表示にすると
Awakeが呼ばれず、「最初は隠す」も動きません
- Hierarchyでは表示したままにしておきます。最初から非表示にすると
- はずむ動き:前半で0→1.3、後半で1.3→1
- 一度大きくふくらんでから、おさまると、ポンとはずんで出てきたように見えます
Retry:SceneManager.LoadSceneで、今のシーンを読み込み直します(8-3 スクリプトでシーンを切り替えよう)
作ったら、つなぎます。
- ResultViewのResult View:TitleにTitle、Retry ButtonにRetryButton
- RetryButtonのOn Click ():「+」で1つ足して、ResultViewをドラッグしてResultView > Retry ()

勝ちと負けの判定
BattleManagerで、勝ち負けを判定します。
ターンの状態を増やす
#9で作ったenumに、「勝ち」「負け」を足します。
public enum TurnState
{
PlayerTurn, // あなたのターン
EnemyTurn, // 敵のターン
Victory, // ★#12 勝ち
Defeat // ★#12 負け
}
EnemyTurnの後ろに,を足すのを忘れずに- 勝ち負けが決まったら、
stateをVictoryかDefeatにしますEndTurnは「PlayerTurnのときだけ」動くので、勝ち負けが決まったあとはターン終了ボタンを押しても何も起きません。#9でenumにしておいた状態を1つ足すだけで、止めたい処理が自然に止まるのがポイントです
項目を足す
[SerializeField] private float enemyActionSeconds = 0.6f; // 敵1体ごとの間
[Header("★#12 勝ち負け")]
[SerializeField] private ResultView resultView;
[SerializeField] private HitStop hitStop;
[SerializeField] private float lastHitStopSeconds = 0.3f; // 最後の1体を倒したときのヒットストップ
[SerializeField] private IdleBounce slimeIdle; // 勝ったときに止めて、勝ちのポーズにする
[SerializeField] private Sprite[] winFrames; // win_01・win_02・win_03
敵を全部倒したら勝ち
EndTurnの上にUpdateを足します。
// ★#12 倒れた敵を片付けて、全部倒したら勝ち
private void Update()
{
if (state != TurnState.PlayerTurn)
{
return;
}
// うしろから調べる(途中で RemoveAt しても、まだ調べていない番号がずれない)
for (int i = enemyParty.Views.Count - 1; i >= 0; i--)
{
EnemyView enemy = enemyParty.Views[i];
if (enemy.Fighter.Hp <= 0)
{
enemyParty.Views.RemoveAt(i);
StartCoroutine(enemy.Defeat());
}
}
if (enemyParty.Views.Count == 0)
{
hitStop.Play(lastHitStopSeconds); // 最後の1体は、長めに止める
StartCoroutine(Victory());
}
}
- 敵のHPが減るのは、あなたのターンにカードを使ったときだけなので、PlayerTurnのときだけ調べます
- HPが0の敵は、
Viewsのリストから外してから、Defeatで倒れさせる- リストから外しておくと、敵のターンで倒れた敵が行動することはありません
- リストはうしろから調べる
- 前から調べると、
RemoveAt(i)で後ろの要素が1つずつ前へずれて、すぐ次の敵を調べ忘れることがあります
- 前から調べると、
- 敵がいなくなったら、ヒットストップを0.3秒(ふだんの5倍)にして、勝ちへ
- #10のヒットストップは、あとから呼んだ方で上書きされるので、最後の一撃だけ長く止まります
StartCoroutine(Victory())を呼ぶと、Victoryの中の最初のyieldまでがすぐに動いて、stateがVictoryになります。次のフレームのUpdateではreturnするので、勝ちの処理が2回動くことはありません。
HPが0になったら負け
EnemyTurnで、敵が攻撃したあとに調べます。
yield return StartCoroutine(enemy.StepOut()); // ★#11 元の位置へ戻る
// ★#12 HP が0になったら負け
if (player.Hp <= 0)
{
StartCoroutine(Defeat());
yield break;
}
yield break:コルーチンをここで終わらせる。残りの敵は行動せず、次の「あなたのターン」にも進みません
勝ったとき・負けたときの流れ
BattleManagerの一番下に足します。
// ★#12 勝ち:勝ちのポーズと「勝利!」→ 報酬を選ぶ →「もう一度」
private IEnumerator Victory()
{
state = TurnState.Victory;
endTurnButton.interactable = false;
yield return new WaitForSeconds(1f); // 最後の敵が消えるのを待つ
deck.DiscardHand();
StartCoroutine(WinPose());
yield return StartCoroutine(resultView.Show("勝利!", new Color(1f, 0.82f, 0.25f)));
yield return new WaitForSeconds(1f);
resultView.ShowRetry();
}
// ★#12 負け:「まけてしまった・・・」→「もう一度」
private IEnumerator Defeat()
{
state = TurnState.Defeat;
endTurnButton.interactable = false;
yield return new WaitForSeconds(1f);
yield return StartCoroutine(resultView.Show("まけてしまった・・・", new Color(0.85f, 0.9f, 1f)));
resultView.ShowRetry();
}
// ★#12 勝ちのポーズ:win_01 → 02 → 03 と見せて、03 で止める
private IEnumerator WinPose()
{
slimeIdle.enabled = false;
Image slimeImage = slimeIdle.GetComponent<Image>();
foreach (Sprite frame in winFrames)
{
slimeImage.sprite = frame;
yield return new WaitForSeconds(0.15f);
}
}
- 勝ち:1秒待ってから手札を捨てて、勝ちのポーズと「勝利!」(金色)
- 報酬の3択は、次の節でここに足します
- 負け:1秒待ってから「まけてしまった・・・」(青白い色)
- 勝ちのポーズ:#11と同じように、IdleBounceを止めてから絵を切りかえる
- 最後のwin_03のまま止まります
BattleManagerのInspectorでつなぎます。
| 項目 | 入れるもの |
|---|---|
| Result View | Hud>ResultView |
| Hit Stop | Effects |
| Last Hit Stop Seconds | 0.3 |
| Slime Idle | Stage>PlayerArea>Slime |
| Win Frames | 「+」で3つにして、win_01・win_02・win_03 |


報酬の3択を作ろう(RewardView)
勝ったら、報酬のカードを3枚出して、1枚選べるようにします。候補の中から同じカードが重ならないように3枚選び、1枚ずつポンと出します。
画面を作る
- Hudの子にRewardView(UI (Canvas)>Image)
- ResultViewの下(Hudの子の一番下)に置く
- アンカー:stretch-stretch、Left・Top・Right・Bottom:0
- Color:#000000、Alpha:150ぐらい
- RewardViewの子にTitle(Text – TextMeshPro)
- Pos Y:300、Width:1400、Height:120
- テキスト:報酬:カードを1枚えらぼう、Font Size:60、Font Style:B、Color:白、Alignment:中央
- RewardViewの子にCardArea(Create Empty)
- Pos Y:-20、Width:1000、Height:360
- Add Component>Horizontal Layout Group
- Spacing:80、Child Alignment:Middle Center
- Control Child SizeのWidth・Heightと、Child Force ExpandのWidth・Heightは、どれもチェックを外す

Horizontal Layout Groupは、子を横にならべてくれるコンポーネントです(9-6 Scroll ViewとLayout Groupでリストを作ろう)。カードの大きさはプレハブのまま使いたいので、Control Child SizeとChild Force Expandは外しておきます。
Deckに足す
選んだカードを山札に加えるメソッドを、DeckのRefillFromDiscardの上に足します。
// ★#12 報酬のカードを山札に加えて、混ぜる
public void AddCard(CardData card)
{
drawPile.Add(card);
Shuffle(drawPile);
UpdateCounts();
Debug.Log($"{card.cardName}をデッキに加えた!");
}
RewardView
- DeckBuilding/Scriptsに「RewardView」スクリプトを作って、RewardViewに付ける
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
// 勝ったときの報酬:候補から重ならないように3枚出して、選んだ1枚をデッキに加える
public class RewardView : MonoBehaviour
{
[SerializeField] private List<CardData> candidates = new List<CardData>(); // 報酬の候補
[SerializeField] private int choiceCount = 3; // 出す枚数
[SerializeField] private CardView cardPrefab;
[SerializeField] private RectTransform cardArea; // カードを並べる場所(Horizontal Layout Group)
[SerializeField] private Deck deck;
[SerializeField] private RectTransform drawPileArea; // 選んだカードが飛んでいく先(山札)
[SerializeField] private float appearInterval = 0.15f; // 1枚ずつ出す間
[SerializeField] private float popSeconds = 0.2f; // 1枚がポンと出るのにかける時間
private CardView chosenView; // 選ばれたカード(選ばれるまでは null)
private void Awake()
{
gameObject.SetActive(false); // 最初は隠しておく
}
// 3枚出して、1枚選ばれるまで待ち、選ばれたカードを山札へ飛ばす
public IEnumerator Open()
{
gameObject.SetActive(true);
chosenView = null;
// 候補をコピーして混ぜ、先頭から使う(同じカードが2枚出ない)
List<CardData> pool = new List<CardData>(candidates);
for (int i = pool.Count - 1; i > 0; i--)
{
int j = Random.Range(0, i + 1);
CardData temp = pool[i];
pool[i] = pool[j];
pool[j] = temp;
}
// 1枚ずつ、ポンと出す
List<CardView> views = new List<CardView>();
for (int i = 0; i < choiceCount && i < pool.Count; i++)
{
CardView view = CreateChoice(pool[i]);
views.Add(view);
StartCoroutine(Pop(view.transform));
yield return new WaitForSeconds(appearInterval);
}
// 選ばれるまで待つ
while (chosenView == null)
{
yield return null;
}
// 選ばなかったカードを消して、選んだカードを山札へ飛ばす
foreach (CardView view in views)
{
if (view != chosenView)
{
Destroy(view.gameObject);
}
}
chosenView.transform.SetParent(transform, true); // 並べる場所から出す(並べ直されないように)
yield return StartCoroutine(chosenView.GetComponent<CardFly>().FlyTo(drawPileArea.position));
deck.AddCard(chosenView.Data);
Destroy(chosenView.gameObject);
gameObject.SetActive(false);
}
// 選べるカードを1枚作る
private CardView CreateChoice(CardData card)
{
CardView view = Instantiate(cardPrefab, cardArea);
view.Setup(card);
// 手札用の動き(つかむ・持ち上げる)と、エネルギーで暗くする処理は使わない
view.GetComponent<CardDrag>().enabled = false;
view.GetComponent<CardHover>().enabled = false;
view.enabled = false;
// ボタンにして、押したらこのカードを選ぶ
Button button = view.gameObject.AddComponent<Button>();
button.targetGraphic = view.GetComponent<Image>();
button.onClick.AddListener(() => Choose(view));
return view;
}
private void Choose(CardView view)
{
if (chosenView != null)
{
return; // もう選んだあと
}
chosenView = view;
}
// 0 から 1 へ、ポンと大きくなって出てくる
private IEnumerator Pop(Transform target)
{
for (float time = 0f; time < popSeconds; time += Time.deltaTime)
{
target.localScale = Vector3.one * Mathf.Lerp(0f, 1f, time / popSeconds);
yield return null;
}
target.localScale = Vector3.one;
}
}
- 重ならない3枚:候補のリストをコピーしてから、#4の山札と同じ方法(Fisher-Yates)で混ぜ、先頭から3枚使う
- コピーするのは、Inspectorで入れた候補の並びを変えないためです
- カードは手札と同じCardのプレハブから作る
- ドラッグ・持ち上げ(CardDrag・CardHover)と、エネルギーで暗くする処理(CardView)は
enabled = falseで止めます。止めておくと、StartやUpdateが呼ばれず、マウスのイベントも届きません
- ドラッグ・持ち上げ(CardDrag・CardHover)と、エネルギーで暗くする処理(CardView)は
AddComponent<Button>()で、カードを押せるようにするonClick.AddListener(() => Choose(view));の() => …は、「押されたらChoose(view)を呼ぶ」という小さなメソッドをその場で書く書き方(ラムダ式)です。3枚それぞれが、自分のviewを覚えています
- 選ばれるまで
whileで待つ → 選ばなかったカードを消す → 選んだカードを#10のCardFlyで山札へ飛ばす →AddCard - 飛ばす前に
SetParent(transform, true)で、CardAreaから外す- CardAreaの中のままだと、ほかのカードを消したときにLayout Groupが並べ直して、飛んでいる途中のカードが真ん中へ引き戻されてしまいます
作ったら、RewardViewのInspectorでつなぎます。
| 項目 | 入れるもの |
|---|---|
| Candidates | 「+」で6つにして、Scissors・Compass・Stapler・PencilCase・StickyNote・ColorPencils |
| Choice Count | 3 |
| Card Prefab | Prefabs/Card |
| Card Area | CardArea |
| Deck | Deck |
| Draw Pile Area | Hud>DrawPile |
報酬の候補は、Inspectorのリストに足すだけで増やせます。#13で作る特殊なカードも、ここに足せば報酬に出てくるようになります。

BattleManagerから呼ぶ
BattleManagerに項目を足します。
[SerializeField] private Sprite[] winFrames; // win_01・win_02・win_03
[SerializeField] private RewardView rewardView; // 報酬の3択
Victoryの最後を、次のように直します(2行足す)。
yield return StartCoroutine(resultView.Show("勝利!", new Color(1f, 0.82f, 0.25f)));
yield return new WaitForSeconds(1f);
resultView.Hide(); // ★#12 報酬を選ぶ間は、勝利の文字を消す
yield return StartCoroutine(rewardView.Open()); // ★#12 選んでデッキに加え終わるまで待つ
resultView.ShowRetry();
}
- BattleManagerのReward Viewに、Hud>RewardViewを入れる



動かして確認!
再生して、敵を全部倒してみましょう。1体ずつ倒すと、最後の1体のときだけ時間が長く止まって、「勝利!」が出ます。負けも確かめたいときは、PlayerAreaのFighterのMax Hpを20ぐらいにして、何もせずにターン終了を押してみてください(確かめたら60に戻しましょう)。

#12の動作確認
- HPが0になった敵は、少しのけぞってから、横にふくらんで縦にしぼみ、火花とともに消える
- 倒れた敵は、敵のターンに行動しない
- 最後の1体を倒すと、長めに止まってから「勝利!」が出て、スライムちゃんが勝ちのポーズになる
- 報酬のカードが3枚ポンポンと出る。同じカードは2枚出ない
- 1枚押すと、そのカードが山札へ飛んで、山札の数字が1つ増える(Consoleに「〇〇をデッキに加えた!」)
- スライムちゃんのHPが0になると、残りの敵は行動せず、「まけてしまった・・・」が出る
- 「もう一度」を押すと、戦いが最初からやり直しになる
スライム報酬を選んでも「もう一度」で最初に戻ると、選んだカードもなくなっちゃいますね・・・



そこは#14で作る「3連戦」で、次の戦いにデッキを持ちこせるようにするよ。その前に、次回は報酬にも出てくる特殊なカードを増やそう!
あとがき
今回は、敵が倒れて勝ち、HPが0で負けの判定と、勝ったときの報酬のカード選びを作りました。これで「戦って、勝って、デッキが強くなる」というデッキ構築ゲームの1周がつながりましたね。次回は、えんぴつをけずる・修正テープ・大そうじ・おやつタイムなど、戦い方が変わる特殊なカードを増やしたいと思います!
コメント