The Events singleton and EntityTracker

We don’t want entities to be aware of each other. A Stirling engine should never hold a reference to another engine, battery, or wire.

Yet, we need a way for entities to work with each other. We also need to manage the game grid so the player can only put one entity per cell.

To do so, we’re going to code two tools:

  1. A singleton that allows any node to emit and receive signals globally. We call this the Events bus pattern, and you can find a lesson dedicated to it in the design pattern chapter.
  2. A helper object that will keep track of which grid cells are occupied by an entity, the EntityTracker.

The Events bus singleton

Entities are in a layer with the player, some in a layer beneath the player, and systems and the GUI will be in different branches of the scene tree. All these can be involved in the creation of an entity and need to work in concert. For example, in the final game, we want the player to be able to take an item in their inventory, select it, and place it or drop it in the world.

Connecting them all individually or bubbling every signal up the scene tree chain would be prohibitively messy and connect every entity.

Our goal, as programmers, is to limit connection and communication between objects to the minimum. We want every entity to be uniquely unaware of any of its neighbors. As a result, we will use a global object to emit events: an autoload.

To learn more about our recommended “best practices” regarding signals, see the Best Practices chapter.

We create an /Autoload/Events.gd script in the Autoloads folder that we add to the list of Autoloads in the Project Settings… -> Autoload tab.

Every signal that may need to transcend the hierarchy can go here. We will add more in future lessons, but for now, we add two signals related to placing and removing entities.

extends Node

## Signal emitted when the player places an entity, passing the entity and its
## position in map coordinates
signal entity_placed(entity, cellv)

## Signal emitted when the player removes an entity, passing the entity and its
## position in map coordinates
signal entity_removed(entity, cellv)

The EntityTracker

The EntityTracker is a helper object that belongs to the Simulation. Its purpose is to help the EntityPlacer figure out if an entity already occupies the current cell and is the one who emits signals when the player places or removes entities.

Create a new script, /Systems/EntityTracker.gd, in the FileSystem dock and have it inherit from Reference. References are lighter than Nodes and get erased by Godot when they are no longer referred to by anyone. That makes them ideal for little helpers like this.

The class provides methods to register, remove, and get a reference to an entity in the game grid. A fourth function allows us to check if a cell already has an entity in it.

## Sub-class of the simulation that keeps track of all entities and their location
## using dictionary keys. Emits signals when the player places or removes entities.
class_name EntityTracker
extends Reference

## A Dictionary of entities, keyed using `Vector2` tilemap coordinates.
var entities := {}

## Adds an entity to the dictionary so we can prevent other entities from taking
## the same location.
func place_entity(entity, cellv: Vector2) -> void:
    # If the cell is already taken, refuse to add it again.
    if entities.has(cellv):
        return

    # Add the entity keyed by its coordinates on the map.
    entities[cellv] = entity
    # Emit the signal about the new entity.
    Events.emit_signal("entity_placed", entity, cellv)


## Removes an entity from the dictionary so other entities can take its place
## in its location on the map.
func remove_entity(cellv: Vector2) -> void:
    # Refuse to function if the entity does not exist.
    if not entities.has(cellv):
        return

    # Get the entity, queue it for deletion, and emit a signal about
    # its removal.
    var entity = entities[cellv]
    var _result := entities.erase(cellv)
    Events.emit_signal("entity_removed", entity, cellv)
    entity.queue_free()


## Returns true if there is an entity at the given location.
func is_cell_occupied(cellv: Vector2) -> bool:
    return entities.has(cellv)


## Returns the entity at the given location, if it exists, or null otherwise.
func get_entity_at(cellv: Vector2) -> Node2D:
    if entities.has(cellv):
        return entities[cellv]
    else:
        return null

Simulation and delegation

The Simulation class itself will not do much processing. Instead, we want it to delegate its duties to other classes and systems.

In the /Systems/Simulation.gd script, we create an instance of our new EntityTracker class. We’ll pass it to the EntityPlacer in the next lesson so it can inform it of new entities.

# ...
# const INVISIBLE_BARRIER_ID := 2

var _tracker := EntityTracker.new()

# onready var _ground := $GameWorld/GroundTiles
# ...

Code reference

Here are complete scripts after adding changes in this lesson.

First, /Autoload/Events.gd.

extends Node

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

Second, /Systems/EntityTracker.gd.

class_name EntityTracker
extends Reference

var entities := {}

func place_entity(entity, cellv: Vector2) -> void:
    if entities.has(cellv):
        return

    entities[cellv] = entity
    Events.emit_signal("entity_placed", entity, cellv)


func remove_entity(cellv: Vector2) -> void:
    if not entities.has(cellv):
        return

    var entity = entities[cellv]
    var _result := entities.erase(cellv)
    Events.emit_signal("entity_removed", entity, cellv)
    entity.queue_free()


func is_cell_occupied(cellv: Vector2) -> bool:
    return entities.has(cellv)


func get_entity_at(cellv: Vector2) -> Node2D:
    if entities.has(cellv):
        return entities[cellv]
    else:
        return null

Finally, /Systems/Simulation.gd.

extends Node

const BARRIER_ID := 1
const INVISIBLE_BARRIER_ID := 2

var _tracker := EntityTracker.new()

onready var _ground := $GameWorld/GroundTiles


func _ready() -> void:
    var barriers: Array = _ground.get_used_cells_by_id(BARRIER_ID)

    for cellv in barriers:
        _ground.set_cellv(cellv, INVISIBLE_BARRIER_ID)
Community Discussion