デッキ構築カードゲームの作り方#14|3連戦をつなげよう

1戦目のブリキロボを倒すと「勝利!」、報酬のカードを3枚から1枚選ぶと山札へ飛んでいく。スライムちゃんが眠るポーズになって緑の「+10」が出てHPが回復し、上の表示が「2戦目(2/3)」に変わって、ブリキロボとびっくり箱が出てくる

前回で、15種類のカードがそろいました。でも、今はまだ1回戦って「もう一度」で最初に戻るだけなので、報酬で選んだカードを使うことが出来ません・・・。今回は、1戦目 → 2戦目 → 3戦目と戦いを続けて、報酬で強くしたデッキを次の戦いに持ちこせるようにしてみたいと思います!

目次

今回作るもの

  • 戦いのデータ(名前と、出てくる敵のリスト)
  • 3つの戦いを順番につなげる。今何戦目かを上に出す
  • 勝ったら「勝利!」→ 報酬を選ぶ → 休んでHP+10 → 次の戦いへ。3戦目に勝ったら「ぜんぶクリア!」
  • 戦いの間に、使ったカードを全部山札に戻す(報酬のカードも入ったまま)
  • 3戦目の敵「ほつれたぬいぐるみ」
スクリプト役割付ける場所
BattleData1回の戦いのデータ(付けない。アセットを作る)
RunManager戦いを順番につなげる、戦いの間に休むRunManager(新しく作る空のオブジェクト)
EnemyParty(直す)敵を並べるのを、あとから呼べるように-
BattleManager(直す)戦いを外から始められるように。「勝利!」と報酬はRunManagerへ-
Deck(足す)次の戦いの前に、カードを全部山札に戻す-

ボスと、手下を呼ぶ行動は次の#15で作ります。

前回の続きから

前回の最後の状態から始めます。ぬいぐるみと、眠るスライムちゃんの絵は、素材のzipを入れ直すと入ります。

  • DeckBuildingAssets.zip
    • DeckBuilding/Sprites/Enemies/enemy_plush.png(ほつれたぬいぐるみ)
    • DeckBuilding/Sprites/Slime/sleep_01.png〜sleep_03.png(眠るスライムちゃん。1→2→3→2の順で、すやすや動く)
    • 自分で入れた場合は、Sprite Mode:Singleにして Apply

3戦目の敵を作ろう

#8と同じ形で、敵のデータを作ります。

  • Data/Enemiesで右クリック>Create>DeckBuilding>Enemy Data、名前をPlush
    • Enemy Name:ほつれたぬいぐるみ、Sprite:enemy_plush、Max Hp:44
    • Actions:2つ
      • Element 0:Attack 5、Block 8(防御を固めながら、少し攻撃)
      • Element 1:Attack 10、Block 0
ProjectウインドでData/Enemies/Plushを選択したInspector。Enemy Nameがほつれたぬいぐるみ、Spriteがenemy_plush、Max Hpが44、ActionsのElement 0がAttack 5・Block 8、Element 1がAttack 10・Block 0

長い名前を折り返さないようにする

「ほつれたぬいぐるみ」は今までの敵より名前が長いので、そのままだと最後の1文字だけが次の行に折り返されてしまいます。

  • Prefabs/Enemyを開いて、NameTextを選ぶ
    • Width:260 → 380
    • TextMeshProの Text Wrapping Mode:No Wrap(折り返さない。Main SettingsのAlignmentの下にあります)
Enemyのプレハブの編集画面。左はHierarchyでNameTextを選択した状態とInspectorのRect Transform(Width 380、Height 40)。右はInspectorを下へスクロールしたところで、Text Wrapping ModeがNo Wrap。WidthとText Wrapping Modeを赤枠で囲む

Text Wrapping ModeがNormalのままだと、枠の幅に入りきらない文字が次の行へ回されます。名前のように1行で見せたい文字は、Text Wrapping Mode:No Wrapにしておくと、長い名前の敵を足しても崩れません。

戦いのデータを作ろう(BattleData)

「1戦目はブリキロボ1体」「2戦目はブリキロボとびっくり箱」のような、1回の戦いの中身をScriptableObjectにします(12-8 ScriptableObjectでデータを作ろう)。

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

