The battery entity

When it comes to powering machines, the final piece of the puzzle is the battery.

The battery acts both as a power receiver and as a source. It can take power from three of its four sides from a generator or wire and act as a power source, providing other machines with electricity from its remaining side.

It’s a device that leverages and illustrates the flexibility of our power system.

The Power Direction scene

Before coding the battery itself, we need a way for the player to choose which side is the input and the output.

While we could make four sprites for each machine and allow rotating it, we can save work by implying the direction with arrows. Besides that, if we want a battery indicator on the battery assembly, rotating it with graphics might hide it!

We’ll create a new scene we can instantiate in blueprints that have a power direction component as a child. We can then configure it using the blueprint itself, which can inform the power source or receiver’s direction property so the system can use it properly.

Create a new PowerDirection scene, a Node2D with four sprites for west, north, east, and south.

The sprites are in a position that fits along the floor of the tiles. That is, in an isometric diagonal fashion. The graphics themselves are part of the main tileset.png sprite sheet in /Shared/ and you can once again use Region -> Enabled and the TextureRegion bottom panel to set the four sprites.

Don’t sweat their vertical position relative to the origin; make sure they are correct relative to each other. We can always move the entire assembly later to fit the sprite we’re working with.

Note that the arrows at the top are further apart than the bottom ones. This is because if they’re too close to one another, the machine’s sprite might hide them.

Let’s get coding. Attach a script to the PowerDirection node.

The script consists of a constant with regions from the sprite sheet for each of the arrows and a setter to configure the four sprites for us.

extends Node2D

## Arrows from the sprite sheet in a dictionary keyed with a description of which way
## the arrow faces.
const REGIONS := {
    "UpLeft": Rect2(899, 134, 31, 17),
    "DownRight": Rect2(950, 179, 31, 17),
    "UpRight": Rect2(950, 134, 31, 17),
    "DownLeft": Rect2(899, 179, 31, 17)
}

## A set of flags based on our `Types.Direction` enum. Allows you to choose the output 
## direction(s) for the entity.
export (Types.Direction, FLAGS) var output_directions: int = 15 setget _set_output_directions

## References to the scene's four sprite nodes.
onready var west := $W
onready var north := $N
onready var east := $E
onready var south := $S


## Compares the output directions to the `Types.Direction` enum and assigns the correct
## arrow texture to it.
func set_indicators() -> void:
    # If LEFT's bits are in `output_directions`
    if output_directions & Types.Direction.LEFT != 0:
        # ...set the west arrow to point out
        west.region_rect = REGIONS.UpLeft
    else:
        # ...otherwise, set it to point in by using the bottom right arrow graphic
        west.region_rect = REGIONS.DownRight

    # Repeat for all four arrows individually
    if output_directions & Types.Direction.RIGHT != 0:
        east.region_rect = REGIONS.DownRight
    else:
        east.region_rect = REGIONS.UpLeft

    if output_directions & Types.Direction.UP != 0:
        north.region_rect = REGIONS.UpRight
    else:
        north.region_rect = REGIONS.DownLeft

    if output_directions & Types.Direction.DOWN != 0:
        south.region_rect = REGIONS.DownLeft
    else:
        south.region_rect = REGIONS.UpRight


## The setter for the blueprint's direction value.
func _set_output_directions(value: int) -> void:
    output_directions = value

    # Wait until the blueprint has appeared in the scene tree at least once.
    # We must do this as setters get called _before_ the node is in the scene tree,
    # meaning the sprites are not yet in their onready variables.
    if not is_inside_tree():
        yield(self, "ready")

    # Set the sprite graphics according to the direction value.
    set_indicators()

Blueprint and rotation

Let’s now work on the battery itself. As always, for it, we need both a blueprint and an entity to place in the world.

We’ll start with the blueprint. The battery blueprint is a Node2D with a single sprite and our new PowerDirection scene instanced in.

Of course, we make sure to also set the power direction’s direction appropriately. We want our battery to have three inputs and one output, so we toggle off the directions from the Output Directions property until only Left remains.

