The Power System

With the wires in place, we have all the entities we need to take energy from a power source (our Stirling engine) to another machine.

Before creating the power receiver and its power storing logic, we’ll tackle transmitting and moving power along the wires.

The Entity-Component-System

We’ll do this by implementing a variant of the Entity-Component-System pattern (ECS). In this pattern, entities have an array of specialized clumps of data, named components, through which they interact with systems.

For example, by the end of this lesson, we’ll have a PowerSource (component) we’ll attach to engines and batteries (entities) that a PowerSystem (system) will detect and use to transfer the energy to a receiver.

A frequent question new Godot users ask is how to implement an ECS in the engine. The pattern’s been popularized by Unity, which uses it at its core.

Godot’s scene and node system are not built around the same idea so you won’t benefit from the ECS pattern in most games. We recommend forgetting about popular architectural patterns when working in Godot, like Model-View-Controller (MVC) or ECS, and thinking in terms of scenes and nodes.

There are some specific types of games that will benefit from the ECS pattern, however. Simulation games can be one of those.

In our case, we have independent and naive entities that do not know of one another. Yet, we need a way to connect and signal that something happened to them. For example, that a machine is receiving electricity.

ECS is a useful pattern in this exact situation. We use it to complement Godot’s scene system rather than replace it.

We won’t code a pure ECS, but rather a variant to keep our entities unaware of one another and their code decoupled.

Power Components

Let’s start building our ECS with the components. They are clumps of data that we can assign to an entity to configure it. In our case, we’ll make them Nodes that the system will collect and store as references. The Stirling engine is a power source, electric machines will be power receivers, and some entities like the battery will be both.

What does a power system component need? It needs an amount of power and a way to signal the entity that something occurred.

Making the power source

Let’s create the PowerSource component first.

It is a Node that lives as a child of entities that belong to the power_sources group. It has a signal to tell it that machines on its system are drawing power, an amount of energy it’s able to provide, and an efficiency rating.

If something about the machine makes it work slow, the efficiency may be less than 1. For example, if it’s dirty or broken, its fuel is inefficient, or it is still priming itself. Your imagination is the limit.

We create a new script, PowerSource.gd.

## Component that can provide connected machines with electricity.
## Add it as a child node of to machines that produce energy.
class_name PowerSource
extends Node

## Signal for the power system to notify the component that it took
## a certain amount of power from the power source. Allows entities to react accordingly.
## For example, a battery can lower its stored amount or a generator can burn a tick of fuel.
signal power_updated(power_draw, delta)

## The maximum amount of power the machine can provide in units per tick.
export var power_amount := 10.0

## The possible directions for power to come `out` of the machine.
## The default value, 15, makes it omnidirectional.
## The FLAGS export hint below turns the value display in the Inspector into a checkbox list.
export (Types.Direction, FLAGS) var output_direction := 15

## How efficient the machine currently is. For instance, a machine that has no work
## to do has an efficiency of `0` where one that has a job has an efficiency of `1`.
var efficiency := 0.0


## Returns a float indicating the possible power multiplied by the current efficiency.
func get_effective_power() -> float:
    return power_amount * efficiency

Adding a power source to the Stirling engine

We can go back to the StirlingEngineEntity.tscn scene and add a new PowerSource child to it.

The amount of power it provides is up to you and your sense of game balance about what a large piston should be able to push out or what your power units even are. Identifying what those units are is going to be the GUI’s job. I set mine for 25, but that is before power balance.

Having a component is not enough unless the engine continually runs at 100% like a cosmic power source. We don’t have a GUI, inventory, or fuel in place yet, but we can use the tween node to update the source’s efficiency and ramp up time.

We can open the StirlingEngineEntity.gd script and add another property to interpolate to the tween.

#onready var shaft := $PistonShaft
onready var power := $PowerSource


func _ready() -> void:
    #...
    tween.interpolate_property(power, "efficiency", 0, 1, BOOTUP_TIME)
    #tween.start()

As we don’t have inventory or fuel, we don’t need the power_updated signal. We’ll come back to this script later to tie the signal to consume fuel. No fuel, no power.

Making the Receiver component

The power receiver is a replica of the source component, but we invert it to receive power instead. Instead of two classes for the source and receiver of power, we could’ve chosen to code a single “PowerUnit” component and controlled whether energy flowed in or out with a flag. Still, the extra class allows us to differentiate them as individual units and name the variables for the inspector.

## Component that receives electricity from a PowerSource.
## Add it as a child of a machine that needs electricity to be powered.
class_name PowerReceiver
extends Node