// 1回の戦いのデータ:名前と、出てくる敵のリスト。Create>DeckBuilding>Battle Data で作る
[CreateAssetMenu(fileName = "NewBattle", menuName = "DeckBuilding/Battle Data")]
public class BattleData : ScriptableObject
{
    public string battleName = "1戦目";                        // 上に出す名前
    public List<EnemyData> enemies = new List<EnemyData>(); // 出てくる敵(左から順に並ぶ)
}
  • Dataフォルダの中にBattlesフォルダを作って、右クリック>Create>DeckBuilding>Battle Dataで3つ作る
ファイル名Battle NameEnemies
Battle11戦目TinRobot
Battle22戦目TinRobot、JackBox
Battle33戦目Plush、Plush
ProjectウインドでData/Battles/Battle2を選択したInspector。Battle Nameが2戦目、Enemiesが2つ(TinRobot・JackBox)

敵を並べるのを、あとから呼べるようにする

今のEnemyPartyは、StartでInspectorのリストから敵を並べていました。これからは戦いごとに敵が変わるので、並べる処理をSpawnというメソッドにして、外から呼ぶ形にします。EnemyPartyを次のように直します。

using System.Collections.Generic;
using UnityEngine;

// 敵のデータのリストから、敵を横に並べて作る
public class EnemyParty : MonoBehaviour
{
    [SerializeField] private EnemyView enemyPrefab;
    [SerializeField] private float spacing = 420f; // となりの敵との間隔

    private List<EnemyView> views = new List<EnemyView>();

    public List<EnemyView> Views => views;

    // ★#14 敵のリストを受け取って、横に並べて作る(前は Start で、Inspector のリストから作っていた)
    public void Spawn(List<EnemyData> enemies)
    {
        views.Clear();

        // 真ん中を0にして、左右に同じだけ広げる
        float left = -(enemies.Count - 1) * spacing / 2f;
        for (int i = 0; i < enemies.Count; i++)
        {
            EnemyView view = Instantiate(enemyPrefab, transform);
            ((RectTransform)view.transform).anchoredPosition = new Vector2(left + i * spacing, 20f);
            view.Setup(enemies[i]);
            views.Add(view);
        }
    }
}
  • enemiesの項目とStartを消して、Spawn(List<EnemyData> enemies)にする
  • 中身は前のStartとほとんど同じです。最初にviews.Clear()で、前の戦いのリストを空にします

BattleManagerを直す

BattleManagerは、これまでStartで自分から戦いを始めていました。これからはRunManagerが「次の戦いを始めて」と頼む形にします。

戦いの前の状態を足す

public enum TurnState
{
    Ready,      // ★#14 戦いが始まる前
    PlayerTurn, // あなたのターン
  • Readyを一番上に足す
    • enumの変数は、何も入れないと一番上の値になります。最初はReadyなので、Updateの「敵を全部倒したら勝ち」の判定が、敵を並べる前に動いてしまうことがありません

Startを、外から呼ぶメソッドに変える

IEnumerator Start()を消して、次のメソッドにします。

    // ★#14 戦いを始める(RunManager から呼ぶ。前は Start で始めていた)
    public void StartBattle()
    {
        StartCoroutine(PlayerTurn());
    }

「勝利!」と報酬は、RunManagerに移す

3戦目に勝ったときは「勝利!」ではなく「ぜんぶクリア!」と出したいので、表示と報酬はRunManagerに任せます。Victoryを次のように短くします。

    // ★#12 勝ち:勝ちのポーズ(★#14「勝利!」と報酬は RunManager に移した)
    private IEnumerator Victory()
    {
        state = TurnState.Victory;
        endTurnButton.interactable = false;
        yield return new WaitForSeconds(1f); // 最後の敵が消えるのを待つ
        deck.DiscardHand();
        StartCoroutine(WinPose());
    }
  • resultView.Show(…)から下の5行を消す
  • 項目のrewardViewも使わなくなるので、消しておきます(Defeatで使うresultViewは残す)

BattleManagerは「1回の戦い」だけ、RunManagerは「戦いの並び」だけを受け持つようにしています。役わりを分けておくと、#15でボス戦を足すときも、RunManagerのBattlesに1つ足すだけで済みます。

次の戦いの前に、カードを山札に戻す

Deckに、捨て札と「なくなった札」を全部山札に戻すメソッドを足します(AddCardの上)。

