In this lesson, we’ll add an interactive quick-bar that gives the player fast access to items to place them in the world.
If you’re the type to prefer to be a little more efficient, opening your inventory window and finding what you need and closing the window may be a bit slow and clunky for you. You may prefer to reach down into a bar at the bottom of the inventory and grab something you use frequently, or press a keyboard shortcut button and switch to it.
That’s what the quick-bar is for. It’s an inventory bar that provides you quick access to some items as it’s always visible, even when the inventory window isn’t.
The quick-bar is a variant of the inventory bar. It functions the same, but it uses a different inventory panel to display keyboard shortcuts, as illustrated in the picture above.
Create a new scene named QuickbarInventoryPanel.tscn
and make it of type VBoxContainer
. We want the shortcut to be in line with the inventory panel and the container will arrange them vertically. Set its Custom Constants -> Separation property to 10
to match the inventory bars’.
Add a Label
node and set its Align property to Center so the number will show in the middle, and instance InventoryPanel.tscn
as a child.
Add a QuickbarInventoryPanel.gd
script to the root of the scene. Remember that the panels will receive a setup()
call to interact with the mouse’s inventory, so we need to forward it to the child inventory panel.
extends VBoxContainer ## Forwards the call to `setup()` to the inventory panel func setup(gui: Control) -> void: $InventoryPanel.setup(gui)
We have the panel. Now, we need to set the scene for the new quick-bar. Create a new scene, QuickBar.tscn
, and make it of type HBoxContainer
.
Attach a new script to the root node, QuickBar.gd
, but instead of extending HBoxContainer
, make it extend InventoryBar
. That’s because our quick-bar is just a variation of the regular inventory rows.
class_name QuickBar extends InventoryBar ## We override _make_panels() from the parent class to configure the label. func _make_panels() -> void: # We create all the item slots as in the parent class, except we're going to instance # `QuickbarInventoryPanel`. Make a new item slot and add it as a child. for i in slot_count: var panel := InventoryPanelScene.instance() add_child(panel) # The inventory bar expects a list of `IventoryPanel` nodes to function. # So we make sure we get that node from each `QuickbarInventoryPanel` and # append it to the `panels`. panels.append(panel.get_node("InventoryPanel")) # Here's where we set the shortcut number on the label. var index := wrapi(i + 1, 0, 10) panel.get_node("Label").text = str(index)
Our value i
ranges from 0
to slot_count - 1
(included). We need to shift the numbers by one because the first key on the keyboard is 1
, and we use wrapi()
so that when the number reaches 10
, the value cycles back to 0
, matching the numbers above the keyboard’s home row.
Now, make sure to also assign QuickBarInventoryPanel.tscn
to the QuickBar
node’s Inventory Panel Scene property.
We need to add the quick-bar to the GUI so it’s always accessible. We don’t want it to float in the middle of the screen like the inventory window, or it’ll get in the way. It should instead anchor at the bottom of the window.
We can do that by wrapping it inside of a container that provides significant padding above it.
In GUI.tscn
, add a new MarginContainer
as a child of GUI
. Set its Custom Constants -> Margin Top to 900, or whatever number you feel will get the bar to the bottom without spilling out of the screen boundaries.
Also, set its Mouse -> Filter to Ignore so it doesn’t block mouse clicks.
Instance the QuickBar.tscn
scene as a child of that MarginContainer
, which should bring it down low.
All that’s left is to set it up inside of GUI.gd
.
#... # onready var player_inventory := $HBoxContainer/InventoryWindow onready var quickbar := $MarginContainer/QuickBar # We'll use it later to reparent the `quickbar` node. onready var quickbar_container := $MarginContainer func _ready() -> void: #... quickbar.setup(self)
And that’s it! We did all the hard work of making InventoryBar
self-sufficient specifically so that it’d be easy to extend and create a variant of it.
Of course, we added those labels and invite the player to press the keyboard, but right now, nothing happens. We need to change that.
When the player presses a number on the number bar, what should happen? We already have the functionality working with clicking the mouse on them. Couldn’t we re-use it?
We can make it so that when the player presses one of those numbers, we simulate a fake mouse click that lands on the corresponding quick-bar panel.
Open up GUI.gd
and let’s do that.
## Each of the action as listed in the input map. We place them in an array so we ## can iterate over each one. const QUICKBAR_ACTIONS := [ "quickbar_1", "quickbar_2", "quickbar_3", "quickbar_4", "quickbar_5", "quickbar_6", "quickbar_7", "quickbar_8", "quickbar_9", "quickbar_0" ] func _unhandled_input(event: InputEvent) -> void: #if event.is_action_pressed("toggle_inventory"): #... # If we pressed anything else, we can check against our quickbar actions else: for i in QUICKBAR_ACTIONS.size(): # If the action matches with one of our quickbar actions, we call # a function that simulates a mouse click at its location. if InputMap.event_is_action(event, QUICKBAR_ACTIONS[i]) and event.is_pressed(): _simulate_input(quickbar.panels[i]) # We break out of the loop, since there cannot be more than one # action pressed in the same event. We'd be wasting resources otherwise. break ## Simulates a mouse click at the location of the panel. func _simulate_input(panel: InventoryPanel) -> void: # Create a new InputEventMouseButton and configure it as a left button click. var input := InputEventMouseButton.new() input.button_index = BUTTON_LEFT input.pressed = true # Provide it directly to the panel's `_gui_input()` function, as we don't care # about the rest of the engine intercepting this event. panel._gui_input(input)
In Godot, you can create custom input events by creating objects that extend InputEvent
or one of its derived types. Then, you can:
Input.parse_input_event()
. In that case, it acts as a regular event and gets forwarded to every node, following the tree and input callback order.This system is powerful. You can use it to define custom event types, like a swipe on mobile, and then feed them to every node.
If you run the game and put items in your quick-bar, you can press the corresponding number key on your keyboard to select the item. Everything works as expected: you can swap with other items or press a number on a blank panel to have the held item jump into it.
Depending on your screen’s size and your GUI layout, you may get eye fatigue dragging your eyes from the inventory window down to the GUI window. It’d be nice if we could merge the two when the inventory window is open, but have the quick-bar still at the bottom when it’s closed.
With a bit of re-parenting, we can do that too. When opening the inventory, we’ll remove the quick-bar from its container and add it as a child of the inventory window.
Open up InventoryWindow.gd
to add a helper function to grab the quick-bar for us.
## Removes the provided quickbar from its current parent and makes it a sibling ## of the other inventory bars. func claim_quickbar(quickbar: Control) -> void: quickbar.get_parent().remove_child(quickbar) inventory_path.add_child(quickbar)
Now in GUI.gd
, we can edit _open_inventories()
and _close_inventories()
to grab or release the quick-bar as needed.
func _open_inventories() -> void: #... player_inventory.claim_quickbar(quickbar) func _close_inventories() -> void: #... _claim_quickbar() ## Removes the quickbar from its current parent and puts it back under the ## quickbar's margin container func _claim_quickbar() -> void: quickbar.get_parent().remove_child(quickbar) quickbar_container.add_child(quickbar)
With the code above, when opening your inventory, you should now see this. It makes it easier to drag items from the inventory to the quick-bar, and vice versa.
This completes the inventory management for the player. You have stacks you can split, a quick-bar you can access with the keyboard, and it looks nice and merges with the inventory window as if it belongs.
Here are the complete scripts we either created or modified in this lesson.
Quickbarinventorypanel.gd
extends VBoxContainer func setup(gui: Control) -> void: $InventoryPanel.setup(gui)
QuickBar.gd
class_name QuickBar extends InventoryBar func _make_panels() -> void: for i in slot_count: var panel := InventoryPanelScene.instance() add_child(panel) panels.append(panel.get_node("InventoryPanel")) var index := wrapi(i + 1, 0, 10) panel.get_node("Label").text = str(index)
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) 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 _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)
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 _on_InventoryBar_inventory_changed(panel, held_item) -> void: emit_signal("inventory_changed", panel, held_item)