デッキ構築カードゲームの作り方#7|エネルギーとカードの効果を作ろう

ホチキスでパチンでブリキロボのHPが28から16に、ハサミでチョキチョキで2回に分けて10に減り、エネルギーが0になる。暗くなった定規でペシッを使おうとするとエネルギーの丸が震えて手札に戻り、コスト0のコンパスでチクッはHP6まで減らせる

前回で、カードをドラッグして使えるようになりました。でも、まだ使っても何も起きません・・・。今回は、1ターンに使えるエネルギーと、攻撃・防御・回復などのカードの効果を作ります。効果は、6-14 クラスを受け継ごう:継承とabstractの応用編と同じ形で、「効果の共通の形」を決めて、中身をクラスごとに分けて作ってみたいと思います!

目次

今回作るもの

  • エネルギー:毎ターン3。カードを使うとコストの分だけ減る
  • エネルギーが足りないカードは暗くなり、使おうとするとエネルギーの表示が震える
  • スライムちゃんと敵のHPと防御
  • カードの効果:ダメージ・防御・回復・カードを引く・エネルギーを増やす
  • 新しいカード6枚(全部で9種類)
スクリプト役割付ける場所
Energyエネルギーの残りと表示。足りないときに震えるEnergy
FighterHPと防御。ダメージ・防御・回復を受けるPlayerArea・Enemy
CardEffect効果の共通の形(abstract)(付けない。アセットを作る)
DamageEffect ほか4つ効果の中身(付けない。アセットを作る)
CardData(直す)効果のリストを持つ-
CardView(直す)エネルギーが足りないカードを暗くするCard(プレハブ)
PlayZone(直す)エネルギーを払って、効果を出すStage

HPはまだ文字で出すだけです。HPのゲージと敵の行動の予告は#8、攻撃や回復の演出は#10・#11で作ります。

前回の続きから

前回の最後の状態から始めます。新しいカードの絵は、素材のzipを入れ直すと入ります(DeckBuilding/Sprites/Cardsに6枚増えます)。

エネルギーを作ろう

まずはエネルギーです。#1で左下に置いたオレンジの丸(Energy)に付けます。

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

// 1ターンに使えるエネルギー。カードを使うとコストの分だけ減る
public class Energy : MonoBehaviour
{
    [SerializeField] private int maxEnergy = 3;
    [SerializeField] private TextMeshProUGUI energyText;

    private int current;
    private Vector2 basePosition;

    public int Current => current;

    private void Start()
    {
        basePosition = ((RectTransform)transform).anchoredPosition;
        Refill();
    }

    // ターンの始めに、最大まで戻す
    public void Refill()
    {
        current = maxEnergy;
        UpdateText();
    }

    public bool CanPay(int cost)
    {
        return current >= cost;
    }

    public void Pay(int cost)
    {
        current -= cost;
        UpdateText();
    }

    public void Gain(int amount)
    {
        current += amount;
        UpdateText();
    }

    // 足りないときに、ブルッと震える
    public void Shake()
    {
        StopAllCoroutines();
        StartCoroutine(ShakeRoutine());
    }

    private IEnumerator ShakeRoutine()
    {
        RectTransform rt = (RectTransform)transform;
        float time = 0f;
        while (time < 0.3f)
        {
            time += Time.deltaTime;
            rt.anchoredPosition = basePosition + new Vector2(Mathf.Sin(time * 80f) * 10f, 0f);
            yield return null;
        }
        rt.anchoredPosition = basePosition;
    }

    private void UpdateText()
    {
        energyText.text = $"{current}/{maxEnergy}";
    }
}
  • CanPay・Pay・Gain
    • 「払えるか」「払う」「増やす」を、それぞれメソッドにしておく。エネルギーの数字を直接さわるのはEnergyだけにしておくと、表示の更新を忘れない
  • Shake
    • Mathf.Sinで左右に行ったり来たりさせて、0.3秒で元の位置に戻す
    • 震えている途中でもう一度呼ばれても、StopAllCoroutinesで前の震えを止めてからやり直すので、位置がずれない