    // ★#14 次の戦いの前に、捨て札となくなった札を全部山札に戻して混ぜる
    public void ResetForBattle()
    {
        drawPile.AddRange(discardPile);
        drawPile.AddRange(exhaustPile);
        discardPile.Clear();
        exhaustPile.Clear();
        Shuffle(drawPile);
        UpdateCounts();
    }
  • 勝ったときに手札は捨て札へ行っているので、捨て札と山札を合わせれば、デッキの全部になります
  • おやつタイムのような「なくなった札」も、次の戦いではまた使えるように戻します
  • 報酬で選んだカードは、もう山札に入っているので、そのまま次の戦いに持ちこせます

戦いを順番につなげる(RunManager)

今何戦目かの表示

  • Hudの子にRunLabel(Text – TextMeshPro)
    • アンカー:top-center、Pivot Y:1、Pos Y:-24、Width:600、Height:70
    • テキスト:1戦目(1/3)、Font Size:44、Font Style:B、Color:#3D2E2E、Alignment:中央
    • Raycast Targetのチェックを外す

RunManager

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

// 戦いを順番につなげる:1戦目 → 2戦目 → 3戦目。勝ったら報酬を選んで休み、次の戦いへ
public class RunManager : MonoBehaviour
{
    [SerializeField] private List<BattleData> battles = new List<BattleData>(); // 戦う順番
    [SerializeField] private BattleManager battleManager;
    [SerializeField] private EnemyParty enemyParty;
    [SerializeField] private Deck deck;
    [SerializeField] private Fighter player;
    [SerializeField] private ResultView resultView;
    [SerializeField] private RewardView rewardView;
    [SerializeField] private TextMeshProUGUI runLabel; // 今何戦目か

    [Header("戦いの間に休む")]
    [SerializeField] private IdleBounce slimeIdle;
    [SerializeField] private Sprite[] sleepFrames; // sleep_01・sleep_02・sleep_03
    [SerializeField] private int restHeal = 10;    // 休んで回復する HP

    [Header("確かめるとき用")]
    [SerializeField] private int startBattle = 0; // 0以外にすると、その番号の戦いから始まる(2 なら3戦目)

    private IEnumerator Start()
    {
        // ほかのスクリプトの Start(山札を混ぜるなど)が終わるのを、1フレーム待つ
        yield return null;

        for (int i = startBattle; i < battles.Count; i++)
        {
            BattleData battle = battles[i];
            runLabel.text = $"{battle.battleName}({i + 1}/{battles.Count})";
            if (i > startBattle)
            {
                yield return StartCoroutine(Rest()); // 戦いの間に休む
                deck.ResetForBattle();               // 使ったカードを全部山札に戻す
            }

            enemyParty.Spawn(battle.enemies);
            battleManager.StartBattle();

            // 勝つか負けるまで待つ
            while (battleManager.State != TurnState.Victory && battleManager.State != TurnState.Defeat)
            {
                yield return null;
            }
            if (battleManager.State == TurnState.Defeat)
            {
                yield break; // 負けたら終わり(「もう一度」は BattleManager が出す)
            }

            // 勝った:最後の敵が消えて、勝ちのポーズになるのを待ってから表示
            yield return new WaitForSeconds(1.5f);
            bool isLast = i == battles.Count - 1;
            yield return StartCoroutine(resultView.Show(isLast ? "ぜんぶクリア!" : "勝利!", new Color(1f, 0.82f, 0.25f)));
            yield return new WaitForSeconds(1f);
            if (isLast)
            {
                resultView.ShowRetry();
                yield break;
            }
            resultView.Hide();
            yield return StartCoroutine(rewardView.Open()); // 報酬を選ぶ
        }
    }

