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