The quick item bar

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.

Creating the quick-bar inventory panel

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)

Creating the quick-bar

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.

Setting up the quick-bar

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.

Adding keyboard shortcuts

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:

  1. Call one of the input callbacks manually, like we did above, to simulate an input received only by this node.
  2. Turn it into a game-wide input event by calling 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.

Merging the quick-bar into the inventory window

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.

Code reference

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)