デッキ構築カードゲームの作り方#6|カードをドラッグして使おう

定規でペシッを敵のブリキロボの上に持っていくと黄色い枠が出て、離すと使われる。下じきガードはスライムちゃんの上で使われ、定規でペシッをスライムちゃんの上で離すと手札に戻る

カードゲームの手触りを決めるのが、「カードをつかんで、相手に向かって使う」操作です。今回は、手札のカードをドラッグして使えるようにします。攻撃のカードは敵の上で離すと使え、防御や回復のカードは画面の上の方ならどこで離しても使えます。使わなかったカードは、すっと手札に戻るようにしてみたいと思います!

目次

今回作るもの

  • 仮の敵(ブリキロボ)を置く
  • 手札のカードをつかんで動かせるようにする
  • 攻撃のカードを敵の上に持っていくと、敵に枠が出る
  • 離したときに「使う」か「手札に戻す」かを決める
  • 使ったカードを、手札から捨て札へ送る
スクリプト役割付ける場所
CardDragカードをつかんで動かす。離して使わなければ手札に戻すCard(プレハブ)
EnemyTarget攻撃のカードが上に来たら、枠を出すEnemy
PlayZone離した場所で、カードを使うかどうかを決めるStage
HandLayout(直す)つかんでいるカードは、並べずにそのままにするHandArea
Deck(足す)使ったカードを、手札から捨て札へ送るDeck

カードの効果(ダメージや防御)は、まだ付けません。今回は「使えた」ことがConsoleで分かるところまでです。効果は#7で作ります。

UIのドラッグ&ドロップの基本は、9-7 UIをドラッグ&ドロップしようで紹介しています。今回は、その形をカードに使います。

前回の続きから

前回の最後の状態から始めます。敵の絵は、素材のzipを入れ直すと入ります(DeckBuilding/Sprites/Enemies/enemy_tinrobot.png)。

仮の敵を置こう

敵のHPや行動は#8で作ります。今回は、ねらう相手として絵と名前だけを置きます。

  • HierarchyのEnemyAreaの子にEnemy(UI (Canvas)>Image)
    • アンカー:middle-center、Pos X:0、Pos Y:20、Width・Height:260
    • Source Image:enemy_tinrobot、Preserve Aspectにチェック
  • Enemyの子にTargetFrame(UI (Canvas)>Image)
    • アンカー:stretch-stretch、Left・Top・Right・Bottom:-16(敵より少し大きい枠)
    • Source Image:UISprite、Image Type:Sliced、Fill Centerのチェックを外す(ふちだけになる)
    • Pixels Per Unit Multiplier:0.25(ふちが太くなる)
    • Color:#FFD84A(黄色)
    • Raycast Targetのチェックを外す
    • Inspectorの一番上のチェックを外して、非表示にしておく
  • Enemyの子にNameText(UI (Canvas)>Text – TextMeshPro)
    • アンカー:bottom-center、Pos Y:-24、Width:260、Height:40
    • テキスト:「ブリキロボ」、Font Size:30、Alignment:中央、Color:#3C2F2F
HierarchyのEnemyArea>Enemy(子にTargetFrame・NameText)と、Gameビューの右側のブリキロボ

TargetFrameのように「ふちだけ」の枠は、SlicedのImageでFill Centerを外すと作れます。まん中が空くので、敵の絵を隠しません。Pixels Per Unit Multiplierを小さくすると、画像の角の部分が大きく使われて、ふちが太くなります。

カードをつかんで動かそう

HandLayoutを直す

#5のHandLayoutは、毎フレーム全部のカードを扇形の位置へ動かしています。このままだと、つかんだカードも扇形に引き戻されてしまいます。そこで、つかんでいるカードだけは並べないようにします。

HandLayoutに、次の変数とメソッドを足します。

    private RectTransform heldCard; // ★#6 つかんでいるカード(無ければ null)

    // ★#6 つかんだ・はなした(CardDrag から呼ばれる)
    public void SetHeld(RectTransform card)
    {
        heldCard = card;
        hoveredCard = null;
    }

    public void ClearHeld(RectTransform card)
    {
        if (heldCard == card)
        {
            heldCard = null;
        }
    }

カードをつかんで動かしている間に、ほかの手札の上を通ると、そのカードが持ち上がってしまいます。つかんでいる間は持ち上げないように、SetHoveredも直します。

    public void SetHovered(RectTransform card)
    {
        if (heldCard != null)
        {
            return; // ★#6 カードをつかんでいる間は、ほかのカードを持ち上げない
        }
        hoveredCard = card;
    }

Updateのforの中、RectTransform card = cards[i];の下に、つかんでいるカードを飛ばす処理を足します。

            // 目標へ少しずつ近づける
            RectTransform card = cards[i];
            if (card == heldCard)
            {
                continue; // ★#6 つかんでいるカードは、並べずにそのまま
            }

最後に、Updateの一番下(マウスが乗っているカードを手前に出すところの下)に足します。

        // ★#6 つかんでいるカードは、さらに手前に描く
        if (heldCard != null)
        {
            heldCard.SetAsLastSibling();
        }
  • continue
    • forの中で、そのカードの残りの処理を飛ばして、次のカードへ進む
  • 離したときにheldCardをnullに戻すと、そのカードも扇形の目標へLerpで動き出すので、特に何も書かなくても、すっと手札に戻ります

