デッキ構築カードゲームの作り方#9|ターンの流れを作ろう

下じきガードを使ってからターン終了ボタンを押すと、ボタンが灰色になり「敵のターン」の帯が出る。ブリキロボの攻撃6を防御5で受けとめてスライムちゃんのHPが59に、びっくり箱はためる。予告が「6」と「16」に変わり、「あなたのターン」の帯が出て5枚配られる

ここまでで、カードを使って敵のHPを減らせるようになりました。でも、まだ敵は何もしてきません・・・。今回は、「あなたのターン → 敵のターン → またあなたのターン」の流れを作ります。ターン終了ボタンを押すと、敵が#8で出した予告どおりに行動して、また自分の番に戻ってくるようにしてみたいと思います!

目次

今回作るもの

  • ターンの状態(あなたのターン・敵のターン)
  • あなたのターンの始め:防御を0に戻す、エネルギーを3に戻す、5枚配る
  • ターン終了:手札を捨てて、敵のターンへ
  • 敵のターン:1体ずつ、予告どおりに行動する
  • 「あなたのターン」「敵のターン」の帯
スクリプト役割付ける場所
BattleManagerターンの流れを動かすBattleManager(新しく作る空のオブジェクト)
TurnBanner「あなたのターン」などの帯を出して消すTurnBanner
Fighter(足す)防御を0に戻す-
EnemyView(足す)次の行動へ進む-
Deck(直す)最初の5枚は BattleManager が配る-

勝ち負け(敵を全部倒す・HPが0になる)は、#12で作ります。

前回の続きから

前回の最後の状態から始めます。新しい素材はありません。

ターンの流れを考えよう

まず、1ターンの中で何が起きるかを並べてみます。

「あなたのターン」(防御を0に・エネルギーを3に・5枚配る → カードを使う)→ ターン終了ボタン →「敵のターン」(手札を捨てる → 敵が1体ずつ予告どおりに行動 → 次の予告を出す)→「あなたのターン」に戻る、の輪を描いた図
  • あなたのターン:自分の防御を0に戻す → エネルギーを3に戻す → 5枚配る → カードを使う
  • ターン終了ボタンを押す
  • 敵のターン:手札を捨てる → 敵が1体ずつ行動する → 次の予告を出す
  • また、あなたのターンへ

「今どちらのターンか」は、6-15 選択肢に名前を付けよう:enum(列挙型)とswitch文で紹介したenumで持ちます。流れは、時間を待ちながら順番に進めたいので、コルーチン(6-11 時間を扱う処理)で作ります。

FighterとEnemyViewに足そう

Fighter:防御を0に戻す

