インベントリUIをGodotで作ろうとすると、つい「InventorySlotButton を継承して…」「アイテムごとに専用スクリプトを生やして…」と、どんどんクラスとノード階層が増えていきますよね。
さらにドラッグ&ドロップ対応までやろうとすると、_gui_input() に条件分岐を詰め込み、スロットとアイテムの両方で処理を書いて…と、管理が一気にカオスになりがちです。

そこでこの記事では、「スロットはただの UI ノードのままにしておいて、ドラッグ&ドロップを受け入れる機能だけをコンポーネントとして後付けする」という方針で進めます。
つまり「継承して SlotButton を作る」のではなく、「任意の Control に DragDropSlot コンポーネントをアタッチする」だけで機能を追加しよう、という発想ですね。

今回作る DragDropSlot コンポーネントは、

  • アイテムデータ(Dictionary / Resource など)を受け取る
  • Godot の Control 標準ドラッグ&ドロップ API をラップして扱いやすくする
  • スロット側は「受け入れるかどうか」と「受け取った時の処理」だけ書けばOK

という「スロット機能」を提供します。

【Godot 4】ドラッグ&ドロップ対応スロットを後付け!「DragDropSlot」コンポーネント

フルコード(GDScript)


## DragDropSlot.gd
## 任意の Control ノードにアタッチして、
## アイテムデータのドラッグ&ドロップを受け入れるためのコンポーネント。
## Godot 4.x 対応。

extends Node
class_name DragDropSlot

## --- 設定パラメータ (@export) ---

## このスロットが有効かどうか。
## 無効にするとドラッグ受け入れ・ハイライトなどを一括で止められます。
@export var enabled: bool = true

## このスロットが受け入れるアイテムの「タイプ」。
## 例: "weapon", "armor", "consumable" など。
## 空文字のままなら、タイプ制限なしとして扱います。
@export var accepted_item_type: String = ""

## ドロップ時に自動でアイテムを保存するかどうか。
## true の場合、このコンポーネントが current_item_data を更新します。
## false の場合、シグナルを受けた側(親ノードなど)で管理してください。
@export var auto_store_item: bool = true

## マウスが上に乗っていて、ドロップ可能な状態のときに
## スロットの見た目を変えるための色(modulate に適用)。
@export var hover_tint_color: Color = Color(1, 1, 1, 1)

## 通常状態の色。null の場合、起動時に実際の色を自動保存します。
@export var normal_tint_color: Color = Color(1, 1, 1, 1)

## ドロップ不可能なアイテムが乗っているときの色。
@export var blocked_tint_color: Color = Color(1, 0.5, 0.5, 1)

## 内部的に「どのノードからドラッグされてきたか」を識別するためのキー。
## ドラッグ開始側と合わせておく必要があります。
@export var drag_data_key: String = "inventory_item"

## --- シグナル ---

## ドラッグされたアイテムがこのスロットにドロップされたときに発火。
## item_data: 任意の Dictionary / Resource など
## from_slot: 元のスロット(DragDropSlot)または null
signal item_dropped(item_data, from_slot: DragDropSlot)

## このスロットにドラッグが乗ったとき。受け入れ可能かどうかを返す。
## ここで条件チェックして false を返せば、ドロップ不可扱いになります。
signal can_accept_item_query(item_data, result: bool)

## スロットに「現在入っているアイテム」が変化したときに発火。
signal current_item_changed(item_data)

## --- 内部状態 ---

## 現在このスロットに入っているアイテムデータ。
## auto_store_item が true のときのみ自動更新されます。
var current_item_data: Variant = null setget _set_current_item_data

## このコンポーネントがアタッチされている Control ノード。
var _control: Control

## 起動時に保存しておく元の色。
var _original_modulate: Color

