In this lesson, we’ll build the work system, one that allows machines to produce items.
Working machines all do different jobs:
And so we need to code a system that supports that diversity.
Once again, we will use the component-system architecture, as we did in the first chapter with power management. We’ll introduce a key difference here, though.
When we built the power system, we put most of the logic inside the system class system, and the components mostly notified it.
That’s because all power sources and all power receivers act the same when transmitting electricity. A certain amount comes in or out, and we emit it as a signal.
As every machine will work differently, we’ll reverse the logic: the work system will be lightweight, and entities will run the bulk of the code.
Note that in this course, we designed the system to focus on crafting items.
Let’s start with the component we’ll attach to crafting machines, like furnaces.
As the power system and power receiver components before it, the work component is a tool to notify the entity of a crafting job’s progress or completion.
We’ll code it so it only crafts one item at a time, reports its progress on every simulation tick, and emits a signal when it is finished creating an item.
In this series, the component only crafts an item, but the idea is that you can use it to do any work that takes some time to complete.
Create a new script, WorkComponent.gd
that extends Node
. You can save it in the Systems/
directory.
We need signals for the entity to connect to, a function to prepare some work, and some functions to set the job up and enact some progress on the work.
## A component that makes entities craft items over time. class_name WorkComponent extends Node ## Emitted when some amount of work has been accomplished. signal work_accomplished(amount) ## Emitted when all the work has been accomplished. signal work_done(output) ## Emitted when something causes the worker to stop working. signal work_enabled_changed(enabled) ## The recipe we are currently using to do the automated crafting with. var current_recipe: Dictionary ## The expected blueprint that should result from this crafting job. var current_output: BlueprintEntity ## How much work time is available to complete in seconds. var available_work := 0.0 ## How fast the machine is working. A value of 1.0 means 100% speed. var work_speed := 0.0 ## If `true`, the worker should do work when the simulation ticks the work ## system. var is_enabled := false setget _set_is_enabled ## Checks the amount of stuff provided in the inventory slots and compares it ## to the recipes inside the provided `recipe_map`. ## ## The `inputs` are a map of items the parent entity possesses. In other words, ## it's the contents of its inventory. ## ## The dictionary should have the form {item_name: amount}. ## ## If we find something we can craft out of it, the function returns `true` and ## configures the component with the crafting job. Otherwise, it returns ## `false`. func setup_work(inputs: Dictionary, recipe_map: Dictionary) -> bool: # The `recipe_map` has an `inputs` array that's keyed to ingredient names and # amounts. # # So for each recipe in the `recipe_map`, we compare the recipe's inputs to # the `inputs` provided by the entity's inventory. for output in recipe_map.keys(): # Skip any instance where we have a recipe that has a blueprint we haven't # created. if not Library.blueprints.has(output): continue # We default to true. In the subsequent loop, we'll set this to false # if we ever find something we can't craft this particular recipe. var can_craft := true var recipe_inputs: Array = recipe_map[output].inputs.keys() for input in inputs.keys(): # The moment we find that we don't have the inputs for this recipe, # or not enough inputs for the recipe, set can_craft to false and # break so we don't keep iterating over this particular recipe. if not input in recipe_inputs or inputs[input] < recipe_map[output].inputs[input]: can_craft = false break # If we managed to make it to the end of the recipe list without setting # `can_craft` to `false`, we configure the component with the recipe, # instance the blueprint we want to create, and return `true`. if can_craft: current_recipe = recipe_map[output] current_output = Library.blueprints[output].instance() current_output.stack_count = current_recipe.amount available_work = current_recipe.time return true # Otherwise, no, we can't craft anything in the map, so we return `false`. return false ## Ticks the system and does `delta` time amount of work. ## ## If the node can complete some work through this function call, we emit ## `work_accomplished`. ## ## When it finishes crafting the item, we emit `work_done`. The parent entity ## can connect to this signal and give the component a new item to craft. func work(delta: float) -> void: if is_enabled and available_work > 0.0: var work_done := delta * work_speed available_work -= work_done emit_signal("work_accomplished", work_done) if available_work <= 0.0: emit_signal("work_done", current_output) ## Emits the `work_enabled_changed` signal if `value` is different from the ## current `is_enabled` property. func _set_is_enabled(value: bool) -> void: if is_enabled != value: emit_signal("work_enabled_changed", value) is_enabled = value
With this, we can create a WorkComponent
node as a child of any entity, which gives us an entryway to talk with the work system. Speaking of which…
We can move on with creating the system itself.
Create a new script in the Systems/
directory, WorkSystem.gd
.
As the power system, its job is to gather all entities that can craft items and call their update function, which we named work()
, when the simulation ticks systems.
class_name WorkSystem extends Reference ## Dictionary which maps the location of entities with a WorkComponent as keys and a ## reference to the component as their value. ## ## This allows us to know where there are machines that can craft items on the map. var workers := {} # We connect to global events to know when the simulation updates and when # entities are placed or removed to see if they can craft items and # register with this system. func _init() -> void: Events.connect("systems_ticked", self, "_on_systems_ticked") Events.connect("entity_placed", self, "_on_entity_placed") Events.connect("entity_removed", self, "_on_entity_removed") ## Calls `work()` for every entity in the workers list. func _on_systems_ticked(delta: float) -> void: for worker in workers.keys(): workers[worker].work(delta) ## Places the given entity's work component in the workers list if it is a ## worker entity. func _on_entity_placed(entity, cellv: Vector2) -> void: if entity.is_in_group(Types.WORKERS): workers[cellv] = _get_work_component_from(entity) ## Removes the given entity from the workers list when erased from the entity ## tracker. func _on_entity_removed(_entity, cellv: Vector2) -> void: var _erased := workers.erase(cellv) ## Gets the first node that is a `WorkComponent` from the entity's children. func _get_work_component_from(entity) -> WorkComponent: for child in entity.get_children(): if child is WorkComponent: return child return null
Unlike the power system, we don’t have to worry about building a wire path with power sources and receivers. We need a list of entities with the WorkComponent
to trigger their work()
function once every simulation update.
Doing so keeps the system lightweight.
In the script above, we introduced a new group, so we need to head to Types.gd
to add it to the list.
const WORKERS := "workers"
We need to create a WorkSystem
object in the simulation for its code to run. Head to Simulation.gd
to create it.
onready var _work_system := WorkSystem.new()
Now that it’s instanced, WorkSystem
will connect to the Events
autoload’s systems_ticked
signal, which gets emitted through Simulation
’s _on_Timer_timeout()
signal callback.
In the next lesson, we’ll create a furnace that takes fuel and smelts ore into ingots to use our shiny new work system.
WorkComponent.gd
class_name WorkComponent extends Node signal work_accomplished(amount) signal work_done(output) signal work_enabled_changed(enabled) var current_recipe: Dictionary var current_output: BlueprintEntity var available_work := 0.0 var work_speed := 0.0 var is_enabled := false setget _set_is_enabled func setup_work(inputs: Dictionary, recipe_map: Dictionary) -> bool: for output in recipe_map.keys(): if not Library.blueprints.has(output): continue var can_craft := true var recipe_inputs: Array = recipe_map[output].inputs.keys() for input in inputs.keys(): if not input in recipe_inputs or inputs[input] < recipe_map[output].inputs[input]: can_craft = false break if can_craft: current_recipe = recipe_map[output] current_output = Library.blueprints[output].instance() current_output.stack_count = current_recipe.amount available_work = current_recipe.time return true return false func work(delta: float) -> void: if is_enabled and available_work > 0.0: var work_done := delta * work_speed available_work -= work_done emit_signal("work_accomplished", work_done) if available_work <= 0.0: emit_signal("work_done", current_output) func _set_is_enabled(value: bool) -> void: if is_enabled != value: emit_signal("work_enabled_changed", value) is_enabled = value
WorkSystem.gd
class_name WorkSystem extends Reference var workers := {} func _init() -> void: Events.connect("systems_ticked", self, "_on_systems_ticked") Events.connect("entity_placed", self, "_on_entity_placed") Events.connect("entity_removed", self, "_on_entity_removed") func _on_systems_ticked(delta: float) -> void: for worker in workers.keys(): workers[worker].work(delta) func _on_entity_placed(entity, cellv: Vector2) -> void: if entity.is_in_group(Types.WORKERS): workers[cellv] = _get_work_component_from(entity) func _on_entity_removed(_entity, cellv: Vector2) -> void: var _erased := workers.erase(cellv) func _get_work_component_from(entity) -> WorkComponent: for child in entity.get_children(): if child is WorkComponent: return child return null
We added one constant to Types.gd
.
const WORKERS := "workers"
And we created a WorkSystem
instance in Simulation.gd
.
onready var _work_system := WorkSystem.new()