## Signal for the entity to react to it for when the receiver gets an amount of
## power each system tick.
## A battery can increase the amount of power stored, or an electric furnace can
## begin smelting ore once it receives the power it needs.
signal received_power(amount, delta)

## The required amount of power for the machine to function in units per tick optimally.
## If it receives less than that amount, it may mean the machine does not work or that it slows down.
export var power_required := 10.0

## The possible directions for power to come _in_ from, if not omnidirectional.
## The FLAGS keyword makes it a multiple-choice answer in the inspector.
export (Types.Direction, FLAGS) var input_direction := 15

## How efficient the machine is at present. For instance, a machine that has no work
## to do has an efficiency of `0` where one that has a job has an efficiency of 1.
## Affects the final power demand.
var efficiency := 0.0


## Returns a float indicating the required power multiplied by the current efficiency.
func get_effective_power() -> float:
    return power_required * efficiency

We don’t have anything to assign this component to yet, but we’ll create a battery soon to test the system. Speaking of, that’s the next step.

Implementing the power system

Now we have the two components, we can code the system that’ll use them both: the PowerSystem

The system’s functions have a couple of jobs. They need to detect and collect entities with the right components, clean them up when destroyed, build a path for power to travel from sources to receivers, and then update both.

If you want realism, you could also have them update the wires to melt under a certain amount of excess voltage or make the power fall off over distance. For the sake of efficiency and simplicity, in this demo, we do not update wires. We use them to check if a power source has a path to a power receiver.

Create a new script named PowerSystem.gd. As ever, we begin with the class variables for keeping track of entities and updating them.

## Holds references to entities in the world, and a series of paths that go from power sources
## to power receivers. Every system tick, it sends power from the sources to the
## receivers in order.
class_name PowerSystem
extends Reference

## Holds a set of power source components keyed by their map position. We keep
## track of components to create "paths" that go from source to receiver,
## which informs the system update loop to notify those components of power flow.
var power_sources := {}

## Holds a set of power receiver components keyed by their map position.
## Same purpose as power sources, we use them to create paths between source and
## receiver used in the update loop.
var power_receivers := {}

## Holds a set of entities that transmit power, like wires, keyed by their map
## position. Used exclusively to create a path from a source to receiver(s).
var power_movers := {}

## An array of 'power paths'. Those arrays are map positions with [0] being
## the location of a power source and the rest being receivers.
## We use these power paths in the update loop to calculate the amount of power
## in any given path (which has one source and one or more receivers) and inform
## the source and receivers of the final number.
var paths := []

## The cells that are already verified while building a power path. This
## allows us to skip revisiting cells that are already in the list so we
## only travel outwards.
var cells_travelled := []

## We use this set to keep track of how much power each receiver has already gotten.
## If you have two power sources with `10` units of power each feeding a machine
## that takes `20`, then each will provide `10` over both paths.
var receivers_already_provided := {}

To have the system keep track of when something has happened or should be happening, we can use the Event bus autoload we already coded.

We’ll add a new event for this script, systems_ticked. When you need a new global event, go back to Events.gd and add the signal accordingly.

## Signal emitted when the simulation triggers the systems for updates.
signal systems_ticked(delta)

We can now head back to PowerSystem.gd to connect to the Events.entity_placed, Events.entity_removed, and Events.systems_ticked signals.

func _init() -> void:
    Events.connect("entity_placed", self, "_on_entity_placed")
    Events.connect("entity_removed", self, "_on_entity_removed")
    Events.connect("systems_ticked", self, "_on_systems_ticked")

We’ll implement the _on_systems_ticked() callback in the next lesson. For now, we’ll code the other two, _on_entity_placed() and _on_entity_removed().

Detecting new entities

The dictionaries above need filling, and the functions connected to the Events need filling in. We start with collecting the power sources and receivers as the entity tracker places them.

To do so, we’ll need two helper functions: one to get a PowerSource component from an entity, and another to get a PowerReceiver component.

## Searches for a PowerSource component in the entity's children. Returns null
## if missing.
func _get_power_source_from(entity: Node) -> PowerSource:
    # For each child in the entity
    for child in entity.get_children():
        # Return the child if it's the component we need
        if child is PowerSource:
            return child
    return null


## Searches for a PowerReceiver component in the entity's children. Returns null
## if missing.
func _get_power_receiver_from(entity: Node) -> PowerReceiver:
    for child in entity.get_children():
        if child is PowerReceiver:
            return child
    return null

With those in place, we can code our _on_entity_placed() and _on_entity_removed() callbacks.