The battery blueprint doesn’t need a new class. Instead, we’ll edit the base BlueprintEntity.gd script and apply it to the Node2D of the scene. The edit is all about the new rotation object. If it exists, we find it and rotate its values accordingly.

Open BlueprintEntity.gd and add the following code to it.

#...

## We use `find_node()` to search for a `PowerDirection` instance. If it does not exist,
## then we don't worry about it: `find_node()` returns `null` if it finds nothing.
## A faster method would be `get_node()`, which only tests one path.
onready var _power_direction := find_node("PowerDirection")


## Rotate the blueprint's direction, if it has one and it is relevant.
func rotate_blueprint() -> void:
    if not _power_direction:
        return

    # Get the current directions flags.
    var directions: int = _power_direction.output_directions
    # Initialize the new direction value at 0.
    var new_directions := 0

    # Below, we check for each `Types.Direction` against the `directions` value.
    # If a direction is included in the `directions` flag, we "rotate" it using the 
    # bitwise pipe operator `|`.
    # LEFT becomes UP
    if directions & Types.Direction.LEFT != 0:
        new_directions |= Types.Direction.UP

    # UP becomes RIGHT
    if directions & Types.Direction.UP != 0:
        new_directions |= Types.Direction.RIGHT

    # RIGHT becomes DOWN
    if directions & Types.Direction.RIGHT != 0:
        new_directions |= Types.Direction.DOWN

    # DOWN becomes LEFT
    if directions & Types.Direction.DOWN != 0:
        new_directions |= Types.Direction.LEFT

    # Set the new direction, which should set the arrow sprites
    _power_direction.output_directions = new_directions

Battery Entity and class

Let’s now create the Battery entity to go along with the blueprint. Create a new scene with StaticBody2D named BatteryEntity as its root.

Like the StirlingEngine, the battery is a static body with a single sprite from the sprite sheet and a collision polygon.

The main difference is that we instantiate both the PowerSource and PowerReceiver components as a child, as the battery should both store and provide electricity.

In the viewport, the battery should look like this.

As such, we need to make the BatteryEntity node part of both the power_receivers and power_source groups so the PowerSystem object can send and draw power from the battery.

For good measure, you can also set the PowerSource’s Output Direction to match the blueprint’s so that, even if we put down a battery without a blueprint, it will still have one direction.

Let’s attach a new script to the BatteryEntity that extends Entity.

The script’s role is to set the amount of power the battery can hold at any time. It connects to both the PowerSource.power_updated signal and the PowerReceiver.power_received signals.

In turn, the signals update the amount of stored power and the efficiency of the battery’s PowerSource and PowerReceiver components, as you’ll see in the _set_stored_power() setter function below. This ensures a battery never provides more power than it has and never receives more than it needs.

I’ve gone ahead and set both the PowerSource and PowerReceiver Power Amount and Power Required properties to 200 in the Inspector so the battery charges and discharges fast.

Here’s the BatteryEntity class.

extends Entity

## The total amount of power the battery can hold at max capacity.
export var max_storage := 1000.0

## The amount of power the battery is currently holding.
## See the setter function below to see how we use it to update the source and receiver 
## components' efficiency.
var stored_power := 0.0 setget _set_stored_power

## References to our `PowerReceiver` and `PowerSource` nodes.
onready var receiver := $PowerReceiver
onready var source := $PowerSource


func _ready() -> void:
    # If the source is not omnidirectional:
    if source.output_direction != 15:
        # Set the receiver direction to the _opposite_ of the source.
        # The ^ is the XOR (exclusive or) operator.
        # If | returns 1 if either bit is 1, and & returns 1 if both bits are 1,
        # ^ returns 1 if the bits _do not_ match.

        # This inverts the direction flags, making each direction that's not an output 
        # a valid input to receive energy.
        receiver.input_direction = 15 ^ source.output_direction


## The setup function fetches the direction from the blueprint, applies it
## to the source, and inverts it for the receiver with the XOR operator (^).
func _setup(blueprint: BlueprintEntity) -> void:
    source.output_direction = blueprint._power_direction.output_directions
    receiver.input_direction = 15 ^ source.output_direction


