We have recipes, we can detect that we have the required materials, and we can display those recipes to the user.
The next thing to do is to craft the items when the user clicks on a recipe. Doing so should consume the prerequisite materials.
CraftingGUI
happens to have a reference to the GUI
, which gives it access to the inventory through it. We can emit signals from the CraftingRecipeItem
nodes and bubble them back up to GUI
when the player clicks on them.
We can detect a mouse click on a user interface using _gui_input()
.
Head to CraftingRecipeItem.gd
to add both a new signal and input code.
signal recipe_activated(recipe, output) func _gui_input(event: InputEvent) -> void: if event.is_action_pressed("left_click"): var recipe_filename: String = recipe_name.recipe_name var recipe: Dictionary = ReciPes.Crafting[recipe_filename] emit_signal("recipe_activated", recipe, recipe_filename)
We can’t use the Inspector to connect this new signal since we instance recipe panels at runtime. So we have to connect via code, when we initialize the CraftingGUI
.
Head to CraftingGUI.gd
’s update_recipes()
function, and right after we call setup()
, we connect to the recipe_activated
signal.
func update_recipes() -> void: #... # for output in Recipes.Crafting.keys(): #... # item.setup( # Library.get_entity_name_from(temp), # sprite.texture, # sprite.region_enabled, # sprite.region_rect # ) item.connect("recipe_activated", self, "_on_recipe_activated") #temp.free()
What should happen when we craft an item?
We need to find the panels that hold the items we want to consume, reduce them by the recipe’s amount, create a new item, and put it in the player’s inventory.
## Crafts the output item by consuming the recipe's inputs from the player ## inventory. func _on_recipe_activated(recipe: Dictionary, output: String) -> void: # We loop over every input and find inventory panels containing this item. for input in recipe.inputs.keys(): var panels: Array = gui.find_panels_with(input) var count: int = recipe.inputs[input] # We then loop over the panels and update their count. for panel in panels: # If there is enough in the stack, we reduce it by the required # amount. if panel.held_item.stack_count >= count: panel.held_item.stack_count -= count # Since we had enough items, make count 0 count = 0 # If there isn't enough, we reduce the required count by how many there # are, then set the stack to 0. else: count -= panel.held_item.stack_count panel.held_item.stack_count = 0 # If the stack is now a size of 0, we delete it. if panel.held_item.stack_count == 0: panel.held_item.queue_free() panel.held_item = null # And we update the count label up to date if it hasn't been # deleted. panel._update_label() # If count is now 0, then we no longer need to check any other panel if count == 0: break # Now that we've consumed all items, we can use the library to instance a # new blueprint for the new item, and add it to the player inventory. var item: BlueprintEntity = Library.blueprints[output].instance() item.stack_count = recipe.amount gui.add_to_inventory(item)
If you run the game now and craft a set of wires, you should see the ingot count go down by two, and a pack of five wires gets added to your inventory.
But right now, there’s a bug: you can keep on crafting wires until you close and re-open the inventory, even if you no longer have enough ingots.
This is because we are not updating the recipe list when the inventory changes.
If we pick up an item or get rid of some items while the inventory is open, we should update it.
Head to GUI.tscn
and connect to InventoryWindow
’s inventory_changed
signal to the GUI node.
Also, connect the QuickBar
’s inventory_changed
signal to the same _on_InventoryWindow_inventory_changed()
function.
That way, all inventories and quick bars will report when they change.
In GUI.gd
, the callback function should call CraftingGUI.update_recipes()
.
func _on_InventoryWindow_inventory_changed(panel, held_item) -> void: crafting_window.update_recipes()
Now, once you run out of materials, the recipe should disappear.
In fact, if you start the game with 10 ingots and craft one bundle of wires, you should see the Stirling Engine appear in the recipe list.
There’s one last bug we can squash: when you craft all the wires you can, the tooltip stays visible, even when the recipe disappears.
To fix it, we need to emit the hovered_over_entity
signal at the end of CraftingGUI.update_recipes()
.
func update_recipes() -> void: #... #item.connect("recipe_activated", self, "_on_recipe_activated") #temp.free() Events.emit_signal("hovered_over_entity", null)
The tooltip tells the player they’re hovering over a recipe, but err on the side of “it’s better than nothing”.
Let’s improve the visual feedback by changing the CraftingRecipeItem
panel’s color on hover, like a button.
We can use the custom style properties to change how a GUI item behaves without changing the theme itself. Custom styles only affect this node, unlike the theme resource.
In this project, panel background styles rely on StyleBoxFlat
resources.
We can add a couple of those as exported variables and slot them in as needed.
We’ll add two: one for the regular color when the mouse isn’t over the recipe, and another to use on hover.
Head to CraftingRecipeItem.gd
and let’s add some custom panels.
export var regular_style: StyleBoxFlat export var highlight_style: StyleBoxFlat
To apply a custom style from code, we need to use the set()
function, because custom styles aren’t accessible as regular variables.
Instead, they’re part of Godot’s theme API.
To update the styles, we need to add code to existing functions.
## We store the property path for the custom panel in a constant for easy ## reference. const CUSTOM_PANEL_PROPERTY := "custom_styles/panel" # If we set styles in the inspector, we should use it as soon as the item is # instanced. # We define the `_ready()` function to do that. func _ready() -> void: if regular_style: set(CUSTOM_PANEL_PROPERTY, regular_style) func _on_CraftingRecipe_mouse_entered() -> void: #... set(CUSTOM_PANEL_PROPERTY, highlight_style) # And we update the styles by adding code to our existing callback functions. func _on_CraftingRecipe_mouse_exited() -> void: #... set(CUSTOM_PANEL_PROPERTY, regular_style)
We now need to add two styleboxes to the CraftingRecipeItem
.
Open CraftingRecipeItem.tscn
and assign our existing inventory_panel_style.tres
and inventory_panel_light.tres
styleboxes to the node’s Regular Style and Highlight Style property, respectively.
If you hover the recipes, they should now light up, making it clearer you can interact with them.
If you remember, we added a custom setting in our project settings to change the panels’ size based on inventory items’ size.
But if you tweak the value now, it won’t change the size. We can fix it, though.
While we’re there, we can also force the crafting window’s size to be a minimum width.
Open up CraftingRecipeItem.gd
to add that functionality, in the _ready()
function.
func _ready() -> void: #... var gui_size: float = ProjectSettings.get_setting("game_gui/inventory_size") # Blueprint sprites are 100 pixels in size. We calcualte the desired scale # modifier by dividing the provided size by 100. var scale := gui_size / 100.0 # Then we can scale the sprite's size and the minimum size of the crafting # window based on a constant 300 pixels, our desired base width. sprite.scale *= Vector2(scale, scale) rect_min_size = Vector2(300, 0) * scale
If you’re going to make your GUI larger, you may want to go into Shared/Theme/Font
in the FileSystem dock and update the font resource’s Size property to be bigger too.
There’s a lot of back and forth to keep the code clean and sequestered, but the result is a simple crafting system that works with the inventory system we already coded.
We’ll add some more items we can craft in the following lessons: tools to chop trees and mine boulders with.
Here are the functions and properties we added or modified in this lesson.
CraftingRecipeItem.gd
signal recipe_activated(recipe, output) const CUSTOM_PANEL_PROPERTY := "custom_styles/panel" export var regular_style: StyleBoxFlat export var highlight_style: StyleBoxFlat func _ready() -> void: if regular_style: set(CUSTOM_PANEL_PROPERTY, regular_style) var gui_size: float = ProjectSettings.get_setting("game_gui/inventory_size") var scale := gui_size / 100.0 sprite.scale *= Vector2(scale, scale) rect_min_size = Vector2(300, 0) * scale func _gui_input(event: InputEvent) -> void: if event.is_action_pressed("left_click"): var recipe_filename: String = recipe_name.recipe_name var recipe: Dictionary = Recipes.Crafting[recipe_filename] emit_signal("recipe_activated", recipe, recipe_filename) func _on_CraftingRecipeItem_mouse_entered() -> void: var recipe_filename: String = recipe_name.recipe_name Events.emit_signal("hovered_over_recipe", recipe_filename, Recipes.Crafting[recipe_filename]) set(CUSTOM_PANEL_PROPERTY, highlight_style) func _on_CraftingRecipeItem_mouse_exited() -> void: Events.emit_signal("hovered_over_entity", null) set(CUSTOM_PANEL_PROPERTY, regular_style)
We connected the inventory and quick bar’s inventory_changed
signal to a new callback function in GUI.gd
.
func _on_InventoryWindow_inventory_changed(panel, held_item) -> void: crafting_window.update_recipes()
In CraftingGUI.gd
, we modified update_recipes()
and added _on_recipe_activated()
func update_recipes() -> void: for child in items.get_children(): child.queue_free() for output in Recipes.Crafting.keys(): var recipe: Dictionary = Recipes.Crafting[output] var can_craft := true for input in recipe.inputs.keys(): if not gui.is_in_inventory(input, recipe.inputs[input]): can_craft = false break if not can_craft: continue var temp: BlueprintEntity = Library.blueprints[output].instance() var item := CraftingItem.instance() items.add_child(item) var sprite: Sprite = temp.get_node("Sprite") item.setup( Library.get_entity_name_from(temp), sprite.texture, sprite.region_enabled, sprite.region_rect ) item.connect("recipe_activated", self, "_on_recipe_activated") temp.free() Events.emit_signal("hovered_over_entity", null) func _on_recipe_activated(recipe: Dictionary, output: String) -> void: for input in recipe.inputs.keys(): var panels: Array = gui.find_panels_with(input) var count: int = recipe.inputs[input] for panel in panels: if panel.held_item.stack_count >= count: panel.held_item.stack_count -= count count = 0 else: count -= panel.held_item.stack_count panel.held_item.stack_count = 0 if panel.held_item.stack_count == 0: panel.held_item.queue_free() panel.held_item = null panel._update_label() if count == 0: break var item: BlueprintEntity = Library.blueprints[output].instance() item.stack_count = recipe.amount gui.add_to_inventory(item)