Wiring entities

For now, we have an unlimited power source with our Stirling engine, but the power is not going anywhere. We want to generate power using engines and transport it over long distances, as illustrated in the image below.

We have a Stirling engine filling up batteries which, in turn, power two furnaces.

To do so, we’ll start by coding wires. They will look like the pipes running along the ground that the player can walk over. That means they will have no collision and will live underneath all other entities.

The wires need to connect to nearby entities when placed. We’ll need to add exceptions and functions to the EntityPlacer to do that.

Here’s an illustration of the result we should get by the end of this lesson. When placing a wire next to cells with wires, we update their sprites so they connect.

To do so, we’ll code a system similar to the TileMap node’s autotile feature.

Storing constants in a helper class

We’ll start by coding a static class, Types.gd. By adding the line a class_name Types at the top, we can access the class’s constants from anywhere in our codebase, making it so we don’t have to re-define the same constants in different places.

The first constants we define in Types are an enum and dictionary to help look for neighboring entities around a given cell.

We also define constants for some node groups: we can use those to identify and find specific nodes using group functions. For example, we’ll use is_in_group() to know whether an entity is a power source, receives power, or a tile that power goes through.

class_name Types
extends Reference

## We store constants for possible directions: up, down, left, and right.
## We'll use bitwise operators to combine them, allowing us to map directions 
## to different wire sprites. 
## For example, if we combine RIGHT and DOWN below, we will get the number `3`.
## We write these numbers in base 10. If you prefer, you can write them in binary using the 
## prefix 0b. For example, 0b0001 is the number `1` in binary, and 0b1000 is the number `8`.
enum Direction { RIGHT = 1, DOWN = 2, LEFT = 4, UP = 8 }

## This dictionary maps our `Direction` values to `Vector2` coordinates, to loop over neighbors of 
## a given cell.
const NEIGHBORS := {
    Direction.RIGHT: Vector2.RIGHT,
    Direction.DOWN: Vector2.DOWN,
    Direction.LEFT: Vector2.LEFT,
    Direction.UP: Vector2.UP
}

# Group name constants. Storing them as constants help prevent typos.
const POWER_MOVERS := "power_movers"
const POWER_RECEIVERS := "power_receivers"
const POWER_SOURCES := "power_sources"

Regarding the Direction enum above, if it’s your first time using numbers to represent directions, the benefit of doing so might be unclear.

Using the powers of two allows us to map every possible combination of directions to a unique number. For example, RIGHT + LEFT + UP = 1 + 4 + 8 = 13. You can do this for each of the fifteen possible combinations to get unique numbers from 1 to 15.

This is how the Godot engine manages flags, like the physics layers and masks. As you’ll see moving forward, we can export these flags as a list of checkboxes in the Inspector.

Also, using bit-wise operators, we can combine and compare directions in interesting ways. You’ll get a couple examples by the end of the series.

When a wire has other wires next to it, we’ll combine the Direction constant of these neighboring tiles and use the result to find the sprite we should use for our newly placed wire.

With our group constants defined, go back to the StirlingEngineEntity scene and assign the power_sources group to the scene’s root node.

Wire blueprint and wiring rules

When we lay down a wire, we want to see if there is a machine or another wire in the adjacent cell to connect to them. In the sprite sheet for both entities and blueprints, I lay down wires in the same place so they work like a sprite sheet.

Since blueprints are always available in the EntityPlacer instance, we can define and store the wire sprites’ region data in their blueprint. Using some helper functions, we can get the right sprite based on a direction value.

Create a new scene with a Node2D named WireBlueprint as its root and a Sprite node as its child.

We assign the blueprints.svg texture to the Sprite and set its Region -> Enabled to true.

You want to use the Snap Mode -> Grid Snap with the properties in the image below and click and drag to define a square region for the wire.

The sprites fit in a grid of 100 by 100 pixels with a 10 pixels gutter, as illustrated below. We’ll reuse these properties for the WireBlueprint.gd script and create a matching sprite in the WireEntity scene.