## Set the efficiency in source and receiver based on the amount of stored power.
func _set_stored_power(value: float) -> void:
    # We set the stored power and prevent it from becoming negative.
    stored_power = max(value, 0)

    # Wait until the entity is ready to ensure we have access to the `receiver` and the `source` nodes.
    if not is_inside_tree():
        yield(self, "ready")

    # Set the receiver's efficiency.
    receiver.efficiency = (
        0.0
        # If the battery is full, set it to 0. We don't want it to draw more power.
        if stored_power >= max_storage
        # If the battery is less than full, set it to between 1 and
        # the percentage of how empty the battery is.
        # This makes the battery fill up slower as it approaches being full.
        else min((max_storage - stored_power) / receiver.power_required, 1.0)
    )

    # Set the source efficiency to `0` if there is no power. Otherwise, we set it to a percentage of how full
    # the battery is. A battery that has more power than it must provide returns one, whereas a battery
    # that has less returns some percentage of that.
    source.efficiency = (0.0 if stored_power <= 0 else min(stored_power / source.power_amount, 1.0))


## Sets the stored power using the setter based on the received amount of power per second.
func _on_PowerReceiver_received_power(amount: float, delta: float) -> void:
    self.stored_power = stored_power + amount * delta


## Sets the stored power using the setter based on the amount of power provided per second.
func _on_PowerSource_power_updated(power_draw: float, delta: float) -> void:
    self.stored_power = stored_power - min(power_draw, source.get_effective_power()) * delta

Power indicator

We could add the battery and its blueprint right away to the entity placer, but we’re going to improve the entity’s visuals first. We’ll draw a gauge directly on the battery that fills up and shows how charged the device is.

Add a new Sprite to the battery and apply the battery_indicator.png image to its Texture. I named the node Indicator.

Position the sprite so it fits the battery icon’s rectangular display.

We can use a shader to render the charge indicator. In the node’s Material, assign a new ShaderMaterial with a new Shader.

We also must enable Local to Scene under the Resource dropdown. Otherwise, every battery would share the same instance of the shader! Godot does this for performance reasons, but Local to Scene ensures each instance of this scene has a unique version of the material.

Open the shader editor by clicking on the Shader resource and use the following shader.

We won’t cover shader basics here because we do so in our Shader Secrets course, but here’s the gist of how it works:

  1. We use the sprite’s Texture as a mask.
  2. Using the step() function and an amount uniform, we crop the mask horizontally. This allows us only to draw part of the gauge.
  3. We draw the cropped bar using the node’s Modulate color.

We’ll control the bar’s fill from the BatteryEntity’s script to animate the indicator.

Here’s the shader’s code.

shader_type canvas_item;

//A percentage from 0 to 1 for how full the battery is.
uniform float amount : hint_range(0, 1) = 0.0;

void fragment() {
    //We sample the texture as a mask to later change the indicator's color based on its fill rate.
  vec4 mask = texture(TEXTURE, UV);
    //We only need the texture's red channel. The rest, black or transparent, is of
  //no interest.
  float masking_area = mask.r;
    
    //We Set the percentage of the UV sampling along the X-axis to either 0 or 1,
  //based on how full the battery is. If `amount` is `0.5`, we want the bar to be half full.
  float uv_percentage = step(UV.x, amount);
    
    //We draw the output pixel using the sprite's `modulate` color and mask out part of it.
  COLOR = vec4(MODULATE.rgb, uv_percentage * masking_area);
}

To test the shader, expand the Shader Params in the Inspector and play with the Amount slider.

We need to go back to the BatteryEntity.gd script to update the indicator in-game. To do so, we can call the sprite material’s set_shader_param() method to set its amount uniform to a value between 0 and 1.

onready var indicator := $Indicator

func _set_stored_power(value: float) -> void:
    #...
    indicator.material.set_shader_param("amount", stored_power / max_storage)

Placing the battery

Let’s now register the battery in the EntityPlacer class. Open the EntityPlacer.gd script and add the battery as we did for the Stirling engine and wire before.

onready var Library := {
    #...
    "Battery": preload("res://Entities/Blueprints/BatteryBlueprint.tscn").instance()
}