デッキ構築ゲームでは、防御は自分の番が来るたびに0に戻るのが一般的です(#7で「防御がターンをまたいで残る」と書いていたところです)。Fighterに、防御を0に戻すメソッドを足します。

    // ★#9 防御を0に戻す(自分の番の始めに呼ぶ)
    public void ResetBlock()
    {
        block = 0;
    }
  • FighterのHealの下に足す

EnemyView:次の行動へ進む

敵が行動したら、次の行動の予告に切りかえます。EnemyViewに足します。

    // ★#9 次の行動へ進んで、予告を出し直す(最後まで行ったら最初に戻る)
    public void AdvanceAction()
    {
        actionIndex = (actionIndex + 1) % data.actions.Count;
        ShowIntent();
    }
  • EnemyViewのSetupの下に足す
  • % data.actions.Countで、行動の並びの最後まで行ったら0に戻る。ブリキロボなら「6 → 6 → 9 → 6 → …」とくり返す
  • ShowIntentを呼ぶので、予告が変わるたびに、#8で作った「ポン」の動きが出ます

Deck:最初の5枚を配らない

#4から、DeckのStartで最初の5枚を配っていました。今回からはターンの始めにBattleManagerが配るので、DeckのStartの最後の1行を消します。

        Shuffle(drawPile);
        Draw(5); // ★#9 この行を消す(最初の5枚は BattleManager が配る)

帯を作ろう(TurnBanner)

「あなたのターン」「敵のターン」を、画面の真ん中に帯で出します。

  • Hudの子にTurnBanner(UI (Canvas)>Image)
    • アンカー:middle-stretch(横いっぱい)、Left・Right:0、Pos Y:80、Height:140
    • Source Image:なし、Color:#000000、Alpha:150ぐらい
    • Raycast Targetのチェックを外す(帯がカードの操作をじゃましないように)
    • Add Component>Canvas Group
  • TurnBannerの子にLabel(Text – TextMeshPro)
    • アンカー:stretch-stretch、Left・Top・Right・Bottom:0
    • テキスト:「あなたのターン」、Font Size:72、Font Style:B、Alignment:中央、Color:白
  • DeckBuilding/Scriptsに「TurnBanner」スクリプトを作る
using System.Collections;
using TMPro;
using UnityEngine;

// 「あなたのターン」「敵のターン」の帯を、画面の真ん中に出して消す
[RequireComponent(typeof(CanvasGroup))]
public class TurnBanner : MonoBehaviour
{
    [SerializeField] private TextMeshProUGUI label;
    [SerializeField] private float fadeSeconds = 0.2f; // 出る・消えるのにかける時間
    [SerializeField] private float showSeconds = 0.6f; // 出たまま止まる時間

    private CanvasGroup canvasGroup;

    private void Awake()
    {
        canvasGroup = GetComponent<CanvasGroup>();
        canvasGroup.alpha = 0f;
        canvasGroup.blocksRaycasts = false;
    }

    // 帯を出して消す(終わるまで待てるように IEnumerator にしている)
    public IEnumerator Show(string text)
    {
        label.text = text;
        RectTransform labelRect = label.rectTransform;

        // 出る:左から文字がすべりこみながら、帯が見えてくる
        for (float time = 0f; time < fadeSeconds; time += Time.deltaTime)
        {
            float rate = time / fadeSeconds;
            canvasGroup.alpha = rate;
            labelRect.anchoredPosition = new Vector2(Mathf.Lerp(-200f, 0f, rate), 0f);
            yield return null;
        }
        canvasGroup.alpha = 1f;
        labelRect.anchoredPosition = Vector2.zero;

        yield return new WaitForSeconds(showSeconds);

        // 消える:右へ抜けながら、帯が消えていく
        for (float time = 0f; time < fadeSeconds; time += Time.deltaTime)
        {
            float rate = time / fadeSeconds;
            canvasGroup.alpha = 1f - rate;
            labelRect.anchoredPosition = new Vector2(Mathf.Lerp(0f, 200f, rate), 0f);
            yield return null;
        }
        canvasGroup.alpha = 0f;
    }
}
  • public IEnumerator Show
    • コルーチンの中身を、ほかのスクリプトからStartCoroutineで呼べるようにしている。呼ぶ側は帯が出て消えるまで待ってから次に進める
  • for (float time = 0f; …; time += Time.deltaTime)
    • 経過時間を数えながら、0.2秒かけてアルファ(すけ具合)と文字の位置を少しずつ変える
  • canvasGroup.blocksRaycasts = false
    • 帯が出ている間も、下のカードやボタンを触れるようにする

TurnBannerにTurnBannerを付けて、Label:Labelを入れます。

HierarchyでHud>TurnBanner(子にLabel)を選択したInspector。Imageのアンカーがmiddle-stretch、Height 140、Colorが半透明の黒、Raycast Targetのチェックなし、Canvas Group、Turn BannerのLabelにLabel。Gameビューで、画面の真ん中に黒い帯と「あなたのターン」

BattleManagerを作ろう

いよいよ、ターンの流れを動かすBattleManagerです。

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

// 今どちらのターンか
public enum TurnState
{
    PlayerTurn, // あなたのターン
    EnemyTurn   // 敵のターン
}

// ターンの流れを動かす:あなたのターン → 敵のターン → あなたのターン …
public class BattleManager : MonoBehaviour
{
    [SerializeField] private Deck deck;
    [SerializeField] private Energy energy;
    [SerializeField] private Fighter player;
    [SerializeField] private EnemyParty enemyParty;
    [SerializeField] private Button endTurnButton;
    [SerializeField] private TurnBanner turnBanner;
    [SerializeField] private int drawCount = 5;              // 毎ターン配る枚数
    [SerializeField] private float enemyActionSeconds = 0.6f; // 敵1体ごとの間

    private TurnState state;

    public TurnState State => state;

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

    // ターン終了ボタンから呼ぶ
    public void EndTurn()
    {
        if (state != TurnState.PlayerTurn)
        {
            return; // 敵のターンの間は何もしない
        }
        StartCoroutine(EnemyTurn());
    }

    // あなたのターン
    private IEnumerator PlayerTurn()
    {
        state = TurnState.PlayerTurn;
        player.ResetBlock();  // 防御は自分の番の始めに0に戻る
        energy.Refill();      // エネルギーを3に戻す
        yield return StartCoroutine(turnBanner.Show("あなたのターン"));
        deck.Draw(drawCount); // 5枚配る
        endTurnButton.interactable = true;
    }

    // 敵のターン
    private IEnumerator EnemyTurn()
    {
        state = TurnState.EnemyTurn;
        endTurnButton.interactable = false; // 敵のターンの間は押せない
        deck.DiscardHand();
        yield return StartCoroutine(turnBanner.Show("敵のターン"));

        foreach (EnemyView enemy in enemyParty.Views)
        {
            enemy.Fighter.ResetBlock(); // 敵の防御も、敵の番の始めに0に戻る
            EnemyData.EnemyAction action = enemy.NextAction;

            if (action.block > 0)
            {
                enemy.Fighter.AddBlock(action.block);
            }
            if (action.attack > 0)
            {
                player.TakeDamage(action.attack);
            }
            if (action.attack == 0 && action.block == 0)
            {
                Debug.Log($"{enemy.Fighter.FighterName}は力をためている・・・");
            }

            enemy.AdvanceAction(); // 次の予告を出す
            yield return new WaitForSeconds(enemyActionSeconds);
        }

        StartCoroutine(PlayerTurn());
    }
}
  • enum TurnState と state
    • 今どちらのターンかを持っておく。EndTurnは、あなたのターンのときだけ敵のターンへ進める。ボタンを2回連打しても、敵のターンが2回始まらないようにするため
  • private IEnumerator Start()
    • StartもIEnumeratorにすると、コルーチンとして動く。yield return nullで1フレーム待ってから始めるのは、DeckのStart(山札を混ぜる)とEnemyPartyのStart(敵を並べる)が先に終わるようにするため
    • 同じシーンにあるスクリプトのStartは、どれが先に呼ばれるか決まっていません。1フレーム待つと、全部のStartが終わったあとになります
  • yield return StartCoroutine(turnBanner.Show(…))
    • 帯のコルーチンを動かして、それが終わるまで待つ。帯が消えてからカードを配る・敵が動く、という順番になる
  • foreach (EnemyView enemy in enemyParty.Views)
    • #8で作った敵のリストを、左から1体ずつ行動させる。1体ごとにWaitForSecondsで0.6秒待つので、何が起きたかを目で追える
  • endTurnButton.interactable
    • falseにすると、ボタンが灰色になって押せなくなる。敵のターンが終わって、あなたのターンで5枚配ったあとに戻す
スライム

StartがIEnumeratorになってる・・・そんな書き方もできるんですね

フェアリー

Unityでは、StartをIEnumeratorにすると自動でコルーチンとして動いてくれるんだ。「最初に少し待ってから始めたい」ときに便利だよ!

BattleManagerを置いて、ボタンをつなごう

  • Hierarchyで右クリック>Create Emptyで「BattleManager」を作り、BattleManagerスクリプトを付ける
  • Inspectorで、次のものをドラッグ&ドロップ
項目入れるもの
DeckDeck
EnergyHud>Energy
PlayerPlayerArea
Enemy PartyEnemyArea
End Turn ButtonHud>EndTurnButton
Turn BannerHud>TurnBanner
Draw Count5
Enemy Action Seconds0.6
HierarchyでBattleManagerを選択したInspector。Battle ManagerのDeckにDeck、EnergyにEnergy、PlayerにPlayerArea、Enemy PartyにEnemyArea、End Turn ButtonにEndTurnButton、Turn BannerにTurnBanner、Draw Countが5、Enemy Action Secondsが0.6

ターン終了ボタンのつなぎ方も変えます。#4と#7で足した3つ(Deck.DiscardHand・Deck.Draw・Energy.Refill)は、BattleManagerが順番に呼ぶようになったので、ボタンからは消します。

  • HierarchyでEndTurnButtonを選び、On Click ()の3つの欄を、それぞれ選んで「-」で消す
  • 「+」で1つ足して、BattleManagerをドラッグしてBattleManager > EndTurn ()
HierarchyでEndTurnButtonを選択したInspector。ButtonのOn Click ()が1つだけで、BattleManager.EndTurnになっている。On Click ()を赤枠で囲む

On Click ()の3つを消し忘れると、ボタンを押したときに「手札を捨てる」「5枚引く」が2回ずつ起きて、枚数がおかしくなります。On Click ()はBattleManager.EndTurnの1つだけにしましょう。

「何を・どの順番でするか」をBattleManagerの1か所にまとめておくと、ターンの流れを変えたいとき(たとえば「ターンの始めに毒のダメージ」を足すなど)も、ここだけを直せば済みます。

動かして確認!

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

下じきガードを使ってからターン終了ボタンを押すと、ボタンが灰色になり「敵のターン」の帯が出る。ブリキロボの攻撃6を防御5で受けとめてスライムちゃんのHPが59に、びっくり箱はためる。予告が「6」と「16」に変わり、「あなたのターン」の帯が出て5枚配られる
Consoleに「スライムちゃんの防御 +5 防御 5」「スライムちゃんに6ダメージ(防御で5防いだ) HP 59/60」「びっくり箱は力をためている・・・」が並んでいる範囲

#9の動作確認

  • 再生すると「あなたのターン」の帯が出てから、5枚配られる
  • ターン終了を押すと、手札が捨て札へ行き、ボタンが押せなくなって「敵のターン」の帯が出る
  • 敵が左から1体ずつ、予告どおりに行動する(ブリキロボは6の攻撃、びっくり箱は1ターン目はためる)
  • 行動したあと、予告が次の行動に変わる(ブリキロボは6、びっくり箱は16)
  • 防御があると、敵の攻撃を先に受けとめる。次のあなたのターンで、防御は0に戻る
  • 2ターン目のびっくり箱の16の攻撃は、防御カードで備えないと大きく減る
スライム

やっと戦えるようになりました!でも、なんだか静かですね・・・

フェアリー

そう、攻撃しても数字が変わるだけだもんね。次の2回で「手応え」を作るよ。反応が返ってくると、同じルールでも別のゲームみたいに気持ちよくなるんだ

あとがき

今回は、ターンの流れを作って、敵が予告どおりに攻撃してくるようになりました。予告を見て「次は16か・・・ふでばこの壁を残しておこう」と考えられるようになり、ぐっとデッキ構築ゲームらしくなりましたね。次回は、攻撃したときに敵が光ってのけぞり、ダメージの数字が飛び出す「攻撃の手応え」を作りたいと思います!

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

コメント

コメント一覧 (1件)

コメントする

目次