Skip to content

Articles

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 qty set.
  • 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.