CardDragを作る

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

// 手札のカードをつかんで動かす。離して使わなければ、手札に戻す
[RequireComponent(typeof(CanvasGroup))]
public class CardDrag : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
{
    [SerializeField] private Vector2 holdOffset = new Vector2(0f, 130f); // マウスから見たカードの位置(少し上に持つ)

    private CardView cardView;
    private CanvasGroup canvasGroup;
    private RectTransform rectTransform;
    private Canvas canvas;
    private HandLayout handLayout;
    private bool isUsed;

    public CardView View => cardView;
    public bool IsAttack => cardView.Data.cardType == CardType.Attack;

    private void Awake()
    {
        cardView = GetComponent<CardView>();
        canvasGroup = GetComponent<CanvasGroup>();
        rectTransform = (RectTransform)transform;
    }

    private void Start()
    {
        canvas = GetComponentInParent<Canvas>();
        handLayout = GetComponentInParent<HandLayout>();
    }

    public void OnBeginDrag(PointerEventData eventData)
    {
        isUsed = false;
        canvasGroup.blocksRaycasts = false; // 下にある敵や Stage に、マウスが届くようにする
        handLayout.SetHeld(rectTransform);
        rectTransform.localRotation = Quaternion.identity; // まっすぐにして持つ
        rectTransform.localScale = Vector3.one * 0.75f;     // 少し小さくして、敵が隠れないようにする
        MoveToPointer(eventData);
    }

    public void OnDrag(PointerEventData eventData)
    {
        MoveToPointer(eventData);
    }

    // マウスの位置に、カードを合わせる(holdOffset だけ上にずらして持つ)
    private void MoveToPointer(PointerEventData eventData)
    {
        RectTransform parent = (RectTransform)rectTransform.parent;
        if (RectTransformUtility.ScreenPointToLocalPointInRectangle(parent, eventData.position, canvas.worldCamera, out Vector2 localPoint))
        {
            rectTransform.anchoredPosition = localPoint + holdOffset;
        }
    }

    public void OnEndDrag(PointerEventData eventData)
    {
        canvasGroup.blocksRaycasts = true;
        if (!isUsed)
        {
            handLayout.ClearHeld(rectTransform); // 使わなかったら、手札の並びに戻す
        }
    }

    // 使われた(PlayZone から呼ばれる)
    public void MarkUsed()
    {
        isUsed = true;
    }
}
  • OnBeginDrag
    • Blocks Raycastsを切って、つかんでいるカードの下に、マウスが届くようにする(9-7と同じ)
    • HandLayoutに「このカードはつかんでいる」と知らせる
    • まっすぐにして、0.75倍に小さくする。大きいままだと、持っているカードでねらう敵が隠れてしまうので、少し小さくしています(使わなかったときは、HandLayoutが元の大きさに戻してくれる)
  • MoveToPointer(OnBeginDrag・OnDrag)
    • 9-7では「マウスが動いた分(eventData.delta)だけ足す」書き方でした。今回のカードは、つかむ直前にマウスを乗せて持ち上がり、つかむと小さくなるので、その書き方だとつかんだ瞬間のずれがそのまま残って、カードがマウスから離れた場所についてきます
    • そこで、毎回「マウスの位置」にカードを合わせます。RectTransformUtility.ScreenPointToLocalPointInRectangleは、画面の位置(ピクセル)を、親(HandArea)の中の位置に変換してくれるメソッドです
    • 3つ目のcanvas.worldCameraは、CanvasのRender Cameraです(#1でScreen Space – Cameraにしたので、Main Camera)。Screen Space – Overlayのときはnullになり、それでも正しく動きます
  • holdOffset
    • カードの真ん中をマウスにぴったり合わせると、離す場所(マウスの先)がカードに隠れて、敵に合わせにくくなります。カードを少し上(130)に持って、マウスの先が見えるようにしています

カードを使うかどうかは、カードの位置ではなく、離したときのマウスの先で決まります。カードが敵に重なっていても、マウスの先が敵から外れていれば使われないので、敵にはマウスの先を合わせましょう。

  • OnEndDrag
    • 使われていなければ、HandLayoutに「はなした」と知らせる。あとはHandLayoutが扇形の位置へ戻してくれる
  • IsAttack
    • このカードが攻撃のカードかどうか。#2で作ったenumのCardTypeで調べる
スライム

離したら、置いた場所にそのまま残っちゃいそうですけど・・・

フェアリー

HandLayoutが毎フレーム「扇形の位置」へ動かしてるから、つかむのをやめたら勝手に戻っていくんだ。#5で作っておいてよかったね!

ねらっている敵を分かるようにしよう

攻撃のカードを持って敵の上に来たら、枠を出します。

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

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

    public string EnemyName => enemyName;

    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);
    }
}
  • eventData.pointerDrag
    • 今ドラッグしているもの。何もドラッグしていなければnull
    • その中のCardDragを見て、攻撃のカードのときだけ枠を出す

