Skip to content

Tutorials · Interfaces · Chapter 10

Drag and drop

30 minutes Uses: DragSource, DropTarget, Panel

By the end

  • Every slot of the hotbar and the bag both ends of a drag
  • The item carried shows under the pointer; a landing moves it; a key over the hotbar, or anything over a full slot or nothing, goes back
On this page

Two ends a slot

A drag has two ends: the node you pick an item up from, and the node you let it go on. A Drag Source is the first and a Drop Target the second, and a slot of the hotbar or the bag is both. You can pick its item up, and you can drop one on it while it is empty.

What is carried

While an item is carried it shows under the pointer. That picture is a node of its own, a copy of the slot’s item, hidden everywhere else. Every slot gets one, a ghost, as a child of the slot.

  1. Select bag_1, click + Add node and choose Panel. Rename it bag_1_ghost, and untick Visible.
  2. In its Rect, set Anchor Min and Anchor Max both to 0.5, 0.5, Offset Min to -24, -24 and Offset Max to 24, 24: a square of 48, centred on the slot. In its Panel, set Color to 255, 255, 255, alpha 255 and Texture to items.png.
  3. In Assets, press Create, choose New script, type slot_ghost, and press Enter. In the Code tab, replace everything in slot_ghost.rgs with the script below, and press Ctrl+S.
rgs slot_ghost.rgs
/// What a slot's item looks like while it is dragged: a copy of the slot's
/// picture, which the slot's Drag Source names as its Preview and draws under
/// the pointer while the item is carried. It stays hidden, so it is drawn
/// nowhere else.
component SlotGhost {
    /// The slot whose item it shows, which is kept by that name: hotbar_1 to
    /// hotbar_4, or bag_1 to bag_16.
    slot: String = "bag_1",

    fn loop() {
        // The satchel and the slots keep what each slot holds: 1 a potion, 2
        // the elixir, 3 a key, 0 nothing. An empty slot is never picked up,
        // so it needs no picture.
        let item = kept_number(slot, 0.0);
        if item == 1.0 {
            set_text("PanelComponent", "region_name", "potion");
        } else if item == 2.0 {
            set_text("PanelComponent", "region_name", "elixir");
        } else if item == 3.0 {
            set_text("PanelComponent", "region_name", "key");
        }
    }
}
  1. Select bag_1_ghost, click Add component and choose SlotGhost. Its Slot starts as bag_1: leave it.

The ghost’s picture is items.png, the sheet of chapter 7, and the script says which region of it: the slot’s own item, as the slot’s picture does. set_text writes the region’s name, potion, elixir or key, into the Panel’s Atlas Region. An empty slot is never picked up, so it needs no picture.

The two ends

  1. Select bag_1, click Add component and choose DragSourceComponent. Set Action to bag_1. Clear Kind, which starts as item. Choose float in the box at the left of Payload and leave its number at 0. Set Preview to bag_1_ghost, with the arrow at its right, and tick Disabled.
  2. Click Add component and choose DropTargetComponent. Set Action to bag_1_in. Under Accepts, press the + three times and set the entries to potion, elixir and key. Disabled stays unticked.
bag_1 selected, its Inspector narrowed with the search box, dragsource, to its Drag Source: Action bag_1, Kind empty, Payload float 0.00, Preview bag_1_ghost and Disabled ticked; the Viewport shows the HUD’s plate at the top left of the room and the bag at its right, with a blue outline round the bag’s first slot, which its Grid places
Action
A Drag Source’s Action: the action it presses when a drag from it ends. Its amount is 1 if the item landed somewhere and 0 if it came back. A script hears it with pressed, and amount says which. Here it is the slot’s own name, bag_1.
Kind
What sort of thing the source carries, a word a Drop Target’s Accepts list is matched against. The script below writes it from what the slot holds, every step: potion, elixir or key. Empty here, since every slot starts empty. A new Drag Source’s is item.
Payload
The thing itself, whatever the game means: an item’s number, a slot’s number, some words. Its box offers a shape, and float, a number, is the one here: the item’s number, which the satchel keeps for the slot, 1 for a potion, 2 for the elixir and 3 for a key. A drop hands it to the target as its amount. The script writes it every step, so it starts at 0.
Preview
The name of the node drawn under the pointer while the item is carried, the ghost, bag_1_ghost. It is laid out by its own Rect in the box of the slot it was picked up from, as it would be in a parent, drawn over everything, a dialog included, and never cut at the bag’s edge.
Disabled
Both ends have a Disabled. On a Drag Source it means the slot cannot be picked up, so a press on it is only a press, and a Button on the same node still clicks. On a Drop Target it means the slot takes no drop, and an item let go over it goes back. The script switches both as the game runs. Every slot starts empty, so each source starts ticked, and the script unticks it as soon as its slot holds something. Each target starts unticked, and the script ticks it while its slot is full.
bag_1 selected, its Inspector narrowed with the search box, droptarget, to its Drop Target: Action bag_1_in, Accepts, a list of 3 items, holding potion, elixir and key, Disabled unticked and a Trim Kinds button; the Viewport shows the HUD’s plate at the top left of the room and the bag at its right, with a blue outline round the bag’s first slot, which its Grid places
Action
A Drop Target’s Action: the action it presses when a drag lands on it, with the number the drag carried as its amount. Here it is the slot’s name with _in after it, bag_1_in.
Accepts
The kinds the target takes, a list of words matched against a source’s Kind. Empty, it takes anything. The bag takes potion, elixir and key. The hotbar takes only potion and elixir: a key let go over it is refused. Press the + to add a kind. Trim Kinds, under the list, strips stray spaces and drops blank entries.
Disabled
A Drop Target’s Disabled: when it is ticked, the target takes no drop, and a drag let go over it comes back. The script ticks it while the slot is full, and unticks it when it empties, as the game runs, so it is never authored ticked.
  1. In Assets, press Create, choose New script, type slot_drag, and press Enter. In the Code tab, replace everything in slot_drag.rgs with the script below, and press Ctrl+S.
