Godot drag and drop inventory
Build drag and drop inventory UI on Item Vault: game-owned slots over container add and remove, with stacking, partial moves, and refresh.
This recipe adds drag and drop to an Item Vault inventory: drag a stack from one slot to another, watch it land, merge, or bounce back when the move is illegal. The split of responsibilities is the whole recipe. Your game owns every pixel (slots, previews, clicks); Item Vault owns every count (capacities, stacks, overflow). Drops call container methods, and the UI redraws from the containers afterward.
It assumes a working inventory from
Getting Started and the pipeline in
Godot inventory system tutorial. The Item
Vault demo ships a working reference: demo/addon_demos/ui/demo_drag_slot.gd
for the slot and demo/addon_demos/03_inventory_grid.gd for the board around
it.
1. Build slots on Godot drag APIs
Each slot is a Control (the demo uses a Panel) implementing the three
standard drag methods. The payload is a plain dictionary carrying the source
container, the source slot index, the item id, and the quantity:
# Drag out: refuse empty slots, tag the payload so only our slots accept it.
func _get_drag_data(_at_position: Vector2) -> Variant:
if drag_payload.is_empty():
return null
var preview := _make_preview()
if preview != null:
set_drag_preview(preview)
var data: Dictionary = drag_payload.duplicate(true)
data["demo_drag"] = true
return data
# Drag over: accept tagged payloads, refuse drops onto the same slot.
func _can_drop_data(_at_position: Vector2, data: Variant) -> bool:
if not (data is Dictionary):
return false
var payload: Dictionary = data
if not bool(payload.get("demo_drag", false)):
return false
if str(payload.get("source", "")) == str(drag_payload.get("source", "")) \
and int(payload.get("slot", -2)) == int(drag_payload.get("slot", -1)):
return false
return true
# Drop: notify the board; the board mutates containers, never the slot.
func _drop_data(_at_position: Vector2, data: Variant) -> void:
if data is Dictionary:
slot_dropped.emit(data as Dictionary)Slots never touch containers directly. They emit slot_clicked,
shift_clicked, and slot_dropped, and the board script answers. That keeps
one mutation path for clicks, drags, and keyboard moves alike.
2. Commit drops through containers
The drop handler moves real stacks between real ItemContainer instances.
Add into the destination first: the return value is the overflow that did not
fit, so the source removes only what actually landed. Nothing is created or
destroyed by the drag itself:
func _transfer_stack(from_p: int, from_s: int, qty: int = -1) -> void:
var stack: ItemStack = _get_stack_at(from_p, from_s)
if stack == null or stack.quantity <= 0:
return
var move_qty: int = stack.quantity
if qty >= 1:
move_qty = mini(qty, stack.quantity)
if move_qty <= 0:
return
var moving := ItemStack.new(stack.item, move_qty)
var overflow: ItemStack = _containers[to_p].add(moving)
var removed: int = move_qty - (0 if overflow == null else overflow.quantity)
if removed > 0:
_containers[from_p].remove(stack.item.id, removed)
_refresh_ui()Merging is free: adding to a container that already holds the item id fills
the existing stack up to max_stack and reports the rest as overflow. A full
destination leaves the source untouched, which reads to the player as the
stack bouncing back.
3. Support click and shift-click beside drag
Not every move is a drag. The demo slot distinguishes three gestures, and your UI should too:
- Plain click selects a source slot; a second click on a destination commits the same transfer a drop would run.
- Shift-click opens an amount picker first, then moves a partial quantity
through the same path with
qtyset. - A drag carries the full stack unless your design says otherwise.
All three gestures funnel into one transfer function. If clicks and drags take separate code paths, they drift: one validates and the other does not.
4. Redraw from containers after every mutation
After each commit, rebuild every slot view from the container contents: clear each slot, render one visual per live stack, and publish a fresh drag payload (or an empty one for empty slots). The UI is a projection, so it never caches counts between mutations. This one habit removes a whole class of desync bugs where the picture shows items the container no longer holds.
Common mistakes
- Letting slots mutate containers. Slots emit; the board commits.
- Separate logic for click moves and drag moves. One transfer function serves both.
- Caching stack counts in slot scripts. Render from containers every time.
- Accepting foreign drop payloads. Tag your own payloads and check the tag
in
_can_drop_data. - Forgetting the same-slot guard. Dropping a slot onto itself must be a no-op.
- Storing item facts in UI code. Slot visuals read names and stack sizes
from the
ItemDatabase, the same source as everything else.
Try the demo
The Item Vault demo includes the drag and drop board this recipe is drawn from: click or drag stacks between two inventories, shift-click for partial amounts, and watch overflow bounce back. Try it in your browser on the Item Vault itch.io page before building your own slots.