When the player picks up an item, we need to check all inventory slots, see which are available and put it in an available one. If there are slots that already have the same item in them and there is room on the stack, we should prioritize them.
To do so, and to handle edge cases when picking up items, we need to write new functions. That’s what we’ll do in this lesson.
We have a blueprint on our ground item waiting for pick up, and it has a name as a string. We want to check if any inventory slots already have this name as its held item, so let’s code a lookup function in our GUI.
That’s another interaction that involves both the game world and the user interface, so we need to coordinate it through the GUI
node.
The GUI
has an InventoryWindow
that contains several InventoryBar
nodes. So we need to loop over the item slots in every bar and send relevant data up to the GUI
that’ll process the information.
The place to start is the script that directly references each inventory panel: InventoryBar.gd
. We write a function that returns all inventory slots matching an item’s name.
## Returns an array of inventory panels that have a held item that have a name that ## matches the item id provided. func find_panels_with(item_id: String) -> Array: var output := [] for panel in panels: # Check if there is an item and its name matches if panel.held_item and Library.get_entity_name_from(panel.held_item) == item_id: output.push_back(panel) return output
We then call that new function on each bar from InventoryWindow.gd
.
## Returns an array of inventory panels that have a held item with a name matching ## the item id from the inventory bars. func find_panels_with(item_id: String) -> Array: var output := [] for inventory in inventories: output += inventory.find_panels_with(item_id) return output
While we don’t need the function in GUI.gd
for picking up items, it will come in useful in the next chapter when we deal with crafting items and keeping track of the user’s inventory.
We can add a call this method and concatenate its return value with a call to quickbar.find_panels_with()
. This gives us a list of all inventory slots with a specific item type, be it in the quick-bar or the inventory window.
## Returns an array of inventory panels containing a held item that has a name ## that matches the provided item id from the player inventory and quick-bar. func find_panels_with(item_id: String) -> Array: var existing_stacks: Array = ( quickbar.find_panels_with(item_id) + player_inventory.find_panels_with(item_id) ) return existing_stacks
With that, we have the data we need to add items to existing stacks.
We’ll code that in GUI.gd
.
First, we define a function to add an item to the inventory. It’s a bit long because we need to handle the following cases:
Also, we prioritize inventory slots in this order:
Let’s start with adding the item to the existing inventory stack. We’ll implement the other features as we move forward in the lesson.
To detect when the player picks up an item, we use our Event bus singleton and call add_to_inventory()
.
Open GUI.gd
and add the following code to it.
func _ready() -> void: #... Events.connect("entered_pickup_area", self, "_on_Player_entered_pickup_area") ## Tries to add the blueprint to the inventory, starting with existing item ## stacks and then to an empty panel in the quickbar, then in the main inventory. ## Returns true if it succeeds. func add_to_inventory(item: BlueprintEntity) -> bool: # This is temporary for testing! We'll replace this shortly. return false ## Tries to add the ground item detected by the player collision into the player's ## inventory and trigger the animation for it. func _on_Player_entered_pickup_area(item: GroundItem, player: KinematicBody2D) -> void: if not (item and item.blueprint): return # We get the current amount inside the stack. It's possible for there to be # no space for the entire stack, but we could still pick up parts of the stack. var amount := item.blueprint.stack_count # Attempts to add the item to existing stacks and available space. if add_to_inventory(item.blueprint): # If we succeed, we play the `do_pickup()` animation, disable collision, etc. item.do_pickup(player) else: # If the attempt failed, we calculate if the stack is smaller than it # used to be before we tried picking it up. if item.blueprint.stack_count < amount: # If so, we need to create a new duplicate ground item whose job is to animate # itself flying to the player. var new_item := item.duplicate() # We need to use `call_deferred` to delay the new item by a frame because # we disable the shape's collision so it can't be picked up twice. # # As the physics engine is currently busy dealing with the collision # with the player's area and Godot doesn't allow us to change # collision states when its physics engine is busy, we need to wait # so it won't complain or cause errors. item.get_parent().call_deferred("add_child", new_item) new_item.call_deferred("setup", item.blueprint) new_item.call_deferred("do_pickup", player)
The call_deferred()
method takes care of calling functions “during idle time”, that is to say, when the engine’s done with essential processes, like physics processing. It adds the calls to a queue and executes them as soon as the engine finished updating the world this frame.
If you run the game now, you can’t actually pick up items because we haven’t coded the logic to actually get them into the user’s inventory. We need to have the items either replenish existing stacks, or go into the first available empty panel.
We need a function to do those tasks. Open InventoryBar.gd
where we can do that.
## Tries to add the provided item to the first available empty space. Returns ## true if it succeeds. func add_to_first_available_inventory(item: BlueprintEntity) -> bool: var item_name := Library.get_entity_name_from(item) for panel in panels: # If the panel already has an item and its name matches that of the item # we are trying to put in it, _and_ there is space for it, we merge the # stacks. if ( panel.held_item and Library.get_entity_name_from(panel.held_item) == item_name and panel.held_item.stack_count < panel.held_item.stack_size ): var available_space: int = panel.held_item.stack_size - panel.held_item.stack_count # If there is not enough space, we reduce the item count by however # many we can fit onto it, then move on to the next panel. if item.stack_count > available_space: var transfer_count := item.stack_count - available_space panel.held_item.stack_count += transfer_count item.stack_count -= transfer_count # If there is enough space, we increment the stack, destroy the item, and # report success. else: panel.held_item.stack_count += item.stack_count item.queue_free() return true # If the item is empty, then automatically put the item in it and report success. elif not panel.held_item: panel.held_item = item return true # There is no more available space in this inventory bar or it cannot pick up # the item. Report as much. return false
Like we did with the find_panels_with()
function, we need to forward the message from the GUI, starting with InventoryWindow.gd
## Adds the provided item to the first available spaces it can find in the ## inventory bars. Returns true if it succeeds. func add_to_first_available_inventory(item: BlueprintEntity) -> bool: for inventory in inventories: if inventory.add_to_first_available_inventory(item): return true return false
And finally, we can replace GUI.gd
’s temporary add_to_inventory()
function with the real one by trying with the quick-bar first, then the inventory window. Remove the temporary return false
and replace it with a call to the quick bar and inventory window.
func add_to_inventory(item: BlueprintEntity) -> bool: # If the item is already in the scene tree, remove it first. if item.get_parent() != null: item.get_parent().remove_child(item) if quickbar.add_to_first_available_inventory(item): return true return player_inventory.add_to_first_available_inventory(item)
Now, if you run the game and put down every battery, then deconstruct them and run over them, you’ll hoover them up right into your quick-bar as intended!
Here are the complete scripts we modified in this lesson.
InventoryBar.gd
class_name InventoryBar extends HBoxContainer signal inventory_changed(panel, held_item) export var InventoryPanelScene: PackedScene export var slot_count := 10 var panels := [] func _ready() -> void: _make_panels() func setup(gui: Control) -> void: for panel in panels: panel.setup(gui) panel.connect("held_item_changed", self, "_on_Panel_held_item_changed") func find_panels_with(item_id: String) -> Array: var output := [] for panel in panels: if panel.held_item and Library.get_entity_name_from(panel.held_item) == item_id: output.push_back(panel) return output func add_to_first_available_inventory(item: BlueprintEntity) -> bool: var item_name := Library.get_entity_name_from(item) for panel in panels: if ( panel.held_item and Library.get_entity_name_from(panel.held_item) == item_name and panel.held_item.stack_count < panel.held_item.stack_size ): var available_space: int = panel.held_item.stack_size - panel.held_item.stack_count if item.stack_count > available_space: var transfer_count := item.stack_count - available_space panel.held_item.stack_count += transfer_count item.stack_count -= transfer_count else: panel.held_item.stack_count += item.stack_count item.queue_free() return true elif not panel.held_item: panel.held_item = item return true return false func _make_panels() -> void: for _i in slot_count: var panel := InventoryPanelScene.instance() add_child(panel) panels.append(panel) func _on_Panel_held_item_changed(panel: Control, held_item: BlueprintEntity) -> void: emit_signal("inventory_changed", panel, held_item)
InventoryWindow.gd
extends MarginContainer signal inventory_changed(panel, held_item) var gui: Control onready var inventory_path := $PanelContainer/MarginContainer/Inventories onready var inventories := inventory_path.get_children() func setup(_gui: Control) -> void: gui = _gui for bar in inventories: bar.setup(gui) var engine: BlueprintEntity = Library.blueprints.StirlingEngine.instance() engine.stack_count = 4 var battery: BlueprintEntity = Library.blueprints.Battery.instance() battery.stack_count = 4 inventories[0].panels[0].held_item = engine inventories[0].panels[1].held_item = battery func claim_quickbar(quickbar: Control) -> void: quickbar.get_parent().remove_child(quickbar) inventory_path.add_child(quickbar) func add_to_first_available_inventory(item: BlueprintEntity) -> bool: for inventory in inventories: if inventory.add_to_first_available_inventory(item): return true return false func find_panels_with(item_id: String) -> Array: var output := [] for inventory in inventories: output += inventory.find_panels_with(item_id) return output func _on_InventoryBar_inventory_changed(panel, held_item) -> void: emit_signal("inventory_changed", panel, held_item)
GUI.gd
extends CenterContainer const QUICKBAR_ACTIONS := [ "quickbar_1", "quickbar_2", "quickbar_3", "quickbar_4", "quickbar_5", "quickbar_6", "quickbar_7", "quickbar_8", "quickbar_9", "quickbar_0" ] var blueprint: BlueprintEntity setget _set_blueprint, _get_blueprint var mouse_in_gui := false onready var _is_open: bool = $HBoxContainer/InventoryWindow.visible onready var player_inventory := $HBoxContainer/InventoryWindow onready var _drag_preview := $DragPreview onready var _gui_rect := $HBoxContainer onready var quickbar := $MarginContainer/QuickBar onready var quickbar_container := $MarginContainer func _ready() -> void: player_inventory.setup(self) quickbar.setup(self) Events.connect("entered_pickup_area", self, "_on_Player_entered_pickup_area") func _unhandled_input(event: InputEvent) -> void: if event.is_action_pressed("toggle_inventory"): if _is_open: _close_inventories() else: _open_inventories() else: for i in QUICKBAR_ACTIONS.size(): if InputMap.event_is_action(event, QUICKBAR_ACTIONS[i]) and event.is_pressed(): _simulate_input(quickbar.panels[i]) break func find_panels_with(item_id: String) -> Array: var existing_stacks: Array = ( quickbar.find_panels_with(item_id) + player_inventory.find_panels_with(item_id) ) return existing_stacks func add_to_inventory(item: BlueprintEntity) -> bool: if item.get_parent() != null: item.get_parent().remove_child(item) if quickbar.add_to_first_available_inventory(item): return true return player_inventory.add_to_first_available_inventory(item) func _simulate_input(panel: InventoryPanel) -> void: var input := InputEventMouseButton.new() input.button_index = BUTTON_LEFT input.pressed = true panel._gui_input(input) func _process(delta: float) -> void: var mouse_position := get_global_mouse_position() mouse_in_gui = _is_open and _gui_rect.get_rect().has_point(mouse_position) func destroy_blueprint() -> void: _drag_preview.destroy_blueprint() func update_label() -> void: _drag_preview.update_label() func _set_blueprint(value: BlueprintEntity) -> void: if not is_inside_tree(): yield(self, "ready") _drag_preview.blueprint = value func _get_blueprint() -> BlueprintEntity: return _drag_preview.blueprint func _open_inventories() -> void: _is_open = true player_inventory.visible = true player_inventory.claim_quickbar(quickbar) func _close_inventories() -> void: _is_open = false player_inventory.visible = false _claim_quickbar() func _claim_quickbar() -> void: quickbar.get_parent().remove_child(quickbar) quickbar_container.add_child(quickbar) func _on_Player_entered_pickup_area(item: GroundItem, player: KinematicBody2D) -> void: if not (item and item.blueprint): return var amount := item.blueprint.stack_count if add_to_inventory(item.blueprint): item.do_pickup(player) else: if item.blueprint.stack_count < amount: var new_item := item.duplicate() item.get_parent().call_deferred("add_child", new_item) new_item.call_deferred("setup", item.blueprint) new_item.call_deferred("do_pickup", player)