
カードゲームの手触りを決めるのが、「カードをつかんで、相手に向かって使う」操作です。今回は、手札のカードをドラッグして使えるようにします。攻撃のカードは敵の上で離すと使え、防御や回復のカードは画面の上の方ならどこで離しても使えます。使わなかったカードは、すっと手札に戻るようにしてみたいと思います!
- シリーズの目次:デッキ構築カードゲームの作り方
- 前回:#5 手札を扇形に並べて、配る・持ち上げる動きを付けよう
今回作るもの
- 仮の敵(ブリキロボ)を置く
- 手札のカードをつかんで動かせるようにする
- 攻撃のカードを敵の上に持っていくと、敵に枠が出る
- 離したときに「使う」か「手札に戻す」かを決める
- 使ったカードを、手札から捨て札へ送る
| スクリプト | 役割 | 付ける場所 |
|---|---|---|
| CardDrag | カードをつかんで動かす。離して使わなければ手札に戻す | Card(プレハブ) |
| EnemyTarget | 攻撃のカードが上に来たら、枠を出す | Enemy |
| PlayZone | 離した場所で、カードを使うかどうかを決める | Stage |
| HandLayout(直す) | つかんでいるカードは、並べずにそのままにする | HandArea |
| Deck(足す) | 使ったカードを、手札から捨て札へ送る | Deck |
カードの効果(ダメージや防御)は、まだ付けません。今回は「使えた」ことがConsoleで分かるところまでです。効果は#7で作ります。
UIのドラッグ&ドロップの基本は、9-7 UIをドラッグ&ドロップしようで紹介しています。今回は、その形をカードに使います。
前回の続きから
前回の最後の状態から始めます。敵の絵は、素材のzipを入れ直すと入ります(DeckBuilding/Sprites/Enemies/enemy_tinrobot.png)。
- DeckBuildingAssets.zip
- 自分で入れた場合は、Sprite Mode:Singleにして Apply
仮の敵を置こう
敵の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

カードをつかんで動かそう
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になり、それでも正しく動きます
- 9-7では「マウスが動いた分(
- holdOffset
- カードの真ん中をマウスにぴったり合わせると、離す場所(マウスの先)がカードに隠れて、敵に合わせにくくなります。カードを少し上(130)に持って、マウスの先が見えるようにしています
カードを使うかどうかは、カードの位置ではなく、離したときのマウスの先で決まります。カードが敵に重なっていても、マウスの先が敵から外れていれば使われないので、敵にはマウスの先を合わせましょう。
- OnEndDrag
- 使われていなければ、HandLayoutに「はなした」と知らせる。あとはHandLayoutが扇形の位置へ戻してくれる
- IsAttack
- このカードが攻撃のカードかどうか。#2で作ったenumの
CardTypeで調べる
- このカードが攻撃のカードかどうか。#2で作ったenumの
スライム離したら、置いた場所にそのまま残っちゃいそうですけど・・・



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


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






#6の動作確認
- 手札のカードをつかむと、まっすぐになってマウスについてくる
- 攻撃のカードを敵の上に持っていくと、黄色い枠が出る。外れると消える
- 攻撃のカードを敵の上で離すと使われて、Consoleに「〜をブリキロボに使った!」と出る
- 防御・回復のカードは、画面の上の方ならどこで離しても使われる
- 手札の場所で離したり、攻撃のカードを敵以外の場所で離したりすると、すっと手札に戻る
- 使ったぶん、捨て札の数字が増える
あとがき
今回は、カードをつかんで使う操作を作りました。敵に枠が出て、離すとカードが消える・・・これだけでも、ぐっとカードバトルらしくなりましたね。次回は、1ターンに使えるエネルギーと、攻撃・防御・回復の「本当の効果」を作りたいと思います!
コメント