デッキ構築カードゲームの作り方#8|HPのゲージと敵の行動の予告を作ろう

スライムちゃんの下にHPのゲージ、ブリキロボとびっくり箱の頭の上に剣と6、…の予告がポンと出る。定規でペシッでブリキロボのゲージが22/28、びっくり箱が30/36に縮み、下じきガードでスライムちゃんのゲージの横に盾と5が出る

デッキ構築ゲームの面白さは、「敵が次に何をしてくるか分かっていて、それを見て攻めるか守るかを決める」ところにあります。今回は、スライムちゃんと敵のHPのゲージを作り、敵のデータをScriptableObjectにして、敵を2体並べ、次の行動の予告を頭の上に出してみたいと思います!

目次

今回作るもの

  • スライムちゃんと敵のHPのゲージ(防御があるときは盾と数字)
  • 敵1種類のデータ(名前・絵・最大HP・行動の並び)
  • 敵のプレハブ:HPのゲージと、次の行動の予告
  • 敵のデータのリストから、敵を横に並べて作る
  • 2体目の敵「びっくり箱」
スクリプト役割付ける場所
HpGaugeHPをゲージと数字で、防御を盾で表示するPlayerArea・Enemy
EnemyData敵1種類のデータ(付けない。アセットを作る)
EnemyView敵の絵・名前と、次の行動の予告を表示するEnemy(プレハブ)
EnemyParty敵を横に並べて作るEnemyArea
Fighter(直す)データから名前と最大HPを入れられるようにする-
EnemyTarget(直す)名前をFighterから取る-

予告が出るところまで作ります。敵が予告どおりに行動するのは、ターンの流れを作る#9です。

前回の続きから

前回の最後の状態から始めます。びっくり箱の絵と予告のアイコン、ゲージ用の白い四角は、素材のzipを入れ直すと入ります。

  • DeckBuildingAssets.zip
    • DeckBuilding/Sprites/Enemies/enemy_jackbox.png(びっくり箱)
    • DeckBuilding/Sprites/Icons/icon_attack.png・icon_block.png・icon_charge.png(剣・盾・ためる)
    • DeckBuilding/Sprites/UI/ui_white.png(まっさらな白い四角。HPのゲージに使う)
    • 自分で入れた場合は、Sprite Mode:Singleにして Apply

Fighterを直そう

#7のFighterは、名前と最大HPをInspectorで決めて、HPを文字(StatusText)で出していました。今回からHPはゲージで出すので、文字の表示は消します。また、敵はデータから作るので、名前と最大HPをスクリプトから入れられるようにします。直したところには★#8と書いてあります。

