So we have an analog furnace. Stick some fuel in, smelt some ore, get some metal ingots back.
It’s good for starting out, but it’s a lot more efficient to burn fuel in some generators and have your furnaces use induction to get hot and melt metal.
Let’s make an electric version of the furnace. This will be my last task taught to you in this series. It uses all the systems we’ve built: power, GUI, and work.
I’m not going to spend much time on the parts we repeated the most in the series, except to go over the differences.
But at this point, I hope I’ve instilled in you the confidence (and the muscle-memory) needed to follow along on this final wild ride.
Please think of this as your test, my pupil.
We’ll start with the furnace’s item, the simplest part of the process.
Create a new scene, ElectricFurnaceBlueprint.tscn
with a BlueprintEntity
root, and add a Sprite
node with its region set to the electric furnace (a furnace with colored buttons).
One difference already is that you’re going to need an instance of the PowerDirection
arrows. The furnace only receives electricity, so it shouldn’t have output directions.
That’s the blueprint done.
Let’s go straight to the GUI, so we don’t have to return to the entity.
Once more, create a new scene, ElectricFurnaceGUI.tscn
, with a MarginContainer
root node.
Give it some margin and make it center horizontally in its parent using its Size Flags.
You should recognize most of this stuff. It’s like the furnace’s GUI, without a fuel input. We can save a lot of time by using “Merge from”.
Add a new HBoxContainer
as a child of ElectricFurnace
, right-click on it, and choose Merge From Scene. Find and select FurnaceGUI.tscn
, then select Input
, GUISprite
, Output
, and Tween
.
You’ll have to click and drag the Tween
out of the HBoxContainer
and into a direct child of ElectricFurnaceGUI
, but that’s a lot of time saved already.
Add a new script, ElectricFurnaceGUI.gd
, that extends BaseMachineGUI
to the ElectricFurnaceGUI
node. Then connect the InputInventoryBar
’s inventory_changed
signal to the script in the inspector.
Note: from Godot 3.2.4, Godot will insert the _on_InputInventoryBar_inventory_changed()
function in the script even if it’s defined in the parent FurnaceGUI
class. Be sure to remove the automatically generated function or it’ll prevent the crafting system from working.
Then we can head in and add the GUI’s functionality.
But before we do that, let’s think a little. What is the GUI going to do? The answer is “a lot of what the FurnaceGUI already does”.
Couldn’t we reuse most of that code? We can, but there are going to be some changes we need to make. So open up FurnaceGUI.gd
first.
We need a class name to identify it with, and we need to change how we initialize some of the variables.
Some nodes are in the same place (Tween
, OutputInventoryBar
, GUISprite
), but some are at a different node path (InputInventoryBar
) or are not even there (FuelInventoryBar
).
So we’ll split the initializing of those variables into a function that we can override for the electric furnace.
Below, the commented lines of code are the ones that didn’t change.
## A class name lets us easily extend it without having to `preload()` it class_name FurnaceGUI #extends BaseMachineGUI #var ore: BlueprintEntity #var fuel: BlueprintEntity #var output: Panel # We remove the node path and `onready` keyword for the following variables. var input_container: InventoryBar var fuel_container: InventoryBar var fuel_bar: ColorRect #onready var output_container := $HBoxContainer/Output #onready var tween := $Tween #onready var arrow := $HBoxContainer/GUISprite func _ready() -> void: #var scale: float = ProjectSettings.get_setting("game_gui/inventory_size") / arrow.region_rect.size.x #arrow.scale = Vector2(scale, scale) # The function takes care of getting the nodes we need. _find_nodes() func _find_nodes() -> void: fuel_container = $HBoxContainer/VBoxContainer/HBoxContainer/FuelInventoryBar input_container = $HBoxContainer/VBoxContainer/InputInventoryBar fuel_bar = $HBoxContainer/VBoxContainer/HBoxContainer/ColorRect
Now we can head back to ElectricFurnaceGUI.gd
and change what it extends to FurnaceGUI
, and update the functions that should be different to avoid executing any code related to fuel.
extends FurnaceGUI ## Like with the Stirling engine, when the system isn't being fed with enough ## electricity, we can slow it down with a speed parameter by affecting the ## tween's playback speed. func update_speed(speed: float) -> void: if not is_inside_tree(): return tween.playback_speed = speed func setup(gui: Control) -> void: input_container.setup(gui) output_panel_container.setup(gui) output_panel = output_panel_container.panels[0] func update_labels() -> void: input_container.update_labels() output_panel_container.update_labels() func _find_nodes() -> void: input_container = $HBoxContainer/InputInventoryBar
Nice and slim! The fuel variables still exist, but they remain empty and unused, and we won’t call the functions that have to do with fuel.
That leaves us just one thing to complete: the electric furnace. Create a new scene, ElectricFurnaceEntity.tscn
, with a StaticBody2D
root node. You know how this goes now. We did it earlier with the furnace.
We can save time, as we did with the GUI, using “Merge From”. Right-click on ElectricFurnaceEntity
and choose Merge From Scene, find FurnaceEntity.tscn
, and select everything from GUIComponent
down to AnimationPlayer
. Select the Sprite
and change the region to the electric furnace’s, and delete the LoadingVent
.
As this entity needs power, instance a PowerReceiver
component.
All that’s left is to replace the GUIComponent
’s Gui Window property with the ElectricFurnaceGUI.tscn
and add a PowerReceiver
node.
So, what does an electric furnace do? If you answered “the same thing as the furnace does but without fuel”, you’re right.
So we can do the same thing with the GUI: reuse the original furnace but leave the fuel parts unused and add power instead.
Head back to FurnaceEntity.gd
.
The main things we want to do are adding a class name and remove the work speed being set in _ready()
into its own space. That is because the electric furnace’s efficiency will depend on available current.
Everything else stays the same.
class_name FurnaceEntity #extends Entity func _ready() -> void: _set_initial_speed() func _set_initial_speed() -> void: work.work_speed = 1.0
Add a new script, ElectricFurnaceEntity.gd
, attached to the ElectricFurnaceEntity
and make it extend FurnaceEntity
.
Now, connect the following signals to ElectricFurnaceEntity
:
WorkComponent
to connect work_enabled_changed
, work_done
, and work_accomplished
.GUIComponent
, connect gui_status_changed
and gui_opened
.PowerReceiver
, connect received_power
.You will find that the only function it adds to the script is _on_PowerReceiver_received_power()
. That’s because FurnaceGUI.gd
, which we extend, already has all those functions.
We can override them if we need to. And, in fact, we do.
What needs to change?
We need to trick the FurnaceGUI
class into working even though we have no fuel or fuel slot.
Note we often write little hacks like these in games, where budgets can be tight and the job requires moving forward continuously.
The idea here is that we know we only have two kinds of furnaces so the time required to refactor and clean up that code wouldn’t necessarily be beneficial.
We can do that by saying that if we receive an update of power, then we temporarily set the fuel to 100%, setup work, then put the fuel back to 0
.
That way, if we don’t receive power next frame, fuel will be 0, and so it won’t do any further work.
extends FurnaceEntity onready var power := $PowerReceiver func _ready() -> void: # We always want the furnace to draw power, so it activates instantly when # it receives electricity and has a job to do. power.efficiency = 1.0 func _set_initial_speed() -> void: # Set the initial work speed to 0. We'll update it using power instead. work.work_speed = 0.0 func _consume_fuel(amount: float) -> void: # We have no fuel to consume, so we override the function to do nothing. pass func _on_PowerReceiver_received_power(amount: float, _delta: float) -> void: # We calculate the work speed based on the amount of power required. # # If we only get 50% of the power we need, we'll still work at 50% capacity. # # The power system never sends more than we need, so we don't have to clamp # it to 1. var new_work_speed: float = amount / power.power_required gui.gui.update_speed(new_work_speed) work.work_speed = new_work_speed # If we have a positive power flow, set the fuel to 100%. if amount > 0: available_fuel = 1.0 # Set up any work that needs doing (or is still ongoing) _setup_work() # Then reset the fake fuel to 0 so that, if power flow is cut off, we don't # keep smelting new ingots. available_fuel = 0 ## As we need to keep the speed up to date as well as update the craft's status, ## we override the signal callback to pass the speed in. func _on_GUIComponent_gui_opened() -> void: if work.is_enabled and work.work_speed > 0.0: gui.gui.work(work.current_recipe.time) gui.gui.update_speed(work.work_speed) gui.gui.seek(work.current_recipe.time - work.available_work)
All that’s left is not to forget to open the Groups panel for the ElectricFurnaceEntity
and assign it to the power_receivers
, gui_entities
, and workers
groups.
And that’s it. Electric smelting power is yours to command by connecting an electric furnace to a power network, generator, or battery.
All you have to do is to connect the furnace with a working generator using wires.
And with that final entity, we’ve done it. We’ve reached the end of the simulation entity.
We have a game where you can put down and pick items, craft new ones from those resources, smelt metal in a furnace, put down a power generator, connect it to some wires, and power an induction electric furnace.
If you’ve made it this far, all the hats off to you.
Go forth, my child, and wreak havoc with your newfound power.
Here are the scripts we created or modified in this chapter.
In FurnaceGUI.gd
, we added a class name and modified how we get three node references. To do so, we updated _ready()
and added a new overridable _find_nodes()
function.
class_name FurnaceGUI var input_container: InventoryBar var fuel_container: InventoryBar var fuel_bar: ColorRect func _ready() -> void: var scale: float = ( ProjectSettings.get_setting("game_gui/inventory_size") / arrow.region_rect.size.x ) arrow.scale = Vector2(scale, scale) _find_nodes() func _find_nodes() -> void: fuel_container = $HBoxContainer/VBoxContainer/HBoxContainer/FuelInventoryBar input_container = $HBoxContainer/VBoxContainer/InputInventoryBar fuel_bar = $HBoxContainer/VBoxContainer/HBoxContainer/ColorRect
ElectricFurnaceGUI.gd
extends FurnaceGUI func update_speed(speed: float) -> void: if not is_inside_tree(): return tween.playback_speed = speed func setup(gui: Control) -> void: input_container.setup(gui) output_panel_container.setup(gui) output_panel = output_panel_container.panels[0] func update_labels() -> void: input_container.update_labels() output_panel_container.update_labels() func _find_nodes() -> void: input_container = $HBoxContainer/InputInventoryBar
In FurnaceEntity.gd
, we added a class name and rewrote the _ready()
callback to call a new _set_initial_speed()
function.
class_name FurnaceEntity func _ready() -> void: _set_initial_speed() func _set_initial_speed() -> void: work.work_speed = 1.0
ElectricFurnaceEntity.gd
extends FurnaceEntity onready var power := $PowerReceiver func _ready() -> void: power.efficiency = 1.0 func _set_initial_speed() -> void: work.work_speed = 0.0 func _consume_fuel(amount: float) -> void: pass func _on_PowerReceiver_received_power(amount: float, _delta: float) -> void: var new_work_speed: float = amount / power.power_required gui.gui.update_speed(new_work_speed) work.work_speed = new_work_speed if amount > 0: available_fuel = 1.0 _setup_work() available_fuel = 0 func _on_GUIComponent_gui_opened() -> void: if work.is_enabled and work.work_speed > 0.0: gui.gui.work(work.current_recipe.time) gui.gui.update_speed(work.work_speed) gui.gui.seek(work.current_recipe.time - work.available_work)