Designing the Stirling engine’s GUI

In this lesson, we’ll design the Stirling engine’s little interface.

It’ll be an inventory slot that only accepts fuel with a bar that empties as the consumed piece of fuel burns.

Like the chest, create a new scene, StirlingEngineGUI.tscn, with a MarginContainer named StirlingEngineGUI as its root node.

Like we did with the chest, change its Size Flags -> Horizontal mode from Fill to Shrink Center so the GUI ends up centered horizontally in the inventory window.

Also, add some margins using the Custom Constants.

Add an HBoxContainer to line up the fuel bar and the fuel slot side by side.

For the bar, add a ColorRect node. We could use a ProgressBar, but it’s more complex than what we need it for.

Leave it white: we’ll use some shader magic to color it.

Set its Rect -> Min Size’s x component to 5 pixels (more if you want a thicker bar), and turn on the Expand Size Flags -> Vertical so it stretches to fill the vertical space created by the inventory slot.

Speaking of, instance a new InventoryBar node under the HBoxContainer.

Change its Slot Count to 1, and in the Item Filters string, type in “Fuels”.

Coloring and animating the bar

Select the ColorRect again, and down in the Material section, add a new ShaderMaterial resource. And in that resource, in the Shader property, add a new Shader.

If you’d like to learn more about shaders, we do offer a https://gdquest.mavenseed.com/courses/godot-shader-secrets that explains all of these juicy shader details. But for now, you can use the shader below as is with helpful commentary to understand what we’re doing.

// As with all 2D shaders, we must define a shader type of `canvas_item`.
shader_type canvas_item;

// This exported value ranges from 0 to 1 and represent how full the bar should be.
uniform float fill_amount : hint_range(0, 1.0) = 0.0;

// The `fragment()` function gets called once for each fragment (pixel or 
// part of a pixel) drawn by the shader.
void fragment() {
    // Get the node's `modulate` color as the base for the bar.
  vec4 color = MODULATE;
    
    // Use the step() function to calculate how much of the sprite's UV
  // map should be filled.
  // If `1.0 - fill_amount` is greater than the current vertical UV value,
  // then the function returns 1.0. Otherwise, it returns 0.0.
  float pixel_fill = step(1.0 - fill_amount, UV.y);
    
    // Set the output fragment's color to the modulate color, and double the 
  // color where `pixel_fill` is 1. We clamp it between 0 and 1 so it sits 
    // in the standard RGB color format instead of HDR colors.
  COLOR = clamp(color + (color * pixel_fill), 0, 1);
}

Next, set the ColorRect’s Modulate color to a nice blue tone. I went with #3076ff.

You can tweak the Material -> Shader -> Shader Param -> Fill Amount to see the bar fill up.

Now, when we want to change how full the bar is, we can call ShaderMaterial.set_shader_param() from a script and provide fill_amount with a value between 0 and 1.

We add a new script, to the StirlingEngineGUI node.

Its main job is to notify the GUIComponent when we add or remove fuel to the slot, and to control the fuel bar’s shader.

extends BaseMachineGUI

## A reference to the fuel inside of the inventory bar, instead of having to
## access the inventory bar and get one of its held items. This makes it easier
## for the entity to access.
var fuel: BlueprintEntity

onready var fuel_container := $HBoxContainer/InventoryBar
onready var fuel_bar := $HBoxContainer/ColorRect


## Sets the shader to the provided amount to fill or empty the bar.
func set_fuel(amount: float) -> void:
    if fuel_bar:
        fuel_bar.material.set_shader_param("fill_amount", amount)


## Sets up the inventory bar that holds the fuel.
func setup(gui: Control) -> void:
    fuel_container.setup(gui)


## Ensures that the inventory bar is up to date when we consume a piece of fuel.
## The number on the stack should go down.
func update_labels() -> void:
    fuel_container.update_labels()


## When the inventory bar changes, we can bubble up the signal and warn the
## entity.
func _on_InventoryBar_inventory_changed(panel, held_item) -> void:
    # Store the item in the fuel variable so the entity can access it.
    fuel = held_item
    emit_signal("gui_status_changed")

Select the InventoryBar in the scene tree, and make sure to connect its “inventory_changed” signal to the _on_InventoryBar_inventory_changed() function on StirlingEngineGUI.

Your scene dock should look like this.

Assigning the GUI to the engine

We can wrap this up with adding the GUI to the Stirling engine entity.

Head to StirlingEngineEntity.tscn, add a new GUIComponent node, and assign the StirlingEngineGUI.tscn scene to its Gui Window property.

Finally, select the StirlingEngineEntity node and make sure to add it to the “gui_entities” node group.

If you place down a Stirling engine in the Simulation scene and click it in-game, you should see its GUI appear.

It should also refuse to let you put anything but branches or lumber inside!

Right now, it doesn’t do anything with it.

In the next lesson, we’ll trigger the burning of fuel and make the engine only work when it has fuel.

Code reference

StirlingEngineEntityGUI

extends BaseMachineGUI

var fuel: BlueprintEntity

onready var fuel_container := $HBoxContainer/InventoryBar
onready var fuel_bar := $HBoxContainer/ColorRect


func set_fuel(amount: float) -> void:
    if fuel_bar:
        fuel_bar.material.set_shader_param("fill_amount", amount)


func setup(gui: Control) -> void:
    fuel_container.setup(gui)


func update_labels() -> void:
    fuel_container.update_labels()


func _on_InventoryBar_inventory_changed(panel, held_item) -> void:
    fuel = held_item
    emit_signal("gui_status_changed")