Item Stacks and Mouse Drag Preview

We can now refer to any blueprint-entity combination by their proper, pascal case name.

Now, we have what we need to store any blueprint inside an inventory panel and find the associated entity. Let’s do that and finally get rid of all the temporary test code we used to spawn machines in the previous chapter.

In this lesson, we’ll replace EntityPlacer’s blueprint property for previewing the entity we’re placing with the mouse holding the current item. In the upcoming lessons, changing what item the player is holding will entail replacing that inventory item from the mouse.

Stacks of blueprints

Let’s start by adding support for stacks of items.

Each inventory slot should be able to hold more than one instance of the same item, depending on the maximum allowed stack size for a given item.

Whether you subscribe to Minecraft’s 64-based stack count or prefer Factorio’s 100 to 200, stacks are common to this kind of grid-based inventory system.

Go back to BlueprintEntity.gd and add the following variables:

## How many items can be in a stack of the given blueprint type.
export var stack_size := 1

## How many items are actually in the stack of the current stack.
var stack_count := 1

Visit each of BatteryBlueprint.tscn, StirlingEngineBlueprint.tscn, and WireBlueprint.tscn and set their Stack Size properties.

I chose 8 for the Battery and Stirling Engine and 50 for wires. The amount is arbitrary. How many of each you want them to hold is up to your game’s balance.

Now head back to InventoryPanel.tscn and add a Label as a child and set its Text to “1”.

This will be the indicator that lets us see how many items are in the stack at present. Click on the 👁 eye icon in the Scene dock to hide it. By default, when there are no items, it should not be visible.

The mouse needs an inventory slot, too, so let’s add that next.

Giving the mouse an inventory

Right now, when we move an object, we display a sprite that lives under the EntityPlacer. But since it’s a big part of inventory management, we can instead make it be part of the GUI and move that functionality out of EntityPlacer, separating concerns.

Back in GUI.tscn, add a new Control node as a child of GUI called DragPreview. Set its Mouse Filter to Ignore. This is doubly important as this control glues itself to the mouse at all times.

Right-click on DragPreview and select Save Branch as Scene and open its scene. Add a Label node as a child of DragPreview to display the number of items stacked in a slot.

Assign a new script, DragPreview.gd, to the DragPreview node so we can code the mouse’s inventory.

## A control that follows the mouse at all times to control the position of the
## blueprint sprite.
extends Control

## The blueprint object held by the drag preview. We use a setter function to
## ensure it's displayed on-screen.
var blueprint: BlueprintEntity setget _set_blueprint

## We need access to the label to display the stack count on-screen.
onready var count_label := $Label


func _ready() -> void:
    # The control will always stick to the mouse at all times. We don't want it
    # to be controlled by the GUI's CenterContainer. Otherwise, every frame, it
    # will be sent back to the middle of the screen.
    # Setting the node as `toplevel` makes its transform independent from its
    # parents.
    set_as_toplevel(true)


## Events in `_input()` happen regardless of the state of the GUI and they
## happen first so this callback is ideal for global events like matching the
## mouse position on the screen.
func _input(event: InputEvent) -> void:
    if event is InputEventMouseMotion:
        # If the mouse moved, we set the control's global position to the
        # mouse's position on the screen.
        rect_global_position = event.global_position


## A helper function to keep the label up-to-date with the stack count. We can
## call this whenever the stack's amount changes.
func update_label() -> void:
    # If we have a blueprint and there is more than 1 item in the stack, we set
    # the text to the amount and show the label.
    if blueprint and blueprint.stack_count > 1:
        count_label.text = str(blueprint.stack_count)
        count_label.show()
    # If there's only one item in the stack, we hide the label.
    else:
        count_label.hide()


## This function both removes the blueprint from the scene tree, but also frees
## it and calls `update_label()`.
func destroy_blueprint() -> void:
    if blueprint:
        remove_child(blueprint)
        blueprint.queue_free()
        blueprint = null
        update_label()