Doing so will make the wire seamlessly turn from a blueprint into a world entity when the player places it in the world.

Attach a new script to WireBlueprint that inherits from BlueprintEntity.

## Blueprint for a wire. Provides functions and data to calculate the sprite's region to use
## depending on where the player places the wire and whether there are wires and machines
## in adjacent cells.
class_name WireBlueprint
extends BlueprintEntity

## Constant dictionary that holds the sprite region information for the wire's spritesheet.
## The numbers used as keys represent combinations of the direction values we 
## wrote in `Types.Directions`.
## Note how some of them repeat: `LEFT`, `RIGHT`, and `LEFT+RIGHT` are all the same region.
## This keeps the helper functions below input safe. In case the user ever passes in a
## single direction, Godot will not crash because of a missing dictionary key.
## All 15 possible numbers have a corresponding sprite region we've chosen.
const DIRECTIONS_DATA := {
    # The `Rect2` values below correspond to different wire sprites in our `tileset.svg` sprite sheet.
    # As shown in the image above, in the `tileset.svg` and `blueprints.svg` textures, each sprite 
    # fits in a `100` by `100` pixels square.
    # Also, I separated them from other cells by `10` pixels, and there's a `10` 
    # pixels margin from the edge of the texture.
    # That's why the width and height of the rectangles below are always `100`, but their start 
    # position is a multiple of 110 + 10.
    1: Rect2(120, 10, 100, 100),
    4: Rect2(120, 10, 100, 100),
    5: Rect2(120, 10, 100, 100),
    2: Rect2(230, 10, 100, 100),
    8: Rect2(230, 10, 100, 100),
    10: Rect2(230, 10, 100, 100),
    15: Rect2(340, 10, 100, 100),
    6: Rect2(450, 10, 100, 100),
    12: Rect2(560, 10, 100, 100),
    3: Rect2(670, 10, 100, 100),
    9: Rect2(780, 10, 100, 100),
    7: Rect2(890, 10, 100, 100),
    14: Rect2(10, 120, 100, 100),
    13: Rect2(120, 120, 100, 100),
    11: Rect2(230, 120, 100, 100)
}

onready var sprite := $Sprite


## Helper function to set the sprite based on the provided combined value for `directions` 
## in which there are neighboring wires or machines to connect wire to.
static func set_sprite_for_direction(sprite: Sprite, directions: int) -> void:
    sprite.region_rect = get_region_for_direction(directions)


## Static function to get an appropriate value from `DIRECTIONS_DATA`.
static func get_region_for_direction(directions: int) -> Rect2:
    # If the `directions` value is invalid, default to `10`, which is UP + DOWN.
    if not DIRECTIONS_DATA.has(directions):
        directions = 10

    return DIRECTIONS_DATA[directions]

Creating an Entity for the wires

We need an entity to place in the world for our wires, to go along with their blueprint.

We create a new WireEntity scene with a Node2D as its root named WireEntity and a Sprite node. We assign the power_movers group to WireEntity so we can identify it in the power system later.

The wire entity does not have much to do. We are using it as a way to access its sprite and to identify it using the is keyword thanks to its class_name.

However, we need to be careful with its sprite, as we’ll assign one of the Rect2 regions we defined in the WireBlueprint class.

We assign the tileset.svg texture to the Sprite node, set its Region -> Enabled to true, and use the TextureRegion bottom panel to pick its sprite.

You want to use the Snap Mode -> Grid Snap with the properties in the image below and click and drag

Like the blueprint, the position of the sprite is at (0, 27).

## Wire. Moves power through it.
class_name WireEntity
extends Entity

onready var sprite := $Sprite

Sorting entities the player can walk over

Back in the main scene, Simulation.tscn, we need a new sorting node for entities that are in the world but that the player can walk over. We create a new Ysort named FlatEntities above the existing YSort node.