func _ready() -> void:
	## 親ノードが Control であることを前提とします。
	_control = get_parent() as Control
	if _control == null:
		push_warning("DragDropSlot must be a child of a Control node.")
		enabled = false
		return

	## 起動時の色を保存(normal_tint_color がデフォルトなら上書き)。
	_original_modulate = _control.modulate
	if normal_tint_color == Color(1, 1, 1, 1):
		normal_tint_color = _original_modulate

	## Control のドラッグ&ドロップ関連シグナルに接続します。
	## Godot 4 では GUI ドラッグ&ドロップは以下の仮想メソッドで扱いますが、
	## ここでは親 Control から「委譲」してもらう形を想定しています。
	## (親スクリプトで call_deferred しても OK)
	## ただし、ここではコンポーネント単体で完結できるように、
	## _control の script が空でも動くようにします。

	## 親 Control の既存処理を壊さないため、
	## input / drop 関連は signal ベースではなく、
	## このコンポーネントが「フック」として呼ばれる前提で実装します。
	## → 実際の使用例は後述のチュートリアルで説明します。
	pass


## --- 公開メソッド ---

## 他のスクリプトからアイテムを設定したいとき用。
func set_item_data(data: Variant) -> void:
	_set_current_item_data(data)

## ドラッグされたアイテムをこのスロットが受け入れ可能か判定する。
## item_data はドラッグ元が設定した任意のデータ。
func can_accept_item(item_data: Variant) -> bool:
	if not enabled:
		return false

	var result := true

	## accepted_item_type が指定されている場合はチェック。
	if typeof(item_data) == TYPE_DICTIONARY and accepted_item_type != "":
		if not item_data.has("type") or String(item_data["type"]) != accepted_item_type:
			result = false

	## 外部にも判定させたい場合はシグナルで問い合わせ。
	## 受け側で result を書き換えることができます。
	emit_signal("can_accept_item_query", item_data, result)

	return result

## 親 Control から呼んでもらう用のフック。
## _can_drop_data(position, data) 相当。
func handle_can_drop_data(position: Vector2, data: Variant) -> bool:
	if not enabled:
		_reset_tint()
		return false

	var item_data := _extract_item_data_from_drag(data)
	var ok := can_accept_item(item_data)
	_update_tint_for_hover(ok)
	return ok

## 親 Control から呼んでもらう用のフック。
## _drop_data(position, data) 相当。
func handle_drop_data(position: Vector2, data: Variant) -> void:
	if not enabled:
		_reset_tint()
		return

	var item_data := _extract_item_data_from_drag(data)
	if not can_accept_item(item_data):
		_update_tint_for_blocked()
		return

	## auto_store_item が有効なら内部状態を更新。
	if auto_store_item:
		_set_current_item_data(item_data)

	## ドロップ元の DragDropSlot を取得(あれば)。
	var from_slot: DragDropSlot = null
	if typeof(data) == TYPE_DICTIONARY and data.has("from_slot") and data["from_slot"] is DragDropSlot:
		from_slot = data["from_slot"]

	emit_signal("item_dropped", item_data, from_slot)
	_reset_tint()

## 親 Control から呼んでもらう用のフック。
## _gui_input(event) などから、ドラッグ終了時に色を戻したい場合に使用。
func handle_drag_exit() -> void:
	_reset_tint()


## --- 内部ユーティリティ ---

func _set_current_item_data(value: Variant) -> void:
	current_item_data = value
	emit_signal("current_item_changed", current_item_data)

func _extract_item_data_from_drag(data: Variant) -> Variant:
	## ドラッグ元が { drag_data_key: item, from_slot: DragDropSlot } の形式で
	## 渡してくることを想定しています。
	if typeof(data) == TYPE_DICTIONARY and data.has(drag_data_key):
		return data[drag_data_key]
	return data

func _update_tint_for_hover(can_accept: bool) -> void:
	if not is_instance_valid(_control):
		return
	if can_accept:
		_control.modulate = hover_tint_color
	else:
		_control.modulate = blocked_tint_color

func _update_tint_for_blocked() -> void:
	if not is_instance_valid(_control):
		return
	_control.modulate = blocked_tint_color

func _reset_tint() -> void:
	if not is_instance_valid(_control):
		return
	_control.modulate = normal_tint_color

このコンポーネントは「ドラッグを開始する側」は持っていません。
ドラッグ開始側は、Godot の Control 標準の _get_drag_data() を使って、drag_data_key 付きの Dictionary を返すだけです。

例(ドラッグ元のスロット用スクリプト・抜粋):


