M6.4 — Item Pickups on the Ground¶
What you'll learn
- How to spawn a dropped item into the world as an
Area2Dpickup that carries itsItemData. - How to show rarity at a glance — a colored label and/or a "beam" — using
Rarity.COLORS(M6.2). - Two pickup styles — walk-over (auto) versus press-to-pick (the M1.3
interactaction) — and when each fits. - How a pickup hands its item to the inventory via signal, closing the gap between "it dropped" (M6.3) and "it's in my bag" (M7).
How it applies
- The drop on the floor is the reward made visible. M6.3 decided an item exists; this is where the player sees it — a glowing thing with a color that tells them whether to run over. The pickup is the moment the loot loop pays out, and its readability (rarity color, label) is what makes a good drop feel good.
- Pickup style shapes pacing. Auto-pickup on walk-over keeps a fast game flowing (good for common currency); press-to-pick gives the player control and avoids grabbing junk (good for gear). The choice is a design decision with a real feel difference.
- Floor clutter is a real problem. A dense fight leaves many drops; rarity-colored labels let the player triage at a glance, and a despawn timer on commons keeps the floor from becoming noise. These are quality-of-life decisions the genre lives and dies on.
- The pickup is the only node in the item story.
ItemDatais data; the pickup is the brief physical node that holds it in the world until collected, then hands it off and frees itself — a clean lifecycle the save system (M8) only has to account for at the data level.
Concepts¶
The pickup is a small node holding data¶
A dropped item needs a physical presence: something the player can see and collect. That is an Area2D
(detects the player) with a Sprite2D (the item icon) and a Label (the name, colored by rarity). It
holds the ItemData from M6.3. When collected, it announces the item and frees itself. The item data
lives on; the pickup node is temporary.
Example
A pickup scene's script. It is handed an ItemData, displays it with the rarity color, and on
collection emits the item and removes itself:
# res://scripts/item_pickup.gd
class_name ItemPickup
extends Area2D
var item: ItemData
@onready var sprite: Sprite2D = $Sprite
@onready var label: Label = $Label
func setup(new_item: ItemData) -> void:
item = new_item
func _ready() -> void:
sprite.texture = item.icon
label.text = item.display_name
label.modulate = Rarity.COLORS[item.rarity] # item.rarity is a Rarity.Tier value (M6.2)
body_entered.connect(_on_body_entered)
func _on_body_entered(body: Node2D) -> void:
if body.is_in_group("player"):
_collect()
func _collect() -> void:
SignalBus.item_picked_up.emit(item) # M7's inventory listens
queue_free()
The pickup is on the pickup layer (8) with a mask for player (2), so body_entered fires when the
player walks over it. It emits item_picked_up (a new SignalBus signal) and frees itself —
decoupled from the inventory exactly like the death event is decoupled from loot.
Showing rarity¶
The single most important readability cue is the rarity color, pulled from the Rarity.COLORS table
(M6.2) so the convention is consistent with everything else (the M7 tooltip uses the same table). Two
common presentations:
- Colored name label above the drop — cheap, precise, readable even in a pile.
- Light beam / glow — a vertical colored gradient or a
PointLight2Din the rarity color, visible from across the room. Higher rarities get brighter/taller beams so the player spots a yellow from afar.
Start with the colored label; add a beam for higher tiers when you want drops to call attention from a distance. Both read from the same color table, so a rarity recolor is one edit.
Walk-over vs press-to-pick¶
Two collection styles, each a design choice:
- Walk-over (auto).
body_enteredcollects immediately, as above. Fast and frictionless — good for currency and commons in a brisk game. Risk: the player hoovers up junk they didn't want. - Press-to-pick.
body_entered/body_exitedmark the pickup as "in range"; the player presses theinteractaction (M1.3) to collect the nearest in-range pickup. More control, no accidental grabs — the usual choice for gear in a loot-focused ARPG.
Example
Press-to-pick: track range, collect on the interact action.
var _player_in_range := false
func _on_body_entered(body: Node2D) -> void:
if body.is_in_group("player"):
_player_in_range = true
func _on_body_exited(body: Node2D) -> void:
if body.is_in_group("player"):
_player_in_range = false
func _unhandled_input(event: InputEvent) -> void:
if _player_in_range and event.is_action_pressed("interact"):
_collect()
Using the interact action (M1.3) means the same button picks up loot, opens doors, and talks to NPCs
— consistent and rebindable. With several pickups in range, you'd collect the nearest; for one-at-a-time
drops the simple version is fine.
Spawning the pickup (closing M6.3)¶
M6.3's _spawn_pickup(item, at_position) stub becomes real here: instance the pickup scene, setup() it
with the rolled ItemData, position it at the drop location (often with a small random scatter so a pack's
drops don't stack), and add it under World.
Example
@export var pickup_scene: PackedScene # item_pickup.tscn
func _spawn_pickup(item: ItemData, at_position: Vector2) -> void:
var pickup := pickup_scene.instantiate() as ItemPickup
pickup.setup(item)
pickup.global_position = at_position + Vector2.RIGHT.rotated(randf() * TAU) * randf() * 16.0
get_tree().current_scene.get_node("World").add_child(pickup)
The small random offset scatters multiple drops so they don't perfectly overlap. setup() before
add_child so the item is set when _ready runs and reads it.
Lifecycle and clutter¶
A pickup that is never collected lingers. For commons, a despawn Timer (e.g., 30–60 s) keeps the floor
clean; rares might linger longer or forever. Pickups are also high-churn in a dense fight — a candidate
for the object pooling from M4.4 if profiling shows the instantiate/free of many drops causing hitches.
Walkthrough¶
- Build the pickup scene:
Area2Droot renamedItemPickup, childrenCollisionShape2D(a small circle),Sprite2DnamedSprite,LabelnamedLabel(positioned above). Attachitem_pickup.gd. Set the area's layer topickup(8) and mask toplayer(2). Save asres://scenes/fx/item_pickup.tscn. - Add the
item_picked_up(item: ItemData)signal toSignalBus. - Replace M6.3's
_spawn_pickupstub with the real instancing (above); assignpickup_scene=item_pickup.tscnon the loot system in the Inspector. - Choose a style: start with walk-over (
_on_body_entered→_collect), or wire press-to-pick with theinteractaction. - (Temporary, until M7) Connect
SignalBus.item_picked_upto a printer:func(i): print("picked up ", i.display_name). - Press
F5, kill skeletons, and watch drops appear with rarity-colored names. Walk over (or pressinteractnear) one and confirm the printer reports it and the pickup disappears.
Optional sanity check
Force a rare by temporarily setting the rarity table so only rare has weight (M6.3). Confirm the
dropped label is the rare color from Rarity.COLORS, not a default — proof the pickup reads the shared
rarity table. Then drop several items at once and confirm the random scatter keeps their labels
readable rather than stacked exactly. Restore the rarity weights.
Self-check quiz¶
Q1 — Why is the pickup an Area2D while the ItemData it carries is a Resource?
A. Area2D renders icons; Resources can't. B. The pickup is the temporary physical presence (detects the player, shows the icon, then frees itself); the item is the persistent data it carries and hands to the inventory. Node for presence, Resource for data. C. Resources can't be added to the scene tree, so loot must be an Area2D. D. Area2D is required for queue_free.
Reveal answer
B. The split mirrors the whole item model: data (Resource) persists into the inventory and the save; the world presence (node) is brief — it detects the player, displays the item, emits it, and frees itself. A is false (a Sprite2D child renders the icon). C is true that Resources aren't tree nodes but isn't the reasoning. D is false.
Q2 — Why does the pickup read its color from Rarity.COLORS rather than storing its own color?
A. To save memory. B. So rarity color is defined once and shared by the pickup label, any beam, and the M7 tooltip — a single source of truth, so recoloring a tier is one edit everywhere. C. Because Area2D can't store a Color. D. Colors must be constants.
Reveal answer
B. Centralizing the rarity-to-color mapping (M6.2) keeps every surface that shows rarity consistent and makes a recolor a one-line change. A duplicated color per pickup would drift from the tooltip's color. A, C, D are false.
Q3 — When is press-to-pick preferable to walk-over auto-pickup?
A. Never; auto-pickup is always better. B. For gear in a loot-focused game, where the player wants control and to avoid grabbing junk; walk-over suits currency/commons in a fast game. C. Only on mobile. D. Press-to-pick is required for rares.
Reveal answer
B. Press-to-pick gives deliberate control (don't hoover up junk, choose what to grab), which suits gear; walk-over keeps a brisk game flowing for low-value pickups. A overstates auto-pickup. C and D are fabricated constraints.
Integration question¶
Q4 — open
Follow one item across the entire loot loop, naming the M4/M6 piece responsible at each step: a skeleton
dies, an item ends up in the player's hands. Explain specifically how the pickup decouples 'an item
exists in the world' from 'an item is in the inventory,' and why that decoupling (via item_picked_up)
is the same architectural move as enemy_died decoupling death from loot.
Reveal expected answer
The chain: the skeleton's Health.died fires and enemy.gd emits SignalBus.enemy_died(self,
position) (M4.1); the loot system, subscribed to that signal, rolls the weighted drop and rarity
tables (M6.3), and on a hit calls ItemFactory.roll to deep-duplicate a base type (M6.1) and roll
affixes for the chosen rarity (M6.2), producing a finished ItemData; the loot system instances the
pickup scene, setup()s it with that item, and places it at the death position (M6.4); the pickup
displays the icon and the rarity-colored name from Rarity.COLORS, waits for the player (walk-over
or interact), then emits SignalBus.item_picked_up(item) and frees itself; M7's inventory,
subscribed to that signal, adds the item to the bag. The pickup decouples "an item exists in the
world" from "an item is in the inventory" by making collection an announcement: the pickup never
references the inventory, it just emits item_picked_up and disappears, and any inventory (or a
test double, or a future auto-collect system) subscribes. This is exactly the enemy_died move from
M4.1 — the producer of an event names no consumer, so the loot system could be added without
touching enemies and the inventory can be added without touching pickups. The same decoupling
principle bookends the loop: an enemy announces its death without knowing loot exists, and a pickup
announces its collection without knowing the inventory exists, so each system attaches by
subscribing rather than by being wired into the others.