func _ready() -> void:
    #...
    Library[Library.Battery] = preload("res://Entities/Entities/BatteryEntity.tscn")


func _exit_tree() -> void:
    #...
    Library.Battery.queue_free()

Inside of _unhandled_input, we add the appropriate action to rotate the blueprint’s power indicator if it has one.

func _unhandled_input(event: InputEvent) -> void:
    #...
    # elif event.is_action_pressed("drop") and _blueprint:
        #...
    
    elif event.is_action_pressed("rotate_blueprint") and _blueprint:
        _blueprint.rotate_blueprint()

And we add a temporary keyboard shortcut to can place batteries down with the keyboard.

    # elif event.is_action_pressed("quickbar_2"):
        #...
    # Same code as above, for testing purposes, feel free to copy and paste the block above, but 
    # be sure to change the input action to `quickbar_3`.
    elif event.is_action_pressed("quickbar_3"):
        if _blueprint:
            remove_child(_blueprint)
        _blueprint = Library.Battery
        add_child(_blueprint)
        _move_blueprint_in_world(cellv)

With this, we can place down engines that generate power. The power travels down wires and fills up batteries, which show how full they get thanks to their on-screen indicator.

This concludes the first chapter of the Godot simulation game course.

In the next and upcoming chapter, we’ll create machines that consume energy, resources the player can gather, and work on the game’s user interface.

Code reference

Here are the complete scripts we worked on in this lesson.

PowerDirection.gd

extends Node2D

const REGIONS := {
    "UpLeft": Rect2(899, 134, 31, 17),
    "DownRight": Rect2(950, 179, 31, 17),
    "UpRight": Rect2(950, 134, 31, 17),
    "DownLeft": Rect2(899, 179, 31, 17)
}

export (Types.Direction, FLAGS) var output_directions: int = 15 setget _set_output_directions

onready var west := $W
onready var north := $N
onready var east := $E
onready var south := $S


func set_indicators() -> void:
    if output_directions & Types.Direction.LEFT != 0:
        west.region_rect = REGIONS.UpLeft
    else:
        west.region_rect = REGIONS.DownRight

    if output_directions & Types.Direction.RIGHT != 0:
        east.region_rect = REGIONS.DownRight
    else:
        east.region_rect = REGIONS.UpLeft

    if output_directions & Types.Direction.UP != 0:
        north.region_rect = REGIONS.UpRight
    else:
        north.region_rect = REGIONS.DownLeft

    if output_directions & Types.Direction.DOWN != 0:
        south.region_rect = REGIONS.DownLeft
    else:
        south.region_rect = REGIONS.UpRight


func _set_output_directions(value: int) -> void:
    output_directions = value
    if not is_inside_tree():
        yield(self, "ready")
    set_indicators()

BlueprintEntity.gd

onready var _power_direction := find_node("PowerDirection")


func rotate_blueprint() -> void:
    if not _power_direction:
        return

    var directions: int = _power_direction.output_directions
    var new_directions := 0

    if directions & Types.Direction.LEFT != 0:
        new_directions |= Types.Direction.UP

    if directions & Types.Direction.UP != 0:
        new_directions |= Types.Direction.RIGHT

    if directions & Types.Direction.RIGHT != 0:
        new_directions |= Types.Direction.DOWN

    if directions & Types.Direction.DOWN != 0:
        new_directions |= Types.Direction.LEFT

    _power_direction.output_directions = new_directions

BatteryEntity.gd

extends Entity

export var max_storage := 1000.0

var stored_power := 0.0 setget _set_stored_power

onready var receiver := $PowerReceiver
onready var source := $PowerSource
onready var indicator := $Indicator


func _ready() -> void:
    if source.output_direction != 15:
        receiver.input_direction = 15 ^ source.output_direction


func _setup(blueprint: BlueprintEntity) -> void:
    source.output_direction = blueprint._power_direction.output_directions
    receiver.input_direction = 15 ^ source.output_direction


