Smelting with an electric furnace

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.

The electric furnace blueprint

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.

The electric furnace GUI

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.

The electric furnace entity

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.

The electric furnace script

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:

  1. Go into WorkComponent to connect work_enabled_changed, work_done, and work_accomplished.
  2. For GUIComponent, connect gui_status_changed and gui_opened.
  3. And on 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.

Conclusion

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.

Code reference

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)