## Whenever we change the blueprint, we need to make sure it's in the scene tree
## to display it on the screen.
func _set_blueprint(value: BlueprintEntity) -> void:
    if blueprint and blueprint.get_parent() == self:
        # If we already are holding a blueprint and its parent is this control,
        # we remove it from the scene tree. The panel will take care of cleaning
        # it up if it needs it.
        remove_child(blueprint)

    # We set the new blueprint and if it's not null, we add it as a child of
    # this control so it is displayed.
    blueprint = value
    if blueprint:
        add_child(blueprint)
        move_child(blueprint, 0)

    # We make sure its label is up to date with its stack size.
    update_label()

That does introduce a minor problem.

If we want the inventory to interact with the mouse’s inventory, we need to pass DragPreview to the entity placer or somewhere else. Keeping track of all these oblique references and who should get what can get out of hand.

Instead, we can make one node responsible for forwarding calls, our GUI node. It already serves as a mediator between the interface and the game world. We can provide all the references and functions systems need on either side of the node so they only ever need to know about GUI and themselves.

Go back to GUI.tscn and add a GUI.gd script to the GUI node.

extends CenterContainer

## A reference to the inventory that belongs to the 'mouse'. It is a property
## that gives indirect access to DragPreview's blueprint through its getter
## function. No one needs to know that it is stored outside of the GUI class.
var blueprint: BlueprintEntity setget _set_blueprint, _get_blueprint

onready var player_inventory := $HBoxContainer/InventoryWindow
## We use the reference to the drag preview in the setter and getter functions.
onready var _drag_preview := $DragPreview


func _ready() -> void:
    # Here, we'll set up any GUI systems that require knowledge of the GUI node.
    # We'll define `InventoryWindow.setup()` in the next lesson.
    player_inventory.setup(self)


## Forwards the `destroy_blueprint()` call to the drag preview.
func destroy_blueprint() -> void:
    _drag_preview.destroy_blueprint()


## Forwards the `update_label()` call to the drag preview.
func update_label() -> void:
    _drag_preview.update_label()


## Setter that forwards setting the blueprint to `DragPreview.blueprint`.
func _set_blueprint(value: BlueprintEntity) -> void:
    if not is_inside_tree():
        yield(self, "ready")
    _drag_preview.blueprint = value


## Getter that returns the DragPreview's blueprint.
func _get_blueprint() -> BlueprintEntity:
    return _drag_preview.blueprint

Now, whenever we set GUI’s blueprint, we are actually setting DragPreview’s without the user needing to know that.

At the moment, the code above will cause an error because we haven’t defined InventoryWindow.setup(). We’ll do this in the next lesson.

Code reference

Here are the new scripts we wrote in this lesson.

DragPreview.gd

extends Control

var blueprint: BlueprintEntity setget _set_blueprint

onready var count_label := $Label


func _ready() -> void:
    set_as_toplevel(true)


func _input(event: InputEvent) -> void:
    if event is InputEventMouseMotion:
        rect_global_position = event.global_position


func update_label() -> void:
    if blueprint and blueprint.stack_count > 1:
        count_label.text = str(blueprint.stack_count)
        count_label.show()
    else:
        count_label.hide()


func destroy_blueprint() -> void:
    if blueprint:
        remove_child(blueprint)
        blueprint.queue_free()
        blueprint = null
        update_label()


func _set_blueprint(value: BlueprintEntity) -> void:
    if blueprint and blueprint.get_parent() == self:
        remove_child(blueprint)

    blueprint = value
    if blueprint:
        add_child(blueprint)
        move_child(blueprint, 0)

    update_label()

GUI.gd

extends CenterContainer

var blueprint: BlueprintEntity setget _set_blueprint, _get_blueprint

onready var player_inventory := $HBoxContainer/InventoryWindow
onready var _drag_preview := $DragPreview


func _ready() -> void:
    player_inventory.setup(self)


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