To provide it as an option for the EntityPlacer, we need to update the Simulation.gd script to fetch it, then EntityPlacer’s setup() function to receive it and store it.

Here’s the code to add to Simulation.gd.

#...
onready var _flat_entities := $GameWorld/FlatEntities

func _ready() -> void:
    # Replace the existing call to `_entity_placer.setup()`
    _entity_placer.setup(_tracker, _ground, _flat_entities, _player)
    #...

And to EntityPlacer.gd’s setup().

#...
var _flat_entities: Node2D
#...

# Don't forget to add the `flat_entities` argument to the function definition.
func setup(tracker: EntityTracker, ground: TileMap, flat_entities: YSort, player: KinematicBody2D) -> void:
    #...
    _flat_entities = flat_entities
    #...

Placing wires

The rest of the chapter focuses on EntityPlacer.gd: placing the wire on the map and connecting them with machines or other wires that surround them.

Updating the library

Until we have a dynamic library system, we need to update our test Library variable with the wire’s blueprint and entity. We also need to free it the instance when the entity placer exits the tree.

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


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


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

Next, we need to select the wire blueprint and begin the process of placing it. The code works the same as the Stirling engine’s keyboard shortcut, except that we map the input to the Library.Wire instead of the Library.StirlingEngine.

func _unhandled_input(event: InputEvent) -> void:
    #elif event.is_action_pressed("quickbar_1"):
        #...
    # We duplicate the temporary hard-coded shortcut above.
    elif event.is_action_pressed("quickbar_2"):
        if _blueprint:
            remove_child(_blueprint)
        # This is the only difference: we assign the `WireBlueprint` to the `_blueprint` variable variable
        _blueprint = Library.Wire
        add_child(_blueprint)
        _move_blueprint_in_world(cellv)

Note that we’re only duplicating code while developing the game and until we have the inventory implemented. It takes us no time and works well for testing purposes.

Finding other nearby power sources

What should we do when we put down a wire and have it connect it to others around it?

We need to inspect entities in each of the four neighbor cells and determine if they are part of any node groups related to the power system. Those are the constants we defined in the Types class.

We can convert this into a direction integer using the Types class. We do this inside of a new helper function, _get_powered_neighbors().

## Returns a bit-wise integer based on whether the nearby objects can carry power.
func _get_powered_neighbors(cellv: Vector2) -> int:
    # Begin with a blank direction of 0
    var direction := 0

    # We loop over each neighboring direction from our `Types.NEIGHBORS` dictionary.
    for neighbor in Types.NEIGHBORS.keys():
        # We calculate the neighbor cell's coordinates.
        var key: Vector2 = cellv + Types.NEIGHBORS[neighbor]

        # We get the entity in that cell if there is one.
        if _tracker.is_cell_occupied(key):
            var entity: Node = _tracker.get_entity_at(key)

            # If the entity is part of any of the power groups,
            if (
                entity.is_in_group(Types.POWER_MOVERS)
                or entity.is_in_group(Types.POWER_RECEIVERS)
                or entity.is_in_group(Types.POWER_SOURCES)
            ):
            # We combine the number with the OR bitwise operator.
            # It's like using +=, but | prevents the same number from adding to itself.
            # Types.Direction.RIGHT (1) + Types.Direction.RIGHT (1) results in DOWN (2), which is wrong.
            # Types.Direction.RIGHT (1) | Types.Direction.RIGHT (1) still results in RIGHT (1).
            # Since we are iterating over all four directions and will not repeat, you can use +,
            # but I use the | operator to be more explicit about comparing bitwise enum FLAGS.
                direction |= neighbor

    return direction

Updating blueprint graphics

We have everything we need to put down wires now and have them connect. Beginning with the blueprint, we can add a stipulation that is specific to WireBlueprint with an if statement at the end of _move_blueprint_in_world().

func _move_blueprint_in_world(cellv: Vector2) -> void:
    #...
    if _blueprint is WireBlueprint:
        WireBlueprint.set_sprite_for_direction(_blueprint.sprite, _get_powered_neighbors(cellv))

