The chest entity is a coffer you place and, when opened, lets you use extra inventory slots. It’s a universal feature in most games with limited storage.
In this lesson, we’ll create both its interface, a small inventory grid, and the corresponding entity.
Let’s start with the interface.
Create a new scene, ChestGUI.tscn
, with a MarginContainer
named ChestGUI as its root node. Looking at the screenshot above, we have three rows of five inventory panels. We’ll instance the InventoryBar.tscn
scene for that.
Add a VBoxContainer
as a child, and then instance InventoryBar.tscn
three times to make the three rows.
You can already save the scene in the Entities/
directory and attach a new script to ChestGUI.
Set every inventory bar’s Slot Count property to 5
. That’ll give the player 15 slots per chest.
While we’re in there, we can set some Control
node parameters to force the also make sure that it centers horizontally in the parent inventory window, where we’ll attach it.
By default, it’ll anchor to the left.
Head into ChestGUI’s Size Flags property and replace Horizontal’s Fill checkbox with Shrink Center. This property is one way to make a Control
node center inside its parent container.
Since our ChestGUI’s a MarginContainer
, we can also add some padding around its edges with the Custom Constants to make it look nice.
We’ll now add a script to ChestGUI
. It extends BaseMachineGUI
and overrides the setup()
function, where it initializes its three inventory bars.
extends BaseMachineGUI func setup(_gui: Control) -> void: for inventory in $VBoxContainer.get_children(): inventory.setup(_gui)
To craft and hold chests in the player’s inventory, we need to create a blueprint for it.
Create a new scene, ChestBlueprint.tscn
, with a BlueprintEntity
node named ChestBlueprint as its root and a Sprite
as its child.
As we’ve done in previous blueprints, we want to set its Texture property to blueprints.svg
, enable Regions Enabled, and use the TextureRegions toolbar to pick out the chest.
I configured the ChestBlueprint to only have stack of two at most.
We have the chest’s blueprint form, and we now need its entity to place it in the game grid.
We build the chest entity as a box and a lid, with collision data and, of course, a GUI.
When the user clicks on it, the lid swings open, and the inventory window opens.
When they close the inventory window, the lid shuts.
As with previous entities with collision, create a new scene, ChestEntity.tscn
, with a StaticBody2D
as its root node. Add two Sprite
s: one for the Box, and one for the Lid.
Set both of them to use tilset.svg
, enable their Region data, and set them to the box and lid sprites from the sheet, respectively.
Ensure the box is raised so that its floor is at the origin and put the lid on top.
Here are the properties for the Box sprite first.
And here are the Lid’s properties.
You’ll notice there are many lid sprites in the texture atlas. They’re here to animate the lid opening and closing.
Animating something rotating in isometric perspective can be time-consuming as you have to draw every frame to give the illusion of 3D movement.
Godot provides two tools to design the animation: the AnimatedSprite
and the AnimationPlayer
.
I went with a series of sprites with an AnimationPlayer
to control them. I have two animations, “Open” and “Close”, and over the course of 0.35
seconds, scroll through each of the sprite lids I drew using the Region Rect property.
If your sprites are in separate images, Godot allows you to animate the Texture property, but as I packed them all in one image, I had to go with Region Rect.
Because the lid’s rotation axis is not in the sprites’ center, I also had to animate the lid’s position whenever it changes sprites until it looked good enough.
To do so, for every keyframe, I lined up the inner corner of the lid with the upper corner of the box.
The last thing to keep in mind is that since we’re working with discrete sprites, we should be setting both the Region rect and Position keyframes to Discrete. Otherwise, we would get… shall we say “interesting” results.
The “Close” animation is a duplicate of the “Open” one but flipped. In the Animation panel, you select all keyframes and use Edit -> Scale Selection with a Scale Ratio of -1
to flip an animation.
As with any static body, we need to add collision. And like previous entities, we add a CollisionPolygon2D
to do it here.
We add a GUIComponent
node and set its Gui Window property to the ChestGUI.tscn
scene.
And because our scene has got a GUIComponent
, we should also assign ChestEntity the gui_entities
group in the Node dock.
I’ve reordered the nodes to put the GUIComponent
at the top, and here’s what your scene should contain.
Assign a new script, ChestEntity.gd
, to the root entity. This is where we can trigger the opening and closing animations.
extends Entity onready var animation := $AnimationPlayer func _on_GUIComponent_gui_opened() -> void: animation.play("Open") func _on_GUIComponent_gui_closed() -> void: animation.play("Close")
To trigger those animations, we need to connect the signals to the appropriate functions.
Head to the inspector and connect the gui_opened
and gui_closed
signals from GUIComponent
to the corresponding functions in ChestEntity.gd
.
And that’s it.
If you put a chest as a child of EntityPlacer
in Simulation.tscn
, you should be able to approach it, click on it, put items in it, and close it by leaving the inventory.
If you run the game, everything works, but you may notice that the Debugger is raising some red errors about held_item_changed
being already connected.
It doesn’t break the game, but it’s usually a good idea to fix red stuff and keep it from causing more trouble down the line!
The issue is we don’t know when, or even if, the player will open the machine’s GUI, so we call setup()
even if we’ve already set the panel up before.
Doing so triggers a new call to connect()
every time for inventory bars.
We can head to InventoryBar.gd
then and add a boolean that checks if we’ve already run the setup()
function. If that’s the case, we skip doing it again.
var setup := false func setup(gui: Control) -> void: if setup: return setup = true #...
And now it’s fixed. Clean!
At the moment, if you do a lot of work and put all your stuff in a chest, and then deconstruct the chest… all the entities within are gone.
That’s not great.
Instead, a machine or entity with an inventory should drop its contents when deconstructing it.
Before we can drop an inventory, we need to grab the inventory from entities.
This trip begins in InventoryBar.gd
, where we want to get every panel from the bar’s slots into a digestible list.
## Returns the combined inventory of all inventory panels. func get_inventory() -> Array: var output := [] for panel in panels: if panel.held_item: output.push_back(panel.held_item) return output
After that, we head to GUIComponent.gd
to create a helper function to collate a gui window’s inventory bars into a list.
# Returns an array of InventoryBar instances in the node's interface. func get_inventory_bars() -> Array: var output := [] var parent_stack := [gui] # We loop recursively over the interface and its children, exploring its # nodes to find all inventory bars. while not parent_stack.empty(): var current: Node = parent_stack.pop_back() if current is InventoryBar: output.push_back(current) # Here's what keeps the loop going: we add the current node's children # to the stack until its empties. parent_stack += current.get_children() return output
With these helper functions created, we can head to EntityPlacer.gd
and edit _finish_deconstruct()
and _drop_entities()
to drop the contents of a GUIComponent
.
func _finish_deconstruct(cellv: Vector2) -> void: # ... # We check if the entity has a GUI component and look for its inventories. if entity.is_in_group(Types.GUI_ENTITIES): var inventories: Array = _gui.find_inventory_bars_in(_gui.get_gui_component_from(entity)) # We then loop over all inventories to find all the items they contain. var inventory_items := [] for inventory in inventories: inventory_items += inventory.get_inventory() # And we drop for item in inventory_items: _drop_entity(item, location) #_tracker.remove_entity(cellv) #_update_neighboring_flat_entities(cellv) #_gui.deconstruct_bar.hide() func _drop_entity(entity: BlueprintEntity, location: Vector2) -> void: # We instantiate dropped items as a child `GroundEntity`, but if they're # part of an inventory, they won't be able to be moved by the ground entity. # So we unparent them first if need be. if entity.get_parent(): entity.get_parent().remove_child(entity) #var ground_entity := GroundEntityScene.instance() #...
There’s a new function on the GUI
class we need to implement: find_inventory_bars_in()
. This function will look through every node of a GUIComponent
and return a list of inventory bars. This lets us grab all the inventory so we can drop it.
## Recursively searches the given component's GUI scene for inventory bars. func find_inventory_bars_in(component: GUIComponent) -> Array: var output := [] # Keep a stack of nodes. We will keep popping the back element from it, and add # all children we find to get _their_ children, so on and so forth. var parent_stack := [component.gui] # Keep searching children until there are no more nodes to search. while not parent_stack.empty(): var current: Node = parent_stack.pop_back() if current is InventoryBar: output.push_back(current) parent_stack += current.get_children() return output
And with that, no more losing items to the aether.
ChestGUI.gd
extends BaseMachineGUI func setup(_gui: Control) -> void: for inventory in $VBoxContainer.get_children(): inventory.setup(_gui)
ChestEntity.gd
extends Entity onready var animation := $AnimationPlayer func _on_GUIComponent_gui_opened() -> void: animation.play("Open") func _on_GUIComponent_gui_closed() -> void: animation.play("Close")
In InventoryBar
, we edited setup()
and added a new get_inventory()
function.
var setup := false func setup(gui: Control) -> void: if setup: return setup = true for panel in panels: panel.setup(gui) panel.connect("held_item_changed", self, "_on_Panel_held_item_changed") func get_inventory() -> Array: var output := [] for panel in panels: if panel.held_item: output.push_back(panel.held_item) return output
In GuiComponent.gd
, we added a function named get_inventory_bars()
.
func get_inventory_bars() -> Array: var output := [] var parent_stack := [gui] while not parent_stack.empty(): var current: Node = parent_stack.pop_back() if current is InventoryBar: output.push_back(current) parent_stack += current.get_children() return output
And in EntityPlacer.gd
, we modified _finish_deconstruct()
and _drop_entity()
func _finish_deconstruct(cellv: Vector2) -> void: var entity := _tracker.get_entity_at(cellv) var entity_name := Library.get_entity_name_from(entity) var location := map_to_world(cellv) if Library.blueprints.has(entity_name): var Blueprint: PackedScene = Library.blueprints[entity_name] for _i in entity.pickup_count: _drop_entity(Blueprint.instance(), location) if entity.is_in_group(Types.GUI_ENTITIES): var inventories: Array = _gui.find_inventory_bars_in(_gui.get_gui_component_from(entity)) var inventory_items := [] for inventory in inventories: inventory_items += inventory.get_inventory() for item in inventory_items: _drop_entity(item, location) _tracker.remove_entity(cellv) _update_neighboring_flat_entities(cellv) _gui.deconstruct_bar.hide() func _drop_entity(entity: BlueprintEntity, location: Vector2) -> void: if entity.get_parent(): entity.get_parent().remove_child(entity) var ground_entity := GroundEntityScene.instance() add_child(ground_entity) ground_entity.setup(entity, location)
And in GUI.gd
, we added find_inventory_bars_in()
.
func find_inventory_bars_in(component: GUIComponent) -> Array: var output := [] var parent_stack := [component.gui] while not parent_stack.empty(): var current: Node = parent_stack.pop_back() if current is InventoryBar: output.push_back(current) parent_stack += current.get_children() return output