rgs slot_drag.rgs
/// A slot's two ends of a drag, on a slot of the hotbar or the bag: the item
/// it holds can be picked up and carried to another slot, and an item carried
/// here lands in it. An empty slot has nothing to pick up, and a full one
/// takes nothing, so an item let go over it stays where it was.
component SlotDrag {
    /// The slot it is, which is kept by that name: hotbar_1 to hotbar_4, or
    /// bag_1 to bag_16. Its Drag Source presses this name, and its Drop
    /// Target this name with _in after it.
    slot: String = "bag_1",

    fn loop() {
        // The satchel and the slots keep what each slot holds: 1 a potion, 2
        // the elixir, 3 a key, 0 nothing.
        let item = kept_number(slot, 0.0);

        // What a drag from here carries: its kind, which a Drop Target takes
        // only if it is on the target's list, and the item's number, which
        // the slot it lands in keeps.
        let kind = "";
        if item == 1.0 {
            kind = "potion";
        } else if item == 2.0 {
            kind = "elixir";
        } else if item == 3.0 {
            kind = "key";
        }
        set_text("DragSourceComponent", "kind", kind);
        set_number("DragSourceComponent", "payload", item);
        // Switched off, an empty slot is not picked up, so a press on it is
        // only a press, and a full one takes no drop.
        set_flag("DragSourceComponent", "disabled", item == 0.0);
        set_flag("DropTargetComponent", "disabled", item != 0.0);

        // An item let go over a slot that takes it presses that slot's _in
        // action, with the item's number as its amount, and this slot's own
        // action with 1, on the game's next step. Let go anywhere else, over
        // nothing or over a slot that refuses it, it presses this slot's
        // action with 0, and nothing needs doing: the item never left.
        if pressed(slot) && amount(slot) == 1.0 {
            // It landed, and the slot it landed in keeps it now.
            keep_number(slot, 0.0);
        }
        if pressed(slot + "_in") {
            // An item landed here, and the amount is its number.
            keep_number(slot, amount(slot + "_in"));
        }
    }
}
  1. Select bag_1, click Add component and choose SlotDrag. Its Slot starts as bag_1: leave it.

The script keeps the two ends in line with what the slot holds. A drag from a slot whose number is 1 carries a potion, with 1 as its payload. A drop that lands presses two actions, both on the game’s next step. The target’s, bag_5_in, has the item’s number as its amount, and the slot keeps it: pressed(slot + "_in") is true for that step, and amount is what the slot now holds. The source’s own action, hotbar_1, has 1 as its amount, which says the item has left, and the slot empties: pressed(slot) and amount(slot) == 1.0. A drop let go anywhere else, over nothing or over a slot that refuses the item, presses the source’s action with 0, and nothing needs doing: the item never left.

The Code panel with slot_drag.rgs open: a comment on a slot’s two ends of a drag, then component SlotDrag with a slot field set to bag_1 and a loop that reads kept_number for the slot and chooses a kind, potion, elixir or key, for the item, down to the line for key; the Scripts list at the left names the project’s scripts, slot_drag.rgs lit

Every slot

