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:
Events
bus pattern, and you can find a lesson dedicated to it in the design pattern chapter.EntityTracker
.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
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 Node
s 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
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 # ...
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)