Unityを触り始めた頃って、つい何でもかんでも Update() に書いてしまいがちですよね。
マウス入力の判定、ドラッグ中の処理、UIの更新、インベントリの判定…全部ひとつのスクリプトに詰め込むと、だんだん何をしているコードなのか分からなくなってきます。
こうなると、
- ちょっと仕様を変えたいだけなのに、巨大なスクリプトを全部読まないといけない
- ドラッグ&ドロップの仕様を変えたら、他の機能まで巻き込んでバグる
- プレハブごとに微妙に違う処理が必要になって、分岐だらけになる
といった「Godクラス」状態になりやすいです。
そこでこの記事では、「スロットはスロットの責務だけ持つ」という考え方で、
ドラッグ&ドロップされたアイテムデータを受け入れるだけの小さなコンポーネントを作ってみます。
名前は DragDropSlot。インベントリUIや装備スロットなどにアタッチして、
「ここにアイテムをドロップできるよ」という機能だけを担当させるイメージですね。
【Unity】ドラッグでポンッと装備!「DragDropSlot」コンポーネント
以下のコードは、
- ドラッグ中の「アイテムデータ」を受け取るスロット
- 受け入れ条件(フィルタ)を簡単に設定できる
- 受け入れ時のイベントをインスペクタから設定できる
という責務だけを持ったコンポーネントです。
ドラッグする側(DragItem 的なコンポーネント)は別に作る前提ですが、
この記事のコードだけでスロット側の処理は完結するようにしてあります。
前提となるシンプルなアイテムデータ
まずは、スロットとドラッグ元で共有するための、ごくシンプルなアイテムデータクラスを用意します。
ScriptableObject を使うことで、プレハブやシーンに依存しないデータを扱えます。
using UnityEngine;
// 非常にシンプルなアイテムデータ
// 実際のプロジェクトでは、攻撃力やレアリティなどを追加していきましょう。
[CreateAssetMenu(
fileName = "ItemData",
menuName = "Game/Item Data",
order = 0)]
public class ItemData : ScriptableObject
{
[SerializeField] private string itemId; // 一意なID
[SerializeField] private string displayName; // 表示名
[SerializeField] private Sprite icon; // アイコン画像
public string ItemId => itemId;
public string DisplayName => displayName;
public Sprite Icon => icon;
}
ドラッグ中のアイテムを管理する「グローバルな手」
次に、今まさにドラッグ中のアイテムを保持しておくための、
とても小さな「グローバルハンドラー」を用意します。
スロットはここを参照して「何がドラッグされているか?」を知るだけにします。
using UnityEngine;
/// <summary>現在ドラッグ中のアイテムを保持する静的クラス</summary>
public static class DragItemContext
{
// 現在ドラッグ中のアイテムデータ(なければ null)
public static ItemData CurrentItem { get; private set; }
// ドラッグ開始時に呼ぶ
public static void BeginDrag(ItemData item)
{
CurrentItem = item;
}
// ドラッグ終了時に呼ぶ
public static void EndDrag()
{
CurrentItem = null;
}
}
ドラッグする側のコンポーネントから BeginDrag / EndDrag を呼び出してもらう想定です。
この記事では「スロット側」に集中したいので、ドラッグ元の実装は省略しますが、
OnBeginDrag / OnEndDrag などのUIイベントからこのクラスを呼ぶだけで動きます。
ドラッグ&ドロップを受け入れる「DragDropSlot」本体
いよいよ本題の DragDropSlot です。
UI のスロット(Image や Button など)にアタッチして使います。
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
using UnityEngine.UI;
/// <summary>
/// アイテムデータのドラッグ&ドロップを受け入れるスロットコンポーネント。
/// - ドラッグ中のアイテムは DragItemContext から取得
/// - スロットへのドロップ時にイベントを発火
/// - アイテム種別などのフィルタリングにも対応
/// </summary>
[DisallowMultipleComponent]
public class DragDropSlot : MonoBehaviour, IDropHandler, IPointerEnterHandler, IPointerExitHandler
{
[Header("受け入れ設定")]
[Tooltip("このスロットが受け入れ可能なアイテム。空なら何でも受け入れる。")]
[SerializeField] private ItemData[] acceptableItems;
[Tooltip("空のスロットとして扱うか。false の場合、すでにアイテムが入っていたら拒否するなどのロジックに使う。")]
[SerializeField] private bool isEmptySlot = true;
[Header("見た目の設定")]
[Tooltip("スロットの背景イメージ(ハイライトなどに使用)。任意。")]
[SerializeField] private Image slotBackground;
[Tooltip("ドラッグ中にスロット上にカーソルが乗ったときの色。")]
[SerializeField] private Color highlightColor = new Color(1f, 1f, 1f, 0.3f);
[Tooltip("通常時の色。")]
[SerializeField] private Color normalColor = Color.white;
[Header("イベント")]
[Tooltip("アイテムがドロップされ、かつ受け入れに成功したときに呼ばれるイベント。")]
[SerializeField] private UnityEvent<ItemData> onItemDropped;
// このスロットに現在入っているアイテム(なければ null)
public ItemData CurrentItem { get; private set; }
// ハイライト中かどうか
private bool isPointerOver;
private void Reset()
{
// コンポーネント追加時に、同じGameObjectのImageを自動で参照させる
if (slotBackground == null)
{
slotBackground = GetComponent<Image>();
}
}
private void Awake()
{
// 初期色を保存
if (slotBackground != null)
{
normalColor = slotBackground.color;
}
}
/// <summary>
/// IDropHandler.OnDrop
/// スロット上でドロップされたときに呼ばれる。
/// </summary>
public void OnDrop(PointerEventData eventData)
{
// 何もドラッグされていない場合は何もしない
if (DragItemContext.CurrentItem == null)
{
return;
}
ItemData draggedItem = DragItemContext.CurrentItem;
// 受け入れ可能かチェック
if (!CanAccept(draggedItem))
{
// 受け入れ不可なら何もせず終了
return;
}
// ここまで来たら受け入れOK
AcceptItem(draggedItem);
// ドラッグ終了を通知(ドラッグ元側の表示を戻したりする想定)
DragItemContext.EndDrag();
}
/// <summary>
/// このスロットが指定のアイテムを受け入れ可能かどうかを判定。
/// </summary>
private bool CanAccept(ItemData item)
{
if (item == null)
{
return false;
}
// すでにアイテムが入っているスロットに新規ドロップを禁止したい場合の例
if (!isEmptySlot && CurrentItem != null)
{
return false;
}
// acceptableItems が空なら何でもOK
if (acceptableItems == null || acceptableItems.Length == 0)
{
return true;
}
// リストに含まれているアイテムだけ受け入れる
foreach (var acceptable in acceptableItems)
{
if (acceptable == item)
{
return true;
}
}
return false;
}
/// <summary>
/// アイテムを実際にスロットへ受け入れる処理。
/// - CurrentItem を更新
/// - イベントを発火
/// - 見た目を更新 など
/// </summary>
private void AcceptItem(ItemData item)
{
CurrentItem = item;
isEmptySlot = false;
// イベントを発火(インスペクタからリスナーを設定しておく)
onItemDropped?.Invoke(item);
// ここでスロットの見た目を更新するなどの処理を行う
// 例: スロット内のアイコンImageに item.Icon を設定する など。
}
/// <summary>
/// スロットからアイテムを取り除く(外部から呼び出し可能)。
/// </summary>
public void ClearItem()
{
CurrentItem = null;
isEmptySlot = true;
}
/// <summary>
/// マウスカーソルがスロットに入ったとき。
/// 見た目のハイライトなどを行う。
/// </summary>
public void OnPointerEnter(PointerEventData eventData)
{
isPointerOver = true;
UpdateHighlight();
}
/// <summary>
/// マウスカーソルがスロットから出たとき。
/// </summary>
public void OnPointerExit(PointerEventData eventData)
{
isPointerOver = false;
UpdateHighlight();
}
/// <summary>
/// ハイライト状態に応じて背景色を更新。
/// slotBackground が設定されていない場合は何もしない。
/// </summary>
private void UpdateHighlight()
{
if (slotBackground == null)
{
return;
}
slotBackground.color = isPointerOver ? highlightColor : normalColor;
}
}
使い方の手順
ここからは、実際のシーンに組み込む手順を具体的に見ていきましょう。
例として「インベントリのスロット」「装備スロット」「動く床の上にアイテムを落とすUI」などで使える形をイメージします。
手順①:アイテムデータ(ScriptableObject)を作成する
- Project ウィンドウで右クリック →
Create > Game > Item Dataを選択。 - 作成された
ItemDataアセットを選び、Item IdやDisplay Name、Iconを設定。 - 武器用、ポーション用など、必要な分だけ複数作っておきましょう。
これらが「ドラッグされるアイテムの中身」になります。
手順②:ドラッグ元(DragItem)側から DragItemContext を呼ぶ
ドラッグする側の UI 要素(インベントリのアイコンなど)には、
BeginDrag / EndDrag で DragItemContext を操作するスクリプトを付けます。
例として、UI の Image に付ける簡易版を示します。
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
/// <summary>シンプルなドラッグ元コンポーネントの例</summary>
[RequireComponent(typeof(Image))]
public class SimpleDragItem : MonoBehaviour, IBeginDragHandler, IEndDragHandler, IDragHandler
{
[SerializeField] private ItemData itemData;
private Canvas canvas;
private RectTransform rectTransform;
private CanvasGroup canvasGroup;
private void Awake()
{
rectTransform = GetComponent<RectTransform>();
canvasGroup = gameObject.AddComponent<CanvasGroup>();
// 一番近い Canvas を取得
canvas = GetComponentInParent<Canvas>();
}
public void OnBeginDrag(PointerEventData eventData)
{
if (itemData == null) return;
// 現在ドラッグ中のアイテムとして登録
DragItemContext.BeginDrag(itemData);
// レイキャストをブロックしないようにする(スロットがドロップを受け取れるように)
canvasGroup.blocksRaycasts = false;
}
public void OnDrag(PointerEventData eventData)
{
if (canvas == null) return;
// マウスに追従させる
RectTransformUtility.ScreenPointToLocalPointInRectangle(
canvas.transform as RectTransform,
eventData.position,
canvas.worldCamera,
out var localPoint);
rectTransform.localPosition = localPoint;
}
public void OnEndDrag(PointerEventData eventData)
{
// ドラッグ終了
DragItemContext.EndDrag();
canvasGroup.blocksRaycasts = true;
}
}
このコンポーネントを「アイテムアイコンのImage」に付けておくと、
ドラッグ中は DragItemContext.CurrentItem に ItemData が入るようになります。
スロット側はそれを読むだけでOKです。
手順③:スロットUIに DragDropSlot をアタッチする
次に、アイテムを受け入れる側の UI を作ります。
- Canvas の下に
Imageを作成(インベントリの1マスや装備スロットの見た目にする)。 - その GameObject に
DragDropSlotコンポーネントを追加。 Slot Backgroundに自分自身のImageをドラッグ&ドロップ。Acceptable Itemsに、受け入れたいアイテム(武器だけ、ポーションだけ など)を設定。
何も入れなければ「何でもOK」なスロットになります。Is Empty Slotは最初にアイテムが入っていないならチェックONにしておきます。
これで「このUIにアイテムをドロップできる」状態が完成です。
手順④:ドロップ時のイベントで見た目や装備処理をつなぐ
DragDropSlot には On Item Dropped という UnityEvent が用意されています。
ここに関数を登録することで、
- 装備スロットなら「プレイヤーの武器をこのアイテムに差し替える」
- インベントリスロットなら「スロット内のアイコンを変更する」
- 動く床UIなら「この床に対応するギミックを有効化する」
といった処理を、スロットの責務からは切り離して実装できます。
例えば、スロット内のアイコンを更新するだけのコンポーネントを別に用意すると、こんな感じになります。
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// DragDropSlot の OnItemDropped イベントから呼ばれて、
/// スロット内のアイコン画像を更新するだけのコンポーネント。
/// </summary>
[RequireComponent(typeof(Image))]
public class SlotIconView : MonoBehaviour
{
[SerializeField] private Image iconImage;
private void Reset()
{
iconImage = GetComponent<Image>();
}
public void UpdateIcon(ItemData item)
{
if (iconImage == null) return;
if (item != null)
{
iconImage.sprite = item.Icon;
iconImage.enabled = true;
}
else
{
iconImage.sprite = null;
iconImage.enabled = false;
}
}
}
使い方は簡単で、
- スロットの子オブジェクトに「アイコン用Image」を作成。
- その Image に
SlotIconViewをアタッチ。 DragDropSlotのOn Item Droppedに、このSlotIconView.UpdateIconを登録。
これで、スロットは「受け入れるだけ」、見た目の更新は SlotIconView に分離され、
責務の分割がきれいになります。
メリットと応用
DragDropSlot のように、「スロットはドロップを受け入れる」だけという小さな責務に絞ると、いろいろな良いことがあります。
- プレハブ管理が楽になる
インベントリ用スロット、装備スロット、スキルスロットなど、見た目が違っても
「ドロップを受け入れる機能」は全て同じコンポーネントを再利用できます。 - レベルデザインがやりやすい
「ここにアイテムを落としたら扉が開く」「ここに鍵をセットしたらエレベーターが動く」など、
スロットをトリガーとして使えるようになります。
デザイナーはシーン上にスロットをポンポン置いて、イベントだけ差し替えればOKです。 - 仕様変更に強い
「受け入れ条件を変えたい」「ドロップ時の演出を変えたい」といったとき、
スロット自体のコードを触らずに、イベント先や見た目のコンポーネントを差し替えるだけで対応できます。
応用としては、
- スロット同士でアイテムを入れ替える
- 右クリックでスロットからアイテムを取り出す
- 装備スロットに入ったアイテムに応じてステータスを自動更新する
など、いくらでも拡張できます。
改造案:ドロップ時に「入れ替え」を許可する
例えば、すでにアイテムが入っているスロットに新しいアイテムをドロップしたとき、
「入れ替え」を許可したい場合は、以下のような関数を追加してみるのもアリです。
/// <summary>
/// 現在のアイテムと新しいアイテムを入れ替えるサンプル。
/// DragDropSlot の AcceptItem 内から呼び出すなどして使う。
/// </summary>
private void SwapItem(ItemData newItem)
{
// すでに入っているアイテムを一時退避
ItemData previousItem = CurrentItem;
// 新しいアイテムをセット
CurrentItem = newItem;
isEmptySlot = false;
// ここで previousItem を元のスロットに戻したり、
// インベントリへ返却する処理を追加できます。
// 例: 別のイベントを発火させて、インベントリ管理コンポーネントに渡す など。
}
このように、小さなコンポーネント単位で責務を分けておけば、
必要になったタイミングで機能を足したり差し替えたりしやすくなります。
「巨大な Update 一つ」から卒業して、コンポーネント指向で気持ちよく開発していきましょう。