エネルギーを付けます。

  • HierarchyのHud>EnergyにEnergyを付ける
    • Max Energy:3、Energy Text:Energyの子のText (TMP)
  • EndTurnButtonのOn Click ()の+を押して、3つ目にEnergyをドラッグしてEnergy > Refill ()
    • ターン終了を押すと、手札が入れかわって、エネルギーが3に戻る
左はEnergyのInspector(Max Energy 3、Energy TextにText (TMP))、右はEndTurnButtonのOn Click ()(Deck.DiscardHand、Deck.Draw 5、Energy.Refill)

HPと防御を持たせよう

次に、スライムちゃんと敵のHPと防御です。どちらも「ダメージを受ける」「防御を得る」「回復する」は同じなので、1つのスクリプト(Fighter)を両方に付けます。

  • DeckBuilding/Scriptsに「Fighter」スクリプトを作る
using TMPro;
using UnityEngine;

// HP と防御を持って、ダメージ・防御・回復を受ける(スライムちゃんと敵の共通部分)
public class Fighter : MonoBehaviour
{
    [SerializeField] private string fighterName = "スライムちゃん";
    [SerializeField] private int maxHp = 60;
    [SerializeField] private TextMeshProUGUI statusText; // 「HP 60/60 防御 5」の表示

    private int hp;
    private int block; // 防御:ダメージを先に受けとめてくれる

    public string FighterName => fighterName;
    public int Hp => hp;

    private void Awake()
    {
        hp = maxHp;
    }

    private void Start()
    {
        UpdateText();
    }

    public void TakeDamage(int amount)
    {
        // 先に防御で受けとめて、残りだけ HP を減らす
        int blocked = Mathf.Min(block, amount);
        block -= blocked;
        int damage = amount - blocked;
        hp = Mathf.Max(0, hp - damage);
        Debug.Log($"{fighterName}に{amount}ダメージ(防御で{blocked}防いだ) HP {hp}/{maxHp}");
        UpdateText();
    }

    public void AddBlock(int amount)
    {
        block += amount;
        Debug.Log($"{fighterName}の防御 +{amount} 防御 {block}");
        UpdateText();
    }

    public void Heal(int amount)
    {
        hp = Mathf.Min(maxHp, hp + amount);
        Debug.Log($"{fighterName}のHPが{amount}回復 HP {hp}/{maxHp}");
        UpdateText();
    }

    private void UpdateText()
    {
        statusText.text = block > 0 ? $"HP {hp}/{maxHp} 防御 {block}" : $"HP {hp}/{maxHp}";
    }
}
  • TakeDamage
    • ダメージは、先に防御で受けとめる。防御5で6ダメージを受けると、防御が0になって、HPは1だけ減る
    • Mathf.Max(0, …)で、HPが0より下にならないようにする
  • Heal
    • Mathf.Min(maxHp, …)で、最大HPをこえないようにする
  • statusText
    • 防御が0のときは「HP 60/60」、あるときは「HP 60/60 防御 5」と出す
防御5の盾に6ダメージが当たり、5は盾で受けとめて、残りの1だけがHPに届く。防御5・HP 60 → 6ダメージ → 防御0・HP 59

同じFighterを、スライムちゃんにも敵にも付けます。「HPと防御を持ってダメージを受ける」のは同じなので、スクリプトは1つで済みます。ちがうのはInspectorで入れる名前と最大HPだけです。

HPの表示を作って、Fighterを付けます。

  • PlayerAreaの子にStatusText(Text – TextMeshPro)
    • アンカー:bottom-center、Pos Y:10、Width:320、Height:50
    • テキスト:「HP 60/60」(再生するとスクリプトが書きかえる)、Font Size:32、Alignment:中央、Color:#3C2F2F
  • Enemyの子にStatusText(Text – TextMeshPro)
    • アンカー:bottom-center、Pos Y:-64(名前の下)、Width:260、Height:40
    • テキスト:「HP 28/28」、Font Size:28、Alignment:中央、Color:#3C2F2F
  • PlayerAreaにFighterを付ける
    • Fighter Name:スライムちゃん、Max Hp:60、Status Text:PlayerAreaの子のStatusText
  • EnemyにFighterを付ける
    • Fighter Name:ブリキロボ、Max Hp:28、Status Text:Enemyの子のStatusText