離したら使う・戻る

最後に、離した場所で「使うかどうか」を決めるPlayZoneを作ります。Stage(スライムちゃんと敵が並ぶ場所)に付けます。

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

// Stage でカードを離したときに、使うかどうかを決める
public class PlayZone : MonoBehaviour, IDropHandler
{
    [SerializeField] private Deck deck;

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

        if (card.IsAttack)
        {
            // 攻撃のカードは、敵の上で離したときだけ使う
            GameObject over = eventData.pointerCurrentRaycast.gameObject;
            EnemyTarget enemy = over != null ? over.GetComponentInParent<EnemyTarget>() : null;
            if (enemy == null)
            {
                return; // 敵の上ではないので使わない(手札に戻る)
            }
            card.MarkUsed();
            deck.UseCard(card.View, enemy.EnemyName);
        }
        else
        {
            // 防御や回復のカードは、Stage のどこで離しても使う
            card.MarkUsed();
            deck.UseCard(card.View, "スライムちゃん");
        }
    }
}
  • Stageで受け取る理由
    • 敵(Enemy)はStageの子。離したときの知らせは、マウスの下のものから親の方へさかのぼって、IDropHandlerを持っているものに届きます
    • なので、敵の上で離してもStageのPlayZoneに届き、どちらもこの1つのスクリプトで決められます
  • eventData.pointerCurrentRaycast.gameObject
    • 離したときに、マウスの下にあったもの(カードはBlocks Raycastsを切っているので、その下のもの)
    • そこから親の方へEnemyTargetを探して、見つかれば「敵の上で離した」
  • return(使わない)
    • MarkUsedを呼ばなければ、CardDragのOnEndDragで手札に戻る

「使うかどうか」を決める場所を、PlayZoneの1か所にまとめておくと、あとで「エネルギーが足りないと使えない」(#7)のような条件を足すときも、ここを直すだけで済みます。

Deckに「使う」を足す

使ったカードを、手札から捨て札へ送るメソッドをDeckに足します。

    // ★#6 手札のカードを使って、捨て札へ送る
    public void UseCard(CardView view, string targetName)
    {
        int index = handViews.IndexOf(view);
        if (index < 0)
        {
            return;
        }

        CardData card = hand[index];
        hand.RemoveAt(index);
        handViews.RemoveAt(index);
        discardPile.Add(card);

        handLayout.Remove((RectTransform)view.transform);
        Destroy(view.gameObject);
        UpdateCounts();
        Debug.Log($"{card.cardName}を{targetName}に使った!");
    }
  • hand(カードのデータ)とhandViews(カードの見た目)は、同じ番号どうしが同じカードになるように並べています。なので、見た目の番号を探して、同じ番号のデータを取り出します
  • 取り出したカードは捨て札に入れて、見た目は消します(カードが飛んでいく演出は#10で作ります)

スクリプトを付けよう

  • Card(プレハブ):Prefabs>Cardをダブルクリックで開いて、CardDragを付ける(Canvas Groupも自動で付く)
  • Enemy:EnemyTargetを付ける
    • Enemy Name:ブリキロボ、Target Frame:TargetFrame
  • Stage:Add Component>Imageを付けて、ColorのA(アルファ)を0にする。そのあとPlayZoneを付けて、DeckにDeckをドラッグ
StageのInspector。ImageのColorのAが0でRaycast Targetにチェック、Play ZoneのDeckにDeck

Stageは#1で空のオブジェクトとして作ったので、そのままではマウスに反応しません。見えないImage(アルファ0)を付けておくと、カードを離したときの知らせを受け取れるようになります。Imageはアルファが0でも、Raycast Targetがオンならマウスに反応します。

EnemyのEnemy Target。Enemy Nameがブリキロボ、Target FrameにTargetFrame

動かして確認!

再生して、いろいろなカードを動かしてみましょう。

定規でペシッをつかんでブリキロボの上に持っていくと黄色い枠が出て、離すとカードが消え、捨て札が増える
下じきガードをスライムちゃんの上で離すと使われる。定規でペシッをスライムちゃんの上で離すと、使われずに手札へ戻る
Consoleに「定規でペシッをブリキロボに使った!」「下じきガードをスライムちゃんに使った!」

#6の動作確認

  • 手札のカードをつかむと、まっすぐになってマウスについてくる
  • 攻撃のカードを敵の上に持っていくと、黄色い枠が出る。外れると消える
  • 攻撃のカードを敵の上で離すと使われて、Consoleに「〜をブリキロボに使った!」と出る
  • 防御・回復のカードは、画面の上の方ならどこで離しても使われる
  • 手札の場所で離したり、攻撃のカードを敵以外の場所で離したりすると、すっと手札に戻る
  • 使ったぶん、捨て札の数字が増える

あとがき

今回は、カードをつかんで使う操作を作りました。敵に枠が出て、離すとカードが消える・・・これだけでも、ぐっとカードバトルらしくなりましたね。次回は、1ターンに使えるエネルギーと、攻撃・防御・回復の「本当の効果」を作りたいと思います!

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

コメント

コメントする

目次