func _set_stored_power(value: float) -> void:
    stored_power = max(value, 0)

    if not is_inside_tree():
        yield(self, "ready")

    receiver.efficiency = (
        0.0
        if stored_power >= max_storage
        else min((max_storage - stored_power) / receiver.power_required, 1.0)
    )
    source.efficiency = (0.0 if stored_power <= 0 else min(stored_power / source.power_amount, 1.0))
    indicator.material.set_shader_param("amount", stored_power / max_storage)


func _on_PowerReceiver_received_power(amount: float, delta: float) -> void:
    self.stored_power = stored_power + amount * delta


func _on_PowerSource_power_updated(power_draw: float, delta: float) -> void:
    self.stored_power = stored_power - min(power_draw, source.get_effective_power()) * delta

The battery’s Indicator sprite’s shader.

shader_type canvas_item;

uniform float amount : hint_range(0, 1) = 0.0;

void fragment() {
  vec4 mask = texture(TEXTURE, UV);
  float masking_area = mask.r;
    
  float uv_percentage = step(UV.x, amount);
    
  COLOR = vec4(MODULATE.rgb, uv_percentage * masking_area);
}

And here’s the complete EntityPlacer.gd.

extends TileMap

const MAXIMUM_WORK_DISTANCE := 275.0
const POSITION_OFFSET := Vector2(0,25)
const DECONSTRUCT_TIME := 0.3

var _blueprint: BlueprintEntity
var _tracker: EntityTracker
var _ground: TileMap
var _flat_entities: Node2D
var _player: KinematicBody2D
var _current_deconstruct_location := Vector2.ZERO


onready var Library := {
    "StirlingEngine": preload("res://Entities/Blueprints/StirlingEngineBlueprint.tscn").instance(),
    "Wire": preload("res://Entities/Blueprints/WireBlueprint.tscn").instance(),
    "Battery": preload("res://Entities/Blueprints/BatteryBlueprint.tscn").instance()
}
onready var _deconstruct_timer := $Timer


func _ready() -> void:
    Library[Library.StirlingEngine] = preload("res://Entities/Entities/StirlingEngineEntity.tscn")
    Library[Library.Wire] = preload("res://Entities/Entities/WireEntity.tscn")
    Library[Library.Battery] = preload("res://Entities/Entities/BatteryEntity.tscn")



func _exit_tree() -> void:
    Library.StirlingEngine.queue_free()
    Library.Wire.queue_free()


func _unhandled_input(event: InputEvent) -> void:
    var global_mouse_position := get_global_mouse_position()

    var has_placeable_blueprint: bool = _blueprint and _blueprint.placeable
    var is_close_to_player := (
        global_mouse_position.distance_to(_player.global_position)
        < MAXIMUM_WORK_DISTANCE
    )

    var cellv := world_to_map(global_mouse_position)
    var cell_is_occupied := _tracker.is_cell_occupied(cellv)
    var is_on_ground := _ground.get_cellv(cellv) == 0

    if event is InputEventMouseButton:
        _abort_deconstruct()

    if event.is_action_pressed("left_click"):
        if has_placeable_blueprint:
            if not cell_is_occupied and is_close_to_player and is_on_ground:
                _place_entity(cellv)
                _update_neighboring_flat_entities(cellv)

    elif event.is_action_pressed("right_click") and not has_placeable_blueprint:
        if cell_is_occupied and is_close_to_player:
            _deconstruct(global_mouse_position, cellv)

    elif event is InputEventMouseMotion:
        if cellv != _current_deconstruct_location:
            _abort_deconstruct()
        if has_placeable_blueprint:
            _move_blueprint_in_world(cellv)

    elif event.is_action_pressed("drop") and _blueprint:
        remove_child(_blueprint)
        _blueprint = null

    elif event.is_action_pressed("rotate_blueprint") and _blueprint:
        _blueprint.rotate_blueprint()

    elif event.is_action_pressed("quickbar_1"):
        if _blueprint:
            remove_child(_blueprint)
        _blueprint = Library.StirlingEngine
        add_child(_blueprint)
        _move_blueprint_in_world(cellv)

    elif event.is_action_pressed("quickbar_2"):
        if _blueprint:
            remove_child(_blueprint)
        _blueprint = Library.Wire
        add_child(_blueprint)
        _move_blueprint_in_world(cellv)

    elif event.is_action_pressed("quickbar_3"):
        if _blueprint:
            remove_child(_blueprint)
        _blueprint = Library.Battery
        add_child(_blueprint)
        _move_blueprint_in_world(cellv)


