cubos::core::ecs::Schedule class

Stores schedule nodes and the restrictions that must be met for them to run.

In short, the algorithm implemented here is based on a graph of dependencies. There are three types of nodes:

  • system nodes.
  • condition nodes;
  • repeat nodes;

Additionally, there are three types of constraints which can be applied to the flow:

  • ordering constraints, e.g., node 1 runs before node 2 - specified through order.
  • only if constraints, e.g., node 1 runs if condition node 2 returns true - specified through onlyIf.
  • repeat constraints, e.g., node 1 is part of repeat node 2 - specified during node creation time.

For a more in-depth and accurate analysis of the behavior of this class, take a look at its unit tests.

Execution

Each node stores its 'needed satisfaction', which represents how many nodes must mark it as 'satisfied' for it to be illegible to run. During execution, nodes keep the current satisfaction level as state. When that level reaches the level needed to run, the node is pushed to the execution queue.

System nodes are the most basic of the node types. When they execute, their respective system is run. All other nodes which have order constraints depending on it have their satisfaction increased.

Condition nodes which evaluate to true mark all dependents as satisfied. If they evaluate to false, they mark the dependents of the dependents as satisfied, effectively skipping these nodes.

Repeat nodes are the most complex of the nodes. Each node stores the identifier of the repeat node it belongs to (which may be none). All nodes belonging to one have a base satisfaction level of 1. When the repeat node evaluates to true, this level is increased, allowing their execution to start. Additionally, the repeat node's satisfaction is decreased by the number of belonging nodes. When each of those nodes finishes, it increments the satisfaction level of the repeat node. When all nodes are finished, the repeat node runs once again.

If a repeat node evaluates to false, it marks as satisfied all of its dependents, as only then it has finished.

The execution stops when the queue is emptied.

Constraint Specification

Any two nodes may constrained to run in a given order in relation to each other. If two nodes are part of different repeat nodes, the ordering is applied for their lowest ancestor repeat nodes which have the same parent repeat node. This means, that, if, for example, you have two systems A and B, each part of their own repeat node, if you force A to run before B, then A's repeat node will entirely run before B's repeat node. This is necessary as the number of times a repeat node executes is unknown - it may vary depending on the condition.

When only if constraints are applied between a node and a condition, the condition always runs before the system, but the system only runs if the condition evaluates to true. Only if constraints are only allowed when the condition doesn't repeat from the point of view of the node. More formally, the condition must be part of the node's repeat node or one of its ancestors.

Any operation which would introduce a cycle fails by returning false and logging an error.

Concurrency

This algorithm makes it easy to run systems concurrently. To do this, we just have to distribute the work between threads each time a node is popped from the queue. One thing to keep in mind is that this class by itself doesn't solve node conflicts. If two nodes write to the same resource, for example, and if they aren't ordered, an arbitrary order constraint should be added between them, to ensure that they won't run concurrently.

Public types

struct NodeId
Identifies a node in the schedule.

Public functions

void clear()
Removes all nodes from the schedule.
auto repeat(ConditionId conditionId, const memory::Opt<NodeId>& repeatId = {}) -> memory::Opt<NodeId>
Adds a repeat node to the schedule which repeats as long as the given condition evaluates to true.
auto system(SystemId systemId, const memory::Opt<NodeId>& repeatId = {}) -> memory::Opt<NodeId>
Adds a system node to the schedule.
auto condition(ConditionId conditionId, const memory::Opt<NodeId>& repeatId = {}) -> memory::Opt<NodeId>
Adds a condition node to the schedule.
auto onlyIf(NodeId node, NodeId condition) -> bool
Specifies that a node should only run if a given condition node returns true after finishing.
auto order(NodeId before, NodeId after) -> bool
Specifies that a node must run after another node finishes.
void run(SystemRegistry& registry, SystemContext& context)
Runs the systems in the schedule.
auto debug(const SystemRegistry& registry) const -> std::string
Generates a multi-line string which represents the order in which the nodes will run.

Function documentation

void cubos::core::ecs::Schedule::clear()

Removes all nodes from the schedule.

All previously returned node identifiers must no longer be used after this is called.

memory::Opt<NodeId> cubos::core::ecs::Schedule::repeat(ConditionId conditionId, const memory::Opt<NodeId>& repeatId = {})

Adds a repeat node to the schedule which repeats as long as the given condition evaluates to true.

Parameters
conditionId Condition identifier.
repeatId Repeat node to become part of, if any.
Returns Repeat node identifier, or nothing if the given repeat node is not a repeat node.

memory::Opt<NodeId> cubos::core::ecs::Schedule::system(SystemId systemId, const memory::Opt<NodeId>& repeatId = {})

Adds a system node to the schedule.

Parameters
systemId System identifier.
repeatId Repeat node to become part of, if any.
Returns System node identifier, or nothing if the given repeat node is not a repeat node.

memory::Opt<NodeId> cubos::core::ecs::Schedule::condition(ConditionId conditionId, const memory::Opt<NodeId>& repeatId = {})

Adds a condition node to the schedule.

Parameters
conditionId Condition identifier.
repeatId Repeat node to become part of, if any.
Returns Condition node identifier, or nothing if the given repeat node is not a repeat node.

bool cubos::core::ecs::Schedule::onlyIf(NodeId node, NodeId condition)

Specifies that a node should only run if a given condition node returns true after finishing.

Parameters
node Node.
condition Condition.
Returns Whether the operation was successful.

Implicitly enforces an ordering constraint, such that the condition always finishes before the node starts. Fails by returning false if the given condition node isn't a condition node.

bool cubos::core::ecs::Schedule::order(NodeId before, NodeId after)

Specifies that a node must run after another node finishes.

Parameters
before Node that must finish.
after Node that must run after before finishes.
Returns Whether the restriction was successfully added.

If doing so would introduce a dependency cycle in the schedule, the operation is cancelled and the method returns false.

void cubos::core::ecs::Schedule::run(SystemRegistry& registry, SystemContext& context)

Runs the systems in the schedule.

Parameters
registry Registry containing the systems.
context Context to run the systems with.

std::string cubos::core::ecs::Schedule::debug(const SystemRegistry& registry) const

Generates a multi-line string which represents the order in which the nodes will run.

Parameters
registry Registry containing the systems, used to get the system names.
Returns String.

The obtained order is not necessarily the one in which the nodes will run. The only guarantee given, is that if an ordering constraint was specified, then this string will reflect it. Thus, if a system A appears before a system B, then there isn't a constraint 'B must run before A'.