EnemyのFighter(Fighter Name:ブリキロボ、Max Hp:28、Status Text)と、スライムちゃんの下にHP 60/60、ブリキロボの下にHP 28/28

効果の共通の形を作ろう

いよいよカードの効果です。効果には「ダメージ」「防御」「回復」「カードを引く」「エネルギー」といろいろありますが、カードを使う側から見ると、どれも「効果を出す」だけです。そこで、効果の共通の形をabstractのクラスで決めておきます。

  • DeckBuilding/Scriptsに「CardEffect」スクリプトを作る
using UnityEngine;

// カードの効果の共通の形。中身は子クラスで決める
public abstract class CardEffect : ScriptableObject
{
    // player:スライムちゃん enemy:ねらった敵(攻撃のカードのとき。ほかは null)
    public abstract void Apply(Fighter player, Fighter enemy, Deck deck, Energy energy);
}
  • abstract class ・ ScriptableObject
    • 6-14の応用編と同じく、「アセットにできる」「中身は子クラスにまかせる」の2つを合わせた形
  • Apply の4つの引数
    • 効果によって、さわる相手がちがう(ダメージは敵、防御と回復はスライムちゃん、カードを引くのはDeck、エネルギーはEnergy)。必要になりそうなものを全部渡しておき、それぞれの効果は、使うものだけ使う

5つの効果を作ろう

子クラスを5つ作ります。どれも短いスクリプトです。1つのファイルに1つのクラスで作り、ファイル名とクラス名をそろえましょう。

using UnityEngine;

// 敵にダメージを与える(hits 回くり返す)
[CreateAssetMenu(fileName = "DamageEffect", menuName = "DeckBuilding/Effect/Damage")]
public class DamageEffect : CardEffect
{
    [SerializeField] private int amount = 6;
    [SerializeField] private int hits = 1;

    public override void Apply(Fighter player, Fighter enemy, Deck deck, Energy energy)
    {
        if (enemy == null)
        {
            return;
        }
        for (int i = 0; i < hits; i++)
        {
            enemy.TakeDamage(amount);
        }
    }
}
using UnityEngine;

// スライムちゃんが防御を得る
[CreateAssetMenu(fileName = "BlockEffect", menuName = "DeckBuilding/Effect/Block")]
public class BlockEffect : CardEffect
{
    [SerializeField] private int amount = 5;

    public override void Apply(Fighter player, Fighter enemy, Deck deck, Energy energy)
    {
        player.AddBlock(amount);
    }
}
using UnityEngine;

// スライムちゃんの HP を回復する
[CreateAssetMenu(fileName = "HealEffect", menuName = "DeckBuilding/Effect/Heal")]
public class HealEffect : CardEffect
{
    [SerializeField] private int amount = 5;

    public override void Apply(Fighter player, Fighter enemy, Deck deck, Energy energy)
    {
        player.Heal(amount);
    }
}
using UnityEngine;

// 山札からカードを引く
[CreateAssetMenu(fileName = "DrawEffect", menuName = "DeckBuilding/Effect/Draw")]
public class DrawEffect : CardEffect
{
    [SerializeField] private int count = 2;

    public override void Apply(Fighter player, Fighter enemy, Deck deck, Energy energy)
    {
        deck.Draw(count);
    }
}
using UnityEngine;

// エネルギーを増やす
[CreateAssetMenu(fileName = "EnergyEffect", menuName = "DeckBuilding/Effect/Energy")]
public class EnergyEffect : CardEffect
{
    [SerializeField] private int amount = 1;