func _get_drag_data(at_position: Vector2) -> Variant:
	var data := {
		"inventory_item": current_item_data, ## DragDropSlot.drag_data_key と合わせる
		"from_slot": drag_drop_slot_component, ## DragDropSlot への参照
	}
	var preview := Sprite2D.new()
	preview.texture = $Icon.texture
	set_drag_preview(preview)
	return data

使い方の手順

ここからは、実際に「プレイヤーのインベントリスロット」に DragDropSlot を組み込む手順を見ていきましょう。

手順① スロット用の Control ノードを用意する

まずは、インベントリスロットとして使う UI ノードを作ります。
例えば TextureRectButton など、Control を継承していれば何でもOKです。

InventorySlot (TextureRect or Button)
 ├── Icon (TextureRect)
 └── DragDropSlot (Node)  ← このコンポーネントをアタッチ
  • InventorySlot: 実際にクリックされる UI ノード(Control)
  • Icon: アイテムアイコンを描画するための子ノード
  • DragDropSlot: 今回のコンポーネント(Node として追加)

ポイントは、「スロット本体(Control)」と「ドラッグ受け入れロジック(DragDropSlot)」を分離していることです。
スロットの見た目やクリック挙動を変えたくなっても、DragDropSlot はそのまま使い回せます。

手順② Control 側のドラッグ&ドロップフックを DragDropSlot に委譲する

次に、InventorySlot(Control)のスクリプトで、Godot のドラッグ&ドロップ仮想メソッドを DragDropSlot に丸投げ します。


## InventorySlot.gd
extends TextureRect
## extends Button でも OK。Control を継承していれば動きます。

@onready var drag_slot: DragDropSlot = $DragDropSlot
@onready var icon: TextureRect = $Icon

var current_item_data: Dictionary = {} ## 見た目更新用に保持(任意)

func _ready() -> void:
	## DragDropSlot のシグナルを受けて見た目を更新する
	drag_slot.current_item_changed.connect(_on_current_item_changed)
	drag_slot.item_dropped.connect(_on_item_dropped)

func _get_drag_data(at_position: Vector2) -> Variant:
	## アイテムが無いスロットはドラッグ開始しない
	if drag_slot.current_item_data == null:
		return null

	var data := {
		drag_slot.drag_data_key: drag_slot.current_item_data,
		"from_slot": drag_slot,
	}

	## プレビュー用のノードを適当に作る
	var preview := TextureRect.new()
	preview.texture = icon.texture
	preview.custom_minimum_size = icon.size
	set_drag_preview(preview)

	return data

func _can_drop_data(at_position: Vector2, data: Variant) -> bool:
	return drag_slot.handle_can_drop_data(at_position, data)

func _drop_data(at_position: Vector2, data: Variant) -> void:
	drag_slot.handle_drop_data(at_position, data)

func _gui_input(event: InputEvent) -> void:
	## ドラッグがキャンセルされたときなどに色を戻したい場合
	if event is InputEventMouseMotion and not Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT):
		drag_slot.handle_drag_exit()

func _on_current_item_changed(item_data: Variant) -> void:
	current_item_data = item_data
	if item_data == null:
		icon.texture = null
	else:
		## ここでは Dictionary {"icon": Texture2D} を想定
		if typeof(item_data) == TYPE_DICTIONARY and item_data.has("icon"):
			icon.texture = item_data["icon"]

func _on_item_dropped(item_data: Variant, from_slot: DragDropSlot) -> void:
	## 例: ドロップ元からアイテムを消す(簡易スワップ)
	if from_slot != null and from_slot != drag_slot:
		from_slot.set_item_data(null)

これで、InventorySlot は「見た目とドラッグ開始の処理」だけを持ち、
「ドロップ判定と受け入れ処理」は DragDropSlot コンポーネントに任せる構造になります。

手順③ コンポーネントのパラメータを設定する

エディタ上で DragDropSlot ノードを選択し、Inspector から以下を設定します。

  • enabled: true(デフォルトのままでOK)
  • accepted_item_type: 例)”weapon” や “armor” など。空なら制限なし。
  • auto_store_item: true にしておくと、ドロップされたアイテムが自動的に current_item_data に入ります。
  • hover_tint_color / blocked_tint_color: スロットのハイライト用の色。
  • drag_data_key: ドラッグ元の _get_drag_data() で使うキーと合わせてください。