using UnityEngine;

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

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

    public string FighterName => fighterName;
    public int Hp => hp;
    public int MaxHp => maxHp; // ★#8 ゲージで使う
    public int Block => block; // ★#8 ゲージで使う

    private void Awake()
    {
        hp = maxHp;
    }

    // ★#8 名前と最大HPを入れて、HPを満タンにする(敵をデータから作るときに使う)
    public void Setup(string newName, int newMaxHp)
    {
        fighterName = newName;
        maxHp = newMaxHp;
        hp = maxHp;
        block = 0;
    }

    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}");
    }

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

    public void Heal(int amount)
    {
        hp = Mathf.Min(maxHp, hp + amount);
        Debug.Log($"{fighterName}のHPが{amount}回復 HP {hp}/{maxHp}");
    }
}
  • statusText・UpdateTextを消した
    • HPの表示は、このあと作るHpGaugeにまかせる。Fighterは「HPと防御の数字を持つ」だけにして、見せ方は別のスクリプトに分けておくと、ゲージを演出付きに変えたいとき(#11)もFighterはさわらずに済みます
  • MaxHp・Block
    • ゲージと防御の盾を出すために、ほかのスクリプトから読めるようにした
  • Setup
    • 名前と最大HPを入れて、HPを満タン・防御を0にする

Fighterから文字の表示を消したので、#7で作ったPlayerAreaのStatusTextは消しておきましょう。

HPのゲージを作ろう

ゲージの部品を作る

まずは敵のゲージから作ります。#6で置いたEnemyの子に、次のものを作ります(#7でEnemyに作ったStatusTextは消します)。

  • HpBar(UI (Canvas)>Image)
    • アンカー:bottom-center、Pos Y:-68、Width:200、Height:34
    • Color:#3C2F2F(こげ茶。ゲージの背景)
  • HpBarの子にHpFill(UI (Canvas)>Image)
    • アンカー:stretch-stretch、Left・Top・Right・Bottom:3
    • Source Image:ui_white(素材のzipのDeckBuilding/Sprites/UIにある白い四角。空だとImage Typeが選べません)、Image Type:Filled、Fill Method:Horizontal、Fill Origin:Left
    • Color:#E8584C(赤)
  • HpBarの子にHpText(Text – TextMeshPro)
    • アンカー:stretch-stretch、Left・Top・Right・Bottom:0
    • テキスト:「28/28」、Font Size:26、Font Style:B、Alignment:中央、Color:白
  • BlockBadge(UI (Canvas)>Image)
    • アンカー:bottom-center、Pos X:-126、Pos Y:-68、Width・Height:40
    • Source Image:icon_block、Raycast Targetのチェックを外す
  • BlockBadgeの子にBlockText(Text – TextMeshPro)
    • アンカー:stretch-stretch、テキスト:「0」、Font Size:22、Font Style:B、Alignment:中央、Color:白
HpFillのImage。Source Imageがui_white、Image TypeがFilled、Fill MethodがHorizontal、Fill OriginがLeft

Image TypeをFilledにすると、Fill Amount(0〜1)で「どこまで塗るか」を変えられます。HPが半分なら0.5にすると、ゲージが半分になります。ゲージの作り方は、ペット育成ゲームの作り方#1でも紹介しています。

ゲージには、角の丸いUISpriteではなく、白い四角(ui_white)を使います。Filledは画像を切り分けずにそのまま伸ばすので、角の丸い画像だとゲージの両端が丸くつぶれて、見苦しくなります。色はImageのColorで付けるので、画像は白い四角が1枚あれば、どの色のゲージにも使えます。

HpGaugeを作る

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

// Fighter の HP をゲージと数字で、防御を盾と数字で表示する(スライムちゃんと敵の共通部分)
[RequireComponent(typeof(Fighter))]
public class HpGauge : MonoBehaviour
{
    [SerializeField] private Image hpFill;
    [SerializeField] private TextMeshProUGUI hpText;
    [SerializeField] private GameObject blockBadge;
    [SerializeField] private TextMeshProUGUI blockText;

    private Fighter fighter;

    private void Awake()
    {
        fighter = GetComponent<Fighter>();
    }

    private void Update()
    {
        // HP のゲージと数字
        hpFill.fillAmount = (float)fighter.Hp / fighter.MaxHp;
        hpText.text = $"{fighter.Hp}/{fighter.MaxHp}";

        // 防御は、あるときだけ盾を出す
        blockBadge.SetActive(fighter.Block > 0);
        blockText.text = fighter.Block.ToString();
    }
}
  • (float)fighter.Hp / fighter.MaxHp
    • HPの割合(0〜1)。intどうしの割り算は小数が切り捨てられて0になってしまうので、片方を(float)にしてから割る
  • blockBadge.SetActive
    • 防御が0のときは盾ごと隠す。防御があるときだけ出るので、「今、防御がある」ことが見ただけで分かる
  • [RequireComponent(typeof(Fighter))]
    • Fighterと同じオブジェクトに付ける決まりにしておく。Fighterが無いところに付けると、自動でFighterも付く

EnemyにHpGaugeを付けて、Hp Fill:HpFill、Hp Text:HpText、Block Badge:BlockBadge、Block Text:BlockTextを入れます。

スライムちゃんにも付ける

スライムちゃんのゲージも、同じ部品で作れます。敵のゲージを複製して持ってきましょう。

  • HierarchyでEnemyの子のHpBarとBlockBadgeを選んで、Ctrl+D(MacはCmd+D)で複製
  • 複製した2つを、PlayerAreaの上にドラッグ&ドロップして、PlayerAreaの子にする
  • 名前の「(1)」を消して、位置を変える
    • HpBar:Pos Y:20、Width:240、Height:38
    • BlockBadge:Pos X:-150、Pos Y:20
    • HpText のテキスト:「60/60」、Font Size:28
  • PlayerAreaにHpGaugeを付けて、PlayerAreaの子のHpFill・HpText・BlockBadge・BlockTextを入れる
PlayerAreaの下にSlime・HpBar(HpFill・HpText)・BlockBadge(BlockText)。PlayerAreaのHp Gaugeに4つの部品が入り、スライムちゃんの下にゲージ60/60
スライム

わたしのゲージも、敵と同じ見た目になりました!

フェアリー

HpGaugeはFighterを読むだけだから、どっちに付けても同じように動くんだ。#9で敵に攻撃されると、このゲージが減っていくよ

敵のデータを作ろう

カードと同じように、敵もデータをScriptableObjectにしておきます(12-8 ScriptableObjectでデータを作ろう)。

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

// 敵1種類のデータ。Project ウインドの Create>DeckBuilding>Enemy Data でアセットを作れる
[CreateAssetMenu(fileName = "NewEnemy", menuName = "DeckBuilding/Enemy Data")]
public class EnemyData : ScriptableObject
{
    // 敵の1回分の行動(攻撃と防御。どちらも0なら「ためる」)
    [System.Serializable]
    public class EnemyAction
    {
        [Min(0)] public int attack;
        [Min(0)] public int block;
    }

    public string enemyName;         // 画面に出す名前
    public Sprite sprite;            // 絵
    [Min(1)] public int maxHp = 20;  // 最大HP
    public List<EnemyAction> actions = new List<EnemyAction>(); // 行動の並び(上から順にくり返す)
}
  • EnemyAction
    • 敵の1回分の行動。「攻撃いくつ」と「防御いくつ」の組にしておき、どちらも0なら「ためる」(何もしない)として扱う
  • actions
    • 行動を上から順に使って、最後まで行ったら最初に戻る。「攻撃・攻撃・大きい攻撃」のような、敵ごとのくせを表せる

アセットを作ります。

  • DeckBuilding>Dataの中に「Enemies」フォルダを作り、右クリック>Create>DeckBuilding>Enemy Data で2つ作る
アセットの名前Enemy NameSpriteMax HpActions(上から)
TinRobotブリキロボenemy_tinrobot28攻撃6 → 攻撃6 → 攻撃9
JackBoxびっくり箱enemy_jackbox36ためる(0・0) → 攻撃16
JackBoxのInspector。Enemy Name:びっくり箱、Sprite:enemy_jackbox、Max Hp:36、ActionsはElement 0がAttack 0・Block 0、Element 1がAttack 16・Block 0
スライム

びっくり箱は、1ターン目に何もしないんですか?

フェアリー

そう、ためてから大きい一撃!予告で「…」が見えたら、次のターンに備えて防御カードを取っておく、みたいに考えられるのがこのジャンルの楽しいところだよ

行動の予告を作ろう

Enemyの子に、頭の上の予告を作ります。

  • Intent(Create Empty)
    • アンカー:top-center、Pos Y:50、Width:140、Height:60
  • Intentの子にIntentIcon(UI (Canvas)>Image)
    • アンカー:middle-center、Pos X:-28、Width・Height:56、Source Image:icon_attack、Raycast Targetのチェックを外す
  • Intentの子にIntentText(Text – TextMeshPro)
    • アンカー:middle-center、Pos X:34、Width:70、Height:60
    • テキスト:「6」、Font Size:44、Font Style:B、Alignment:左寄せ・上下中央、Color:#C0392B(こい赤)

これで、敵の部品がそろいました。

  • Enemy(敵の絵)
    • TargetFrame(ねらいの枠。#6)
    • NameText(名前。#6)
    • HpBar(ゲージの背景)
      • HpFill(ゲージの中身)
      • HpText(「28/28」)
    • BlockBadge(防御の盾)
      • BlockText
    • Intent(頭の上の予告)
      • IntentIcon(剣・盾・…)
      • IntentText(数字)
敵のプレハブのHierarchy。Enemyの下にTargetFrame・NameText・HpBar(HpFill・HpText)・BlockBadge(BlockText)・Intent(IntentIcon・IntentText)

EnemyViewを作る

敵の絵と名前を入れて、次の行動の予告を出すスクリプトです。

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

// 敵の見た目:絵・名前と、次の行動の予告(HP のゲージは HpGauge)
[RequireComponent(typeof(Fighter))]
public class EnemyView : MonoBehaviour
{
    [SerializeField] private Image bodyImage;
    [SerializeField] private TextMeshProUGUI nameText;

    [Header("行動の予告")]
    [SerializeField] private RectTransform intent;
    [SerializeField] private Image intentIcon;
    [SerializeField] private TextMeshProUGUI intentText;
    [SerializeField] private Sprite attackIcon;
    [SerializeField] private Sprite blockIcon;
    [SerializeField] private Sprite chargeIcon;

    private EnemyData data;
    private Fighter fighter;
    private int actionIndex; // 次に使う行動の番号

    public EnemyData Data => data;
    public Fighter Fighter => fighter;
    public EnemyData.EnemyAction NextAction => data.actions[actionIndex];

    private void Awake()
    {
        fighter = GetComponent<Fighter>();
    }

    // データを受け取って、見た目と HP を入れる
    public void Setup(EnemyData enemyData)
    {
        data = enemyData;
        bodyImage.sprite = data.sprite;
        nameText.text = data.enemyName;
        fighter.Setup(data.enemyName, data.maxHp);
        actionIndex = 0;
        ShowIntent();
    }

    // 次の行動の予告を出す
    private void ShowIntent()
    {
        EnemyData.EnemyAction action = NextAction;
        if (action.attack > 0)
        {
            intentIcon.sprite = attackIcon;
            intentText.text = action.attack.ToString();
        }
        else if (action.block > 0)
        {
            intentIcon.sprite = blockIcon;
            intentText.text = action.block.ToString();
        }
        else
        {
            intentIcon.sprite = chargeIcon; // ためる
            intentText.text = "";
        }
        intent.localScale = Vector3.one * 1.6f; // ポンと大きく出して、Update で元の大きさに戻す
    }

    private void Update()
    {
        // 予告の大きさを、1へなめらかに戻す
        intent.localScale = Vector3.Lerp(intent.localScale, Vector3.one, Mathf.Min(1f, 12f * Time.deltaTime));
    }
}
  • Setup
    • データの絵と名前を表示して、FighterにHPを入れる。最後に予告を出す(ゲージはHpGaugeが勝手に合わせてくれる)
  • ShowIntent
    • 次の行動が攻撃なら剣と数字、防御なら盾と数字、どちらも0なら「…」(ためる)
    • intent.localScaleを1.6倍にしてから、Updateで1へ戻すと、予告がポンと飛び出すように見える
  • NextAction
    • 次に使う行動。#9で、敵のターンにこの行動をして、次の行動へ進めます

EnemyDataのActionsが空のままだと、NextActionでArgumentOutOfRangeExceptionのエラーになります。敵のデータには、行動を1つ以上入れておきましょう。

EnemyTargetを直す

#6のEnemyTargetは、名前をInspectorで入れていました。名前はFighterが持っているので、そこから取るように直します。

using UnityEngine;
using UnityEngine.EventSystems;

// 攻撃のカードを持って上に来たら、ねらっている印の枠を出す
public class EnemyTarget : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler
{
    [SerializeField] private GameObject targetFrame; // ねらっている印の枠

    public string EnemyName => GetComponent<Fighter>().FighterName; // ★#8 名前は Fighter から取る

    private void Start()
    {
        targetFrame.SetActive(false);
    }

    public void OnPointerEnter(PointerEventData eventData)
    {
        // 攻撃のカードをドラッグしているときだけ、枠を出す
        if (eventData.pointerDrag == null)
        {
            return;
        }
        CardDrag card = eventData.pointerDrag.GetComponent<CardDrag>();
        if (card != null && card.IsAttack)
        {
            targetFrame.SetActive(true);
        }
    }

    public void OnPointerExit(PointerEventData eventData)
    {
        targetFrame.SetActive(false);
    }
}

敵をプレハブにして並べよう

プレハブにする

  • EnemyにEnemyViewを付けて、部品をドラッグ&ドロップ
    • Body Image:Enemy(自分自身)、Name Text:NameText
    • Intent:Intent、Intent Icon:IntentIcon、Intent Text:IntentText
    • Attack Icon・Block Icon・Charge Icon:icon_attack・icon_block・icon_charge
  • Enemyを、ProjectウインドのPrefabsにドラッグ&ドロップしてプレハブにする
  • プレハブにできたら、Hierarchyの Enemy は消す(次のEnemyPartyがスクリプトから作ります)
敵のプレハブのEnemy ViewとHp Gauge。Enemy Viewに絵・名前・予告の部品とicon_attack・icon_block・icon_charge、Hp Gaugeに4つの部品

敵を並べて作る

最後に、敵のデータのリストから、敵を横に並べて作るEnemyPartyです。EnemyAreaに付けます。

using System.Collections.Generic;
using UnityEngine;

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

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

    public List<EnemyView> Views => views;

    private void Start()
    {
        // 真ん中を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);
        }
    }
}
  • 並べ方は、#4で手札を横一列に並べたときと同じ考え方です(真ん中を0にして、左右に広げる)
  • Views
    • 作った敵のリスト。#9で、敵のターンに1体ずつ行動させるときに使います

EnemyAreaに付けて、設定します。

  • HierarchyのEnemyAreaにEnemyPartyを付ける
    • Enemy Prefab:Prefabs>Enemy
    • Enemies:「+」で2つにして、TinRobot・JackBox
    • Spacing:420
EnemyAreaのEnemy Party。Enemy PrefabにEnemy、EnemiesにTinRobotとJackBox、Spacingが420

戦う敵を変えたいときは、Enemiesのリストを入れかえるだけ。1体にしたり3体にしたりも、ここだけでできます。#14の3連戦では、このリストを戦いごとに入れかえます。

動かして確認!

再生してみましょう。

再生した画面。スライムちゃんのゲージ60/60、ブリキロボ(予告:剣と6、28/28)とびっくり箱(予告:…、36/36)
スライムちゃんの下にHPのゲージ、ブリキロボとびっくり箱の頭の上に剣と6、…の予告がポンと出る。定規でペシッでブリキロボのゲージが22/28、びっくり箱が30/36に縮み、下じきガードでスライムちゃんのゲージの横に盾と5が出る

#8の動作確認

  • 再生すると、スライムちゃんの下にHPのゲージ「60/60」が出る
  • ブリキロボとびっくり箱が横に並び、それぞれにゲージが出る
  • ブリキロボの頭の上に剣と「6」、びっくり箱の頭の上に「…」の予告が、ポンと出る
  • 攻撃のカードを敵に使うと、その敵のゲージが縮んで、数字が減る
  • 下じきガードを使うと、スライムちゃんのゲージの横に盾と「5」が出る

あとがき

今回は、HPのゲージと、敵のデータ、行動の予告を作りました。「次に何が来るか」が見えるだけで、手札を見ながら考える時間がぐっと楽しくなりますね。次回は、ターン終了ボタンで敵のターンにして、敵が予告どおりに行動する「ターンの流れ」を作りたいと思います!

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

コメント

コメントする

目次