    public override void Apply(Fighter player, Fighter enemy, Deck deck, Energy energy)
    {
        energy.Gain(amount);
    }
}
  • どれも: CardEffectを受け継いで、Applyをoverrideしているだけ
  • DamageEffectのhits
    • 同じダメージを何回与えるか。「ハサミでチョキチョキ」の「3ダメージを2回」に使う。1回ずつTakeDamageを呼ぶので、防御は1回目で削られて、2回目はそのままHPに届く
スライム

同じ形のスクリプトが5つも並ぶと、ちょっと多く感じますね・・・

フェアリー

でも、あとで新しい効果を足すときは「1つ増やすだけ」で、ほかは何も直さなくていいんだ。#13で特殊なカードを足すときに、このありがたさが分かるよ!

カードに効果を持たせよう

CardDataに効果のリストを足す

#2で作ったCardDataに、効果のリストを1行足します。

using System.Collections.Generic;
using UnityEngine;

// カードの種類(枠の色や、使う相手を決めるときに使う)
public enum CardType
{
    Attack,  // 攻撃
    Defense, // 防御
    Heal,    // 回復
    Special  // 特殊
}

// カード1枚分のデータ。Project ウインドの Create>DeckBuilding>Card Data でアセットを作れる
[CreateAssetMenu(fileName = "NewCard", menuName = "DeckBuilding/Card Data")]
public class CardData : ScriptableObject
{
    public string cardName;               // 画面に出す名前
    [Min(0)] public int cost = 1;         // 使うのに要るエネルギー
    public CardType cardType;             // 種類
    [TextArea] public string description; // 説明
    public Sprite artwork;                // カードの絵
    public List<CardEffect> effects = new List<CardEffect>(); // ★#7 使ったときの効果(上から順に出す)
}

効果のアセットを作る

  • DeckBuilding>Dataの中に「Effects」フォルダを作る
  • 右クリック>Create>DeckBuilding>Effect から、次の9つを作る
アセットの名前作るメニュー値
Damage6DamageAmount:6、Hits:1
Damage3x2DamageAmount:3、Hits:2
Damage4DamageAmount:4、Hits:1
Damage12DamageAmount:12、Hits:1
Block5BlockAmount:5
Block12BlockAmount:12
Heal5HealAmount:5
Draw2DrawCount:2
Energy1EnergyAmount:1
Data>Effectsに9つの効果のアセット。Damage3x2のAmountは3、Hitsは2

カードに効果を入れる・カードを増やす

今までの3枚に効果を入れて、新しいカードを6枚作ります(Data>Cardsで右クリック>Create>DeckBuilding>Card Data)。Effectsの「+」で欄を足して、効果のアセットをドラッグ&ドロップします。

アセットの名前Card NameCostCard TypeDescriptionArtworkEffects
Ruler定規でペシッ1Attack敵に6ダメージcard_rulerDamage6
Scissorsハサミでチョキチョキ1Attack3ダメージを2回card_scissorsDamage3x2
Compassコンパスでチクッ0Attack敵に4ダメージcard_compassDamage4
Staplerホチキスでパチン2Attack敵に12ダメージcard_staplerDamage12
PencilBoard下じきガード1Defense防御を5得るcard_pencilboardBlock5
PencilCaseふでばこの壁2Defense防御を12得るcard_pencilcaseBlock12
Bandageばんそうこう1HealHPを5回復するcard_bandageHeal5
StickyNote付せんメモ1Specialカードを2枚引くcard_stickynoteDraw2
ColorPencils色えんぴつセット0Specialエネルギーを1増やすcard_colorpencilsEnergy1
ScissorsのInspector。ハサミでチョキチョキ、Cost 1、Attack、3ダメージを2回、EffectsにDamage3x2

「3ダメージを2回」は、新しいスクリプトを書かずに、DamageEffectのアセットの数字を変えるだけで作れました。効果を1つ作っておくと、数字を変えたアセットを増やすだけで、いろいろなカードが作れます。

カードを使ったら効果を出そう

