Adding entities to pick up

We have a grid-based inventory, and we can grab items from it and pick up deconstructed items. It works, and so now, we can clean up the code and enhance the world to make testing faster.

First, it’d be nice to have some entities in the world we can pick up already without putting it down first. As the next chapter will cover crafting, let’s add some sticks and rocks to build with.

The second thing is to replace the hard-coded debug code we have in InventoryWindow.gd. We could code something that’s built in the inspector instead.

Twigs and rocks

To illustrate the crafting system in the next chapter, we’ll allow crafting basic tools with wood and rocks.

Let’s add a couple of new entities to enable that. The player can’t place them on the game grid, but they’ll spawn as part of the world instead.

Create a new scene, BranchesEntity.tscn. We’ve made previous entities static bodies because the player couldn’t walk over them, but these are sticks and branches so we don’t need to collide with them. Make it a Node2D instead, and name the node BranchesEntity.

Add a Sprite as a child and assign it one of the branches’ regions using the tileset.svg graphics, with the bottom located at the origin, about -25 pixels on the Y-axis.

We have four regions, though. Wouldn’t it be nice if they were a little random and pick from each one? It sure would.

Add a script to BranchesEntity and make it extend Entity.

extends Entity

# I've pre-written the region coordinates for you.
# They correspond to the four small branch sprites.
const REGIONS := [
    Rect2(135, 450, 24, 42),
    Rect2(177, 450, 41, 42),
    Rect2(125, 505, 39, 45),
    Rect2(180, 498, 38, 52)
]


func _ready() -> void:
    # We set the sprite as one of the four available regions at random.
    $Sprite.region_rect = REGIONS[int(rand_range(0, REGIONS.size() - 1))]

We need to create the corresponding blueprint.

Make a new scene, BranchesBlueprint.tscn, as a BlueprintEntity. Add a Sprite child, assign it the blueprints.svg graphics, and set its region to the branches icon. Set its Y position to -25 pixels so it matches the other blueprint entities.

Assign the BlueprintEntity.gd script to the root node and configure it. I set its Stack size to 100 and disabled Placeable in the Inspector. With this setting, the player can’t put branches down on the game grid.

Go back to Simulation.tscn and instantiate some BranchesEntities.tscn scenes as a child of the EntityPlacer node. You can place them anywhere on the grid.

If you remember, the EntityPlacer has code to take any entity that is already one of its children and keep track of them. You should be able to run the game and deconstruct the branches without doing anything else.

You can repeat the process to create a StoneEntity.tscn and StoneBlueprint.tscn scene. The process is the same, but there are more texture regions for stones.

In StoneEntity.tscn, be sure to position the sprite vertically so the stone’s center of gravity aligns with the scene’s origin.

Here’s the StoneEntity.gd script, which you should attach to the StoneEntity node.

extends Entity

const REGIONS := [
    Rect2(12, 452, 38, 24),
    Rect2(52, 451, 25, 23),
    Rect2(79, 451, 30, 22),
    Rect2(12, 489, 24, 28),
    Rect2(38, 498, 42, 19),
    Rect2(83, 485, 26, 34),
    Rect2(12, 524, 27, 25),
    Rect2(45, 525, 31, 23),
    Rect2(87, 526, 21, 21)
]


func _ready() -> void:
    $Sprite.region_rect = REGIONS[int(rand_range(0, REGIONS.size() - 1))]

For the stone blueprint, remember to make it non-placeable.

Provided you created both the entity and the blueprint, in Simulation.tscn, you can instantiate some StoneEntity as a child of EntityPlacer and deconstruct them like the branches.

Proper inventory dictionary

We’ll wrap up this chapter by doing some cleanup and adding some debug code to pre-fill your inventory with entities of your choice. To do so, we’ll export a dictionary on the GUI node and create items from its keys and values.

Doing so will simplify testing during development.

We had some temporary code in InventoryWindow.setup() to add engines and batteries to the inventory. Head over there and remove the old lines. You should be left with the following setup() function.

func setup(_gui: Control) -> void:
    gui = _gui
    for bar in inventories:
        bar.setup(gui)

Now, head over to GUI.gd, and let’s add a way to convert a dictionary into an inventory.

## Prefills the player inventory with objects from this dictionary
export var debug_items := {}

func _ready() -> void:
    #...

    # ----- Debug system -----
    # We loop over all the keys in the `debug_items` dictionary and ensure they exist in the `Library`.
    for item in debug_items.keys():
        if not Library.blueprints.has(item):
            continue

        # If so, we instantiate the item and set its stack count to the value dictionary's value.
        var item_instance: Node = Library.blueprints[item].instance()
        item_instance.stack_count = min(item_instance.stack_size, debug_items[item])
        
        # We then try to add it to the inventory and if it's full, we free the leftover blueprint.
        if not add_to_inventory(item_instance):
            item_instance.queue_free()

Now you can use item names without the “Entity” and “Blueprint” suffixes to add them to the inventory.

For example, you can add the keys “Battery” and “StirlingEngine” to the GUI node’s Debug Items property, with an integer as the associated value.

You now no longer need to find and edit code to get any entity you create into the inventory, so long as it has a blueprint.

That brings this chapter to a close. We’ve built a file-analyzing library, a grid-based inventory, and can pick up, drop and deconstruct entities from it.

The next and final chapter will be all about crafting from the inventory and making machines do some production work.

Code reference

Here are all the scripts we created or modified in this lesson.

BranchesEntity.gd

extends Entity

const REGIONS := [
    Rect2(135, 450, 24, 42),
    Rect2(177, 450, 41, 42),
    Rect2(125, 505, 39, 45),
    Rect2(180, 498, 38, 52)
]


func _ready() -> void:
    $Sprite.region_rect = REGIONS[int(rand_range(0, REGIONS.size() - 1))]

StoneEntity.gd

extends Entity

const REGIONS := [
    Rect2(12, 452, 38, 24),
    Rect2(52, 451, 25, 23),
    Rect2(79, 451, 30, 22),
    Rect2(12, 489, 24, 28),
    Rect2(38, 498, 42, 19),
    Rect2(83, 485, 26, 34),
    Rect2(12, 524, 27, 25),
    Rect2(45, 525, 31, 23),
    Rect2(87, 526, 21, 21)
]


func _ready() -> void:
    $Sprite.region_rect = REGIONS[int(rand_range(0, REGIONS.size() - 1))]

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)


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"
]

export var debug_items := {}

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")

    for item in debug_items.keys():
        if not Library.blueprints.has(item):
            continue

        var item_instance: Node = Library.blueprints[item].instance()
        item_instance.stack_count = min(item_instance.stack_size, debug_items[item])
        
        if not add_to_inventory(item_instance):
            item_instance.queue_free()


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)