たとえば「武器だけを受け入れるスロット」と「防具だけを受け入れるスロット」を作りたい場合は、

  • WeaponSlot: accepted_item_type = "weapon"
  • ArmorSlot: accepted_item_type = "armor"

とするだけで OK です。スクリプトを分ける必要はありません。

手順④ 実際のアイテムデータを流し込む

最後に、ゲーム側の「インベントリ管理オブジェクト」から、スロットにアイテムをセットします。
DragDropSlot には set_item_data() が用意されているので、これを呼ぶだけです。


## InventoryManager.gd (例)
extends Node

@export var slots: Array[DragDropSlot] = []

func assign_items(items: Array) -> void:
	for i in range(min(slots.size(), items.size())):
		slots[i].set_item_data(items[i])

func clear_all_slots() -> void:
	for slot in slots:
		slot.set_item_data(null)

シーン構成例:

InventoryUI (Control)
 ├── InventoryManager (Node)
 ├── Slot1 (TextureRect)
 │    ├── Icon (TextureRect)
 │    └── DragDropSlot (Node)
 ├── Slot2 (TextureRect)
 │    ├── Icon (TextureRect)
 │    └── DragDropSlot (Node)
 └── Slot3 (TextureRect)
      ├── Icon (TextureRect)
      └── DragDropSlot (Node)

こうしておけば、InventoryManager は「どのスロットにどのアイテムを入れるか」だけを考えればよく、
ドラッグ&ドロップ処理はすべてコンポーネントにカプセル化されます。

メリットと応用

DragDropSlot コンポーネントを導入することで、次のようなメリットがあります。

  • ノード構造がシンプル
    スロットはただの Control ノードのままなので、
    「SlotButton」「WeaponSlot」「ArmorSlot」などの派生クラスを増やさずに済みます。
  • ドラッグ&ドロップのロジックを一箇所に集約
    受け入れ判定やハイライト処理が DragDropSlot に閉じているため、
    仕様変更(例:ハイライト色を変える、受け入れ条件を追加する)を一箇所で済ませられます。
  • UI の再利用性が高い
    インベントリだけでなく、「装備スロット」「クイックスロット」「クラフト材料スロット」など、
    どこでも DragDropSlot をアタッチするだけで同じドラッグ&ドロップ挙動を使い回せます。
  • 継承地獄からの解放
    「InventorySlotBase → EquipmentSlot → WeaponSlot」のような継承ツリーを作らずに、
    コンポーネントを組み合わせて機能を足していく構成にできます。

さらに、レベルデザインの観点でもメリットがあります。
UI デザイナーやレベルデザイナーが「このボタン、アイテムドロップ受け取れるようにしておいて」と言ったとき、
DragDropSlot ノードをペタっと貼り付けるだけで対応できます。コード改造は最小限です。

改造案:右クリックでアイテムをクイック使用する

たとえば DragDropSlot を少し拡張して、「右クリックでアイテムを使用する」機能を入れたい場合は、
親の InventorySlot.gd にこんな関数を追加するだけで済みます。


func _gui_input(event: InputEvent) -> void:
	## 既存のドラッグ終了処理
	if event is InputEventMouseMotion and not Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT):
		drag_slot.handle_drag_exit()

	## 右クリックでアイテム使用
	if event is InputEventMouseButton \
	and event.button_index == MOUSE_BUTTON_RIGHT \
	and event.pressed:
		if drag_slot.current_item_data != null:
			_use_item(drag_slot.current_item_data)

func _use_item(item_data: Variant) -> void:
	## ここにアイテム使用ロジックを実装
	## 例: HP 回復アイテムならプレイヤーの HP を回復して、スロットを空にする
	print("Use item:", item_data)
	drag_slot.set_item_data(null)

ドラッグ&ドロップのロジックは DragDropSlot に閉じているので、
右クリック処理のような「スロット固有の振る舞い」は、親の Control 側に好きなだけ足していけます。
この「機能ごとに責務を分けてコンポーネント化する」スタイルに慣れてくると、Godot の UI 開発がかなり楽になりますね。