最後に、#6のPlayZoneを直します。カードを使う前にエネルギーを確かめて、使ったら効果を順番に出します。

using UnityEngine;
using UnityEngine.EventSystems;

// Stage でカードを離したときに、使うかどうかを決める
public class PlayZone : MonoBehaviour, IDropHandler
{
    [SerializeField] private Deck deck;
    [SerializeField] private Energy energy;  // ★#7
    [SerializeField] private Fighter player; // ★#7 スライムちゃん

    public void OnDrop(PointerEventData eventData)
    {
        if (eventData.pointerDrag == null)
        {
            return;
        }
        CardDrag card = eventData.pointerDrag.GetComponent<CardDrag>();
        if (card == null)
        {
            return;
        }
        CardData data = card.View.Data;

        // ★#7 エネルギーが足りないカードは使えない
        if (!energy.CanPay(data.cost))
        {
            energy.Shake();
            return;
        }

        Fighter enemy = null;
        string targetName = player.FighterName;
        if (card.IsAttack)
        {
            // 攻撃のカードは、敵の上で離したときだけ使う
            GameObject over = eventData.pointerCurrentRaycast.gameObject;
            EnemyTarget target = over != null ? over.GetComponentInParent<EnemyTarget>() : null;
            if (target == null)
            {
                return; // 敵の上ではないので使わない(手札に戻る)
            }
            enemy = target.GetComponent<Fighter>();
            targetName = target.EnemyName;
        }

        card.MarkUsed();
        energy.Pay(data.cost);             // ★#7 エネルギーを払う
        deck.UseCard(card.View, targetName);

        // ★#7 カードの効果を、上から順番に出す
        foreach (CardEffect effect in data.effects)
        {
            effect.Apply(player, enemy, deck, energy);
        }
    }
}
  • energy.CanPay → Shake
    • 足りなければ、エネルギーを震わせて何もしない。MarkUsedを呼ばないので、カードは手札に戻る
  • foreach (CardEffect effect in data.effects)
    • ここが今回のいちばんのポイント。PlayZoneは、効果が「ダメージ」なのか「カードを引く」なのかを知らない。CardEffectとしてApplyを呼ぶだけで、それぞれの効果が自分の仕事をしてくれる
  • UseCardのあとにApply
    • 先に手札から捨て札へ送ってから効果を出す。「付せんメモ」で2枚引くとき、使った付せんメモが手札に残っていると、引いた数がずれてしまうため

StageのPlay Zoneに、EnergyとPlayerを入れます。

  • Energy:Hud>Energy、Player:PlayerArea

カードのEffectsが空のままだと、エネルギーは減るのに何も起きません。「使ったのに効果が出ない」ときは、まずそのカードのEffectsに効果のアセットが入っているかを確かめましょう。

使えないカードを暗くしよう

エネルギーが足りないカードは、見ただけで分かるように暗くします。#3のCardViewに足します。

using TMPro;
using UnityEngine;
using UnityEngine.UI;

// カードのデータ(CardData)を、見た目に反映する
public class CardView : MonoBehaviour
{
    [SerializeField] private CardData cardData; // 表示するカード

    [Header("表示する部品")]
    [SerializeField] private Image frameImage;
    [SerializeField] private Image artworkImage;
    [SerializeField] private TextMeshProUGUI nameText;
    [SerializeField] private TextMeshProUGUI costText;
    [SerializeField] private TextMeshProUGUI descriptionText;

    [Header("種類ごとの枠の色")]
    [SerializeField] private Color attackColor = new Color(0.91f, 0.42f, 0.36f);  // 攻撃:赤
    [SerializeField] private Color defenseColor = new Color(0.36f, 0.61f, 0.88f); // 防御:青
    [SerializeField] private Color healColor = new Color(0.42f, 0.77f, 0.48f);    // 回復:緑
    [SerializeField] private Color specialColor = new Color(0.65f, 0.48f, 0.84f); // 特殊:紫