func _process(_delta: float) -> void:
    var has_placeable_blueprint: bool = _blueprint and _blueprint.placeable
    if has_placeable_blueprint:
        _move_blueprint_in_world(world_to_map(get_global_mouse_position()))


func setup(tracker: EntityTracker, ground: TileMap, flat_entities: YSort, player: KinematicBody2D) -> void:
    _tracker = tracker
    _ground = ground
    _player = player
    _flat_entities = flat_entities

    for child in get_children():
        if child is Entity:
            var map_position := world_to_map(child.global_position)
            _tracker.place_entity(child, map_position)


func _move_blueprint_in_world(cellv: Vector2) -> void:
    _blueprint.global_position = map_to_world(cellv) + POSITION_OFFSET

    var is_close_to_player := (
        get_global_mouse_position().distance_to(_player.global_position)
        < MAXIMUM_WORK_DISTANCE
    )
    var is_on_ground: bool = _ground.get_cellv(cellv) == 0
    var cell_is_occupied := _tracker.is_cell_occupied(cellv)

    if not cell_is_occupied and is_close_to_player and is_on_ground:
        _blueprint.modulate = Color.white
    else:
        _blueprint.modulate = Color.red

    if _blueprint is WireBlueprint:
        WireBlueprint.set_sprite_for_direction(_blueprint.sprite, _get_powered_neighbors(cellv))


func _place_entity(cellv: Vector2) -> void:
    var new_entity: Node2D = Library[_blueprint].instance()

    if _blueprint is WireBlueprint:
        var directions := _get_powered_neighbors(cellv)
        _flat_entities.add_child(new_entity)
        WireBlueprint.set_sprite_for_direction(new_entity.sprite, directions)
    else:
        add_child(new_entity)

    new_entity.global_position = map_to_world(cellv) + POSITION_OFFSET
    new_entity._setup(_blueprint)
    _tracker.place_entity(new_entity, cellv)


func _deconstruct(event_position: Vector2, cellv: Vector2) -> void:
    _deconstruct_timer.connect(
        "timeout", self, "_finish_deconstruct", [cellv], CONNECT_ONESHOT
    )
    _deconstruct_timer.start(DECONSTRUCT_TIME)
    _current_deconstruct_location = cellv


func _finish_deconstruct(cellv: Vector2) -> void:
    var entity := _tracker.get_entity_at(cellv)
    _tracker.remove_entity(cellv)
    _update_neighboring_flat_entities(cellv)


func _abort_deconstruct() -> void:
    if _deconstruct_timer.is_connected("timeout", self, "_finish_deconstruct"):
        _deconstruct_timer.disconnect("timeout", self, "_finish_deconstruct")
    _deconstruct_timer.stop()


func _get_powered_neighbors(cellv: Vector2) -> int:
    var direction := 0

    for neighbor in Types.NEIGHBORS.keys():
        var key: Vector2 = cellv + Types.NEIGHBORS[neighbor]

        if _tracker.is_cell_occupied(key):
            var entity: Node = _tracker.get_entity_at(key)
            if (
                entity.is_in_group(Types.POWER_MOVERS)
                or entity.is_in_group(Types.POWER_RECEIVERS)
                or entity.is_in_group(Types.POWER_SOURCES)
            ):
                direction |= neighbor

    return direction


func _update_neighboring_flat_entities(cellv: Vector2) -> void:
    for neighbor in Types.NEIGHBORS.keys():
        var key: Vector2 = cellv + Types.NEIGHBORS[neighbor]
        var object = _tracker.get_entity_at(key)

        if object and object is WireEntity:
            var tile_directions := _get_powered_neighbors(key)
            print(tile_directions)
            WireBlueprint.set_sprite_for_direction(object.sprite, tile_directions)