## Detects when the simulation places a new entity and puts its location in the respective
## dictionary if it's part of the powers groups. Triggers an update of power paths.
func _on_entity_placed(entity, cellv: Vector2) -> void:
    # A running tally of if we should update paths. If the new entity
    # is in none of the power groups, we don't need to update anything, so false
    # is the default.
    var retrace := false

    # Check if the entity is in the power sources or receivers groups.
    # Get its component using a helper function and trigger a power path update.
    if entity.is_in_group(Types.POWER_SOURCES):
        power_sources[cellv] = _get_power_source_from(entity)
        retrace = true

    if entity.is_in_group(Types.POWER_RECEIVERS):
        power_receivers[cellv] = _get_power_receiver_from(entity)
        retrace = true

    # If a power mover, store the entity and trigger a power path update.
    if entity.is_in_group(Types.POWER_MOVERS):
        power_movers[cellv] = entity
        retrace = true

    # Update the power paths only if necessary.
    if retrace:
        _retrace_paths()


## Detects when the simulation removes an entity. If any of our dictionaries held this
## location, erase it, and trigger a path update.
func _on_entity_removed(_entity, cellv: Vector2) -> void:
    # `Dictionary.erase()` returns true if it found the key and erased it.
    var retrace := power_sources.erase(cellv)
    # Note the use of `or` below. If any of the previous flags came back true, we don't
    # want to overwrite the previous true.
    retrace = power_receivers.erase(cellv) or retrace
    retrace = power_movers.erase(cellv) or retrace

    # Update the power paths only if necessary.
    if retrace:
        _retrace_paths()


# We'll implement this function in the next lesson.
func _retrace_paths() -> void:
    pass

We’ve made a lot of progress here, although we’re still missing some code to get the power system working. In the next lesson, we’ll build the power paths and calculate them for real.

Code reference

Here are the complete scripts from this lesson.

PowerSource.gd

class_name PowerSource
extends Node

signal power_updated(power_draw, delta)

export var power_amount := 10.0
export (Types.Direction, FLAGS) var output_direction := 15

var efficiency := 0.0


func get_effective_power() -> float:
    return power_amount * efficiency

PowerReceiver.gd

class_name PowerReceiver
extends Node

signal received_power(amount, delta)

export var power_required := 10.0
export (Types.Direction, FLAGS) var input_direction := 15

var efficiency := 0.0


func get_effective_power() -> float:
    return power_required * efficiency

StirlingEngineEntity.gd

extends Entity

const BOOTUP_TIME := 6.0
const SHUTDOWN_TIME := 3.0

onready var animation_player := $AnimationPlayer
onready var tween := $Tween
onready var shaft := $PistonShaft
onready var power := $PowerSource


func _ready() -> void:
    animation_player.play("Work")
    tween.interpolate_property(animation_player, "playback_speed", 0, 1, BOOTUP_TIME)
    tween.interpolate_property(shaft, "modulate", Color.white, Color(0.5, 1, 0.5), BOOTUP_TIME)
    tween.interpolate_property(power, "efficiency", 0, 1, BOOTUP_TIME)
    tween.start()

PowerSystem.gd

class_name PowerSystem
extends Reference

var power_sources := {}
var power_receivers := {}
var power_movers := {}
var paths := []
var cells_travelled := []
var receivers_already_provided := {}


func _init() -> void:
    Events.connect("entity_placed", self, "_on_entity_placed")
    Events.connect("entity_removed", self, "_on_entity_removed")
    Events.connect("systems_ticked", self, "_on_systems_ticked")


func _get_power_source_from(entity: Node) -> PowerSource:
    for child in entity.get_children():
        if child is PowerSource:
            return child
    return null


func _get_power_receiver_from(entity: Node) -> PowerReceiver:
    for child in entity.get_children():
        if child is PowerReceiver:
            return child
    return null


func _on_entity_placed(entity, cellv: Vector2) -> void:
    var retrace := false

    if entity.is_in_group(Types.POWER_SOURCES):
        power_sources[cellv] = _get_power_source_from(entity)
        retrace = true

    if entity.is_in_group(Types.POWER_RECEIVERS):
        power_receivers[cellv] = _get_power_receiver_from(entity)
        retrace = true

    if entity.is_in_group(Types.POWER_MOVERS):
        power_movers[cellv] = entity
        retrace = true

    if retrace:
        _retrace_paths()


func _on_entity_removed(_entity, cellv: Vector2) -> void:
    var retrace := power_sources.erase(cellv)
    retrace = power_receivers.erase(cellv) or retrace
    retrace = power_movers.erase(cellv) or retrace

    if retrace:
        _retrace_paths()


func _retrace_paths() -> void:
    pass

Events.gd

extends Node

signal entity_placed(entity, cellv)
signal entity_removed(entity, cellv)
signal systems_ticked(delta)