    private CanvasGroup canvasGroup; // ★#7 暗くするのに使う
    private Energy energy;           // ★#7 エネルギーが足りるか見る

    public CardData Data => cardData;

    private void Awake()
    {
        canvasGroup = GetComponent<CanvasGroup>(); // ★#7
    }

    private void Start()
    {
        energy = FindFirstObjectByType<Energy>(); // ★#7

        // Inspector でデータを入れておいたときは、そのカードを表示する
        if (cardData != null)
        {
            Setup(cardData);
        }
    }

    // ★#7 エネルギーが足りないカードは、半分すける
    private void Update()
    {
        if (canvasGroup == null || energy == null || cardData == null)
        {
            return;
        }
        canvasGroup.alpha = energy.CanPay(cardData.cost) ? 1f : 0.5f;
    }

    // データを受け取って、見た目を書き換える
    public void Setup(CardData data)
    {
        cardData = data;
        nameText.text = data.cardName;
        costText.text = data.cost.ToString();
        descriptionText.text = data.description;
        artworkImage.sprite = data.artwork;
        frameImage.color = FrameColor(data.cardType);
    }

    // 種類ごとの枠の色
    private Color FrameColor(CardType type)
    {
        switch (type)
        {
            case CardType.Attack:
                return attackColor;
            case CardType.Defense:
                return defenseColor;
            case CardType.Heal:
                return healColor;
            case CardType.Special:
                return specialColor;
            default:
                return Color.gray;
        }
    }
}
  • canvasGroup.alpha
    • Canvas Groupのアルファは、子のImageやテキストも全部まとめてすけさせる。カードのプレハブには、#6でCardDragを付けたときにCanvas Groupが付いています
  • FindFirstObjectByType()
    • シーンの中からEnergyを探す。カードはスクリプトから作るプレハブなので、Inspectorでシーンのものを入れておけないため、Startで探しています

動かして確認!

新しいカードは、まだデッキに入っていません(報酬でもらえるのは#12から)。確かめるときは、DeckのStarting Deckに1枚ずつ足してみましょう。

  • 例:Starting Deckを、Ruler・PencilBoard・Bandage・Scissors・Compass・Stapler・PencilCase・StickyNote・ColorPencilsの1枚ずつ(9行・Countは全部1)にする
  • 確かめ終わったら、元の3行(Ruler 5・PencilBoard 4・Bandage 1)に戻す
ホチキスでパチンでブリキロボのHPが28から16に、ハサミでチョキチョキで2回に分けて10に減り、エネルギーが0になる。暗くなった定規でペシッを使おうとするとエネルギーの丸が震えて手札に戻り、コスト0のコンパスでチクッはHP6まで減らせる
Consoleに、ホチキスで12ダメージ、ハサミで3ダメージが2回、コンパスで4ダメージのログが並ぶ

#7の動作確認

  • 再生すると、エネルギーが3/3、スライムちゃんがHP 60/60、ブリキロボがHP 28/28
  • カードを使うと、コストの分だけエネルギーが減る
  • エネルギーが足りないカードは暗くなり、使おうとするとエネルギーの丸が震えて、手札に戻る
  • 攻撃のカードで敵のHPが減る。ハサミでチョキチョキは2回に分けて減る
  • 下じきガードで「防御 5」が付き、ばんそうこうでHPが回復する(最大HPはこえない)
  • 付せんメモで2枚引ける、色えんぴつセットでエネルギーが1増える
  • ターン終了で、エネルギーが3に戻る

今はまだ、防御がターンをまたいで残ります。「ターンの始めに防御を0に戻す」のは、ターンの流れを作る#9で入れます。

あとがき

今回は、エネルギーと、カードの効果を作りました。効果を「共通の形+中身のクラス」に分けておいたので、カードが増えても、PlayZoneは「Applyを呼ぶ」だけのままです。次回は、敵のHPのゲージと、敵が次にすることを予告する表示を作りたいと思います!

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

コメント

コメント一覧 (1件)

コメントする

目次