Placing the wire entity

When placing the entity itself in the world in _place_entity(), we can test the blueprint and add the entity as a child of the FlatEntities node instead of EntityPlacer so it will be in its own sorting group.

We replace the line add_child(new_entity) with the following.

func _place_entity(cellv: Vector2) -> void:
    #...
    # Add it to the tilemap as a child so it gets sorted properly
    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
    #...

Connecting the wires

Our wire now connects to the wires and machines around it, but other wires do not connect to the newly added one.

We need neighbor wires to update their sprite to connect to the new wire. We can write a helper function for that. We define the new function at the bottom of EntityPlacer.gd

## Looks at each of the neighboring tiles and updates each of them to use the
## correct graphics based on their own neighbors.
func _update_neighboring_flat_entities(cellv: Vector2) -> void:
    # For each neighboring tile,
    for neighbor in Types.NEIGHBORS.keys():
        # We get the entity, if there is one
        var key: Vector2 = cellv + Types.NEIGHBORS[neighbor]
        var object = _tracker.get_entity_at(key)

        # If it's a wire, we have that wire update its graphics to connect to the new
        # entity.
        if object and object is WireEntity:
            var tile_directions := _get_powered_neighbors(key)
            WireBlueprint.set_sprite_for_direction(object.sprite, tile_directions)

We need to call the new function in two places:

  1. When the player places a new entity in the world.
  2. When the player deconstructs an entity.

Let’s start with adding an entity. We need to add a line to the _unhandled_input() function, right after calling _place_entity()

func _unhandled_input(event: InputEvent) -> void:
    #...
    #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)

We add the other call at the end of _finish_deconstruct().

func _finish_deconstruct(cellv: Vector2) -> void:
    #...
    _update_neighboring_flat_entities(cellv)

This should allow you to create any combination of wire patterns you need.

In the next lesson, we’ll start working on the power system, that’ll send energy from power sources like the Stirling engine through the wires.

Code reference

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

First, Types.gd.

class_name Types
extends Reference

enum Direction { RIGHT = 1, DOWN = 2, LEFT = 4, UP = 8 }

const NEIGHBORS := {
    Direction.RIGHT: Vector2.RIGHT,
    Direction.DOWN: Vector2.DOWN,
    Direction.LEFT: Vector2.LEFT,
    Direction.UP: Vector2.UP
}

const POWER_MOVERS := "power_movers"
const POWER_RECEIVERS := "power_receivers"
const POWER_SOURCES := "power_sources"

Second, WireBlueprint.gd.

class_name WireBlueprint
extends BlueprintEntity

const DIRECTIONS_DATA := {
    1: Rect2(120, 10, 100, 100),
    4: Rect2(120, 10, 100, 100),
    5: Rect2(120, 10, 100, 100),
    2: Rect2(230, 10, 100, 100),
    8: Rect2(230, 10, 100, 100),
    10: Rect2(230, 10, 100, 100),
    15: Rect2(340, 10, 100, 100),
    6: Rect2(450, 10, 100, 100),
    12: Rect2(560, 10, 100, 100),
    3: Rect2(670, 10, 100, 100),
    9: Rect2(780, 10, 100, 100),
    7: Rect2(890, 10, 100, 100),
    14: Rect2(10, 120, 100, 100),
    13: Rect2(120, 120, 100, 100),
    11: Rect2(230, 120, 100, 100)
}

onready var sprite := $Sprite


static func set_sprite_for_direction(sprite: Sprite, directions: int) -> void:
    sprite.region_rect = get_region_for_direction(directions)


static func get_region_for_direction(directions: int) -> Rect2:
    if not DIRECTIONS_DATA.has(directions):
        directions = 10

    return DIRECTIONS_DATA[directions]

Finally, EntityPlacer.gd, that is getting big!

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()
}
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")


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("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)


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)