    // 戦いの間に休む:眠るポーズをしてから HP を回復して、待機の動きに戻す
    private IEnumerator Rest()
    {
        slimeIdle.enabled = false;
        Image slimeImage = slimeIdle.GetComponent<Image>();
        int[] order = { 0, 1, 2, 1, 0, 1, 2, 1 }; // 1→2→3→2 を2回
        foreach (int index in order)
        {
            slimeImage.sprite = sleepFrames[index];
            yield return new WaitForSeconds(0.2f);
        }
        player.Heal(restHeal); // #11 の緑の数字とキラキラも出る
        yield return new WaitForSeconds(1f);
        slimeIdle.enabled = true;
    }
}
  • Startをコルーチンにして、forで戦いを1つずつ進める
    • 「敵を並べる → 戦いを始める → 勝つか負けるまで待つ → 報酬 → 休む → 次へ」を、上から順番に読める形で書けるのがコルーチンのいいところです
  • 勝つか負けるまでwhileで待つ
    • BattleManagerのStateを毎フレーム見て、VictoryかDefeatになったら次へ進みます
  • 2戦目からは、始める前にRestで休んで、ResetForBattleでカードを山札に戻す
  • 最後の戦いだけ「ぜんぶクリア!」と「もう一度」ボタン。それ以外は「勝利!」のあと報酬
  • Rest:#12の勝ちのポーズと同じように、IdleBounceを止めてから眠る絵を切りかえ、player.Heal(10)で回復します
    • HPが減っていれば、#11の緑の「+10」とキラキラ、ゲージが伸びる動きがそのまま出ます
  • startBattle:3戦目だけ確かめたいときなどに、2を入れると3戦目から始まります

HPは戦いの間で引きつがれます(戦いの間に回復するのは10だけ)。#9の「防御は自分の番の始めに0」とちがって、HPはリセットしません。1戦目で受けたダメージが、3戦目までじわじわ効いてくるのが、連戦の面白いところです。

置いて、つなぐ

  • HierarchyにRunManager(Create Empty)を作って、Add Component>RunManager
項目入れるもの
Battles「+」で3つにして、Battle1・Battle2・Battle3
Battle ManagerBattleManager
Enemy PartyStage>EnemyArea
DeckDeck
PlayerStage>PlayerArea
Result ViewHud>ResultView
Reward ViewHud>RewardView
Run LabelHud>RunLabel
Slime IdleStage>PlayerArea>Slime
Sleep Frames「+」で3つにして、sleep_01・sleep_02・sleep_03
Rest Heal10
Start Battle0
HierarchyでRunManagerを選択したInspector。BattlesにBattle1〜3、Battle ManagerにBattleManager、Enemy PartyにEnemyArea、DeckにDeck、PlayerにPlayerArea、Result ViewにResultView、Reward ViewにRewardView、Run LabelにRunLabel、Slime IdleにSlime、Sleep Framesにsleep_01〜03、Rest Heal 10、Start Battle 0

EnemyAreaのEnemy Partyにあった「Enemies」のリストは、スクリプトから消したのでInspectorからも消えます。どの敵が出るかは、これからはBattleDataで決めます。

動かして確認!

再生して、敵を倒してみましょう。1戦目に勝つと、報酬のあとに休んで、2戦目が始まります。

報酬のカードが山札へ飛んだあと、スライムちゃんが眠るポーズ(sleep_01〜03)ですやすやして、緑の「+10」とキラキラが出てHPが54/60から60/60に回復する。スライムちゃんのまわりを切り抜き

3戦目だけ確かめたいときは、RunManagerのStart Battleを2にして再生します(確かめたら0に戻しましょう)。

Start Battleを2にして再生したGameビュー。上に「3戦目(3/3)」、右にほつれたぬいぐるみが2体(名前は1行で表示、予告は剣と5)、左にスライムちゃん

#14の動作確認

  • 上に「1戦目(1/3)」が出て、ブリキロボ1体と戦う
  • 勝つと「勝利!」→ 報酬を選ぶ → スライムちゃんが眠ってHP+10 → 「2戦目(2/3)」でブリキロボとびっくり箱が出てくる
  • 1戦目で選んだ報酬のカードが、2戦目の手札に出てくる(山札の数字も1つ多い)
  • 3戦目はほつれたぬいぐるみが2体。名前が1行で出る
  • 3戦目に勝つと「ぜんぶクリア!」と「もう一度」ボタン
  • どこかで負けると「まけてしまった・・・」と「もう一度」ボタン。「もう一度」で1戦目からやり直し
スライム

報酬で選んだカードが次の戦いで手札に来ると、ちょっとうれしいですね!

フェアリー

「このカードを取ったから勝てた!」が、デッキ構築の一番の気持ちよさだよね。次回はいよいよボスだよ!

あとがき

今回は、戦いのデータを作って、3つの戦いを続けて遊べるようにしました。報酬で選んだカードが次の戦いに持ちこせるようになって、ようやく「デッキを育てる」遊びがつながりましたね。次回は、おもちゃの王さまの登場演出と、手下を呼ぶ行動を作って、ボス戦を作りたいと思います!

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

コメント

コメント一覧 (1件)

コメントする

目次