The other nineteen slots work the same, with their own names. A component’s ≡ menu has Copy values and Paste values, and an edit made with several nodes selected is made on all of them, so the shared fields are set once.

  1. Select bag_1, open the ≡ menu of its DragSourceComponent and choose Copy values. Select bag_2, then Ctrl-click bag_3 to bag_16, click Add component and choose DragSourceComponent, then open its ≡ menu and choose the paste entry, Paste values. All fifteen now hold bag_1’s source.
  2. Select bag_1 again, copy the values of its DropTargetComponent, select the same fifteen, add a DropTargetComponent and paste. Then, with the fifteen still selected, add SlotDrag.
  3. Select each of the fifteen in turn and give it its own names. In bag_2, set the source’s Action to bag_2 and its Preview to bag_2_ghost, the target’s Action to bag_2_in and the script’s Slot to bag_2. Do the same up to bag_16.
  4. Select bag_1_ghost and press Ctrl+C. Select bag_2 and press Ctrl+V: the copy lands under the slot, with its Panel and its script. Rename it bag_2_ghost and set its Slot to bag_2. Do the same for each slot up to bag_16.
  5. The hotbar’s four slots are the same, with two differences. Set hotbar_1 up by hand as you did bag_1, with its own names, hotbar_1, hotbar_1_ghost, hotbar_1_in and hotbar_1, and give its Accepts only potion and elixir. A key is not for the hotbar. Then copy the values of its DragSourceComponent and of its DropTargetComponent onto hotbar_2 to hotbar_4, as you did for the bag, add SlotDrag to them and give each its own names.
  6. A hotbar ghost is a Panel under its slot, as the bag’s are, hidden, with Color 255, 255, 255, alpha 255 and Texture items.png, but its Rect is anchored at the slot’s top left corner: Anchor Min stays 0, 0, and Anchor Max is 0, 0, Offset Min -2, -2 and Offset Max 46, 46. Give it a SlotGhost with the slot’s name. Build hotbar_1_ghost once, copy it under each of the other three, and rename and set Slot as before.

The satchel has one more thing to know. A drop fills its slot on the game’s next step, and until then the slot still reads empty. A pickup the satchel is filing, one item a step, into the first empty slot, could go into the very slot an item is being dropped in, and the drop would write over it. So the satchel now skips a slot whose _in action was just pressed.

  1. Open satchel.rgs and replace everything in it with the script below. What changed is a few lines of comment and one test in each of the two loops, where first_hotbar and first_bag are chosen: both now also ask !pressed(slot + "_in"). Press Ctrl+S.
rgs satchel.rgs
/// The hero's satchel: it files what the hero carries into the slots, the
/// hotbar's four and the bag's sixteen. What the hero owns is kept in
/// potions, elixirs and keys. Each slot is kept too, hotbar_1 to hotbar_4 and
/// bag_1 to bag_16, as the item in it: 1 a potion, 2 the elixir, 3 a key, 0
/// nothing. Only the satchel files, one item a step, so two pickups taken on
/// the same step never go in the same slot, and it files nothing into a slot
/// an item is being dropped in.
component Satchel {
    fn loop() {
        // What the slots hold as this step began: how many of each item, how
        // many slots are empty, the first empty one of the hotbar and of the
        // bag that no drop is filling, and the last one holding each item.
        let potions = 0.0;
        let elixirs = 0.0;
        let keys = 0.0;
        let empty_hotbar = 0.0;
        let empty_bag = 0.0;
        let first_hotbar = "";
        let first_bag = "";
        let last_potion = "";
        let last_elixir = "";
        let last_key = "";
        for i in 1..5 {
            let slot = "hotbar_" + text(i);
            let item = kept_number(slot, 0.0);
            if item == 0.0 {
                empty_hotbar += 1.0;
                // An item let go over an empty slot presses the slot's _in
                // action on the game's next step, and the slot takes it on
                // that step: it is filling, so nothing is filed into it.
                if first_hotbar == "" && !pressed(slot + "_in") {
                    first_hotbar = slot;
                }
            } else if item == 1.0 {
                potions += 1.0;
                last_potion = slot;
            } else if item == 2.0 {
                elixirs += 1.0;
                last_elixir = slot;
            } else if item == 3.0 {
                keys += 1.0;
                last_key = slot;
            }
        }
        for i in 1..17 {
            let slot = "bag_" + text(i);
            let item = kept_number(slot, 0.0);
            if item == 0.0 {
                empty_bag += 1.0;
                if first_bag == "" && !pressed(slot + "_in") {
                    first_bag = slot;
                }
            } else if item == 1.0 {
                potions += 1.0;
                last_potion = slot;
            } else if item == 2.0 {
                elixirs += 1.0;
                last_elixir = slot;
            } else if item == 3.0 {
                keys += 1.0;
                last_key = slot;
            }
        }

        // What the hero owns that no slot holds yet waits to be filed; less
        // than none is an item a slot holds that the hero no longer owns.
        let waiting_potions = kept_number("potions", 0.0) - potions;
        let waiting_elixirs = kept_number("elixirs", 0.0) - elixirs;
        let waiting_keys = kept_number("keys", 0.0) - keys;

        // One item a step, into the first empty slot. A potion or the elixir,
        // the two things the hero drinks, goes in the hotbar while it has
        // room, then in the bag: drink_slot is where it goes. A key goes only
        // in the bag.
        let drink_slot = first_hotbar;
        if drink_slot == "" {
            drink_slot = first_bag;
        }
        if waiting_potions > 0.0 && drink_slot != "" {
            keep_number(drink_slot, 1.0);
        } else if waiting_elixirs > 0.0 && drink_slot != "" {
            keep_number(drink_slot, 2.0);
        } else if waiting_keys > 0.0 && first_bag != "" {
            keep_number(first_bag, 3.0);
        }

        // An item the hero no longer owns, as the key the door took, leaves
        // the last slot holding it.
        if waiting_potions < 0.0 {
            keep_number(last_potion, 0.0);
        }
        if waiting_elixirs < 0.0 {
            keep_number(last_elixir, 0.0);
        }
        if waiting_keys < 0.0 {
            keep_number(last_key, 0.0);
        }

        // The room left once what waits is filed, which a pickup reads before
        // it lets the hero take it. What the hotbar has no room for waits for
        // the bag.
        let room_hotbar = empty_hotbar - max(waiting_potions, 0.0) - max(waiting_elixirs, 0.0);
        let room_bag = empty_bag - max(waiting_keys, 0.0);
        if room_hotbar < 0.0 {
            room_bag += room_hotbar;
            room_hotbar = 0.0;
        }
        keep_number("room_hotbar", room_hotbar);
        keep_number("room_bag", room_bag);
    }
}

pressed(slot + "_in") is true for one step, the first after an item was let go over that slot and accepted, and the satchel then leaves the slot to the drop.

Carrying an item

To carry an item, press on its slot and move the pointer, and let go over another slot. A press that moves less than 8 UI units is still a click, and a slot holding an item still opens its menu: a drag suppresses the click, so that one gesture never counts as both. Dragging is a pointer’s gesture. Chapter 14 adds a Move row to the bag’s menus, for a player with no mouse.

The next four pictures pick up the first potion into the hotbar, open the bag, and drag the potion from the hotbar’s first slot to the bag’s fifth. The first is in flight, and the second after it lands.

The bag’s window at the top right, empty, the hotbar’s four slots at the bottom left with the first one paler and still showing its flask, and in the room between them a red flask carried under the pointer
The potion dragged from the hotbar: it is under the pointer, and its slot is drawn as pressed.
The same room later: the red flask in the bag’s fifth slot, the first of its second row, and the hotbar’s four slots empty
Let go over the bag’s fifth slot: it moves there.

The next two pictures pick up the key into the bag instead, and drag it onto the hotbar’s second slot. The hotbar takes potions and the elixir only.

The bag with the key in its first slot, which is drawn darker, and the key itself carried over the hotbar’s second slot at the bottom left, with the hero standing above it in the room
The key carried over the hotbar…
The key back in the bag’s first slot, and the hotbar’s four slots empty
…refused: let go, it goes back to its slot.

The ghosts and the drag ends

The drag ends

NodeComponentFieldValue
bag_1 to bag_16Drag SourceActionbag_1 to bag_16
Kindempty
Payload0
Previewbag_1_ghost to bag_16_ghost
Disabledticked
Drop TargetActionbag_1_in to bag_16_in
Acceptspotion, elixir, key
SlotDragSlotbag_1 to bag_16
hotbar_1 to hotbar_4Drag SourceActionhotbar_1 to hotbar_4
Kindempty
Payload0
Previewhotbar_1_ghost to hotbar_4_ghost
Disabledticked
Drop TargetActionhotbar_1_in to hotbar_4_in
Acceptspotion, elixir
SlotDragSlothotbar_1 to hotbar_4

The ghosts

NodeComponentFieldValue
bag_1_ghost to bag_16_ghostNodeVisibleunticked
RectAnchor Min0.5, 0.5
Anchor Max0.5, 0.5
Offset Min-24, -24
Offset Max24, 24
PanelColor255, 255, 255, alpha 255
Textureitems.png
SlotGhostSlotbag_1 to bag_16
hotbar_1_ghost to hotbar_4_ghostNodeVisibleunticked
RectAnchor Max0, 0
Offset Min-2, -2
Offset Max46, 46
PanelColor255, 255, 255, alpha 255
Textureitems.png
SlotGhostSlothotbar_1 to hotbar_4

Every other field of every node is as it comes.

Check it works