Processes are a useful tool to work around the strict definition of a system and
introduce logic in a different way, usually without resorting to other component
types.
EnTT offers minimal support to this paradigm by introducing a few classes used
to define and execute cooperative processes.
A typical task inherits from the process class template. Derived classes also
specify what the intended type for elapsed times is.
A process should implement the following member functions whether needed (note that it is not required to define a function unless the derived class wants to override the default behavior):
void update(Delta, void *);This is invoked once per tick until a process is explicitly aborted or ends
either with or without errors. Each process should at least define it to work
properly. The void * parameter is an opaque pointer to user data (if any)
forwarded directly to the process during an update.
void succeeded();This is invoked in case of success, immediately after an update and during the same tick.
void failed();This is invoked in case of errors, immediately after an update and during the same tick.
void aborted();This is invoked only if a process is explicitly aborted. There is no guarantee that it executes in the same tick. It depends solely on whether the process is aborted immediately or not.
A class can also change the state of a process by invoking succeed and fail,
as well as pause and unpause the process itself.
All these are public member functions made available to manage the life cycle of
a process easily.
Here is a minimal example for the sake of curiosity:
struct my_process: entt::process {
using delta_type = typename entt::process::delta_type;
my_process(delta_type delay)
: remaining{delay}
{}
void update(delta_type delta, void *) {
remaining -= std::min(remaining, delta);
// ...
if(!remaining) {
succeed();
}
}
private:
delta_type remaining;
};
Lambdas and functors cannot be used directly with a scheduler because they are
not properly defined processes with managed life cycles.
This class helps in filling the gap and turning lambdas and functors into
full-featured processes usable by a scheduler.
Function call operators have signatures similar to that of the update member
function of a process, except that they receive a reference to the handle to
manage its lifecycle as needed:
void(entt::process &handle, delta_type delta, void *data);
Parameters have the following meaning:
handle is a reference to the process handle itself.delta is the elapsed time.data is an opaque pointer to user data if any, nullptr otherwise.The library also provides the process_from function to simplify the creation
of processes starting from lambdas or the like.
TBD (TODO)
A cooperative scheduler runs different processes and helps manage their life cycles.
Each process is invoked once per tick. If it terminates, it is removed
automatically from the scheduler, and it is never invoked again. Otherwise,
it is a good candidate to run one more time the next tick.
A process can also have a child. In this case, the parent process is replaced
with its child when it terminates and only if it returns with success. In case
of errors, both the parent process and its child are discarded. This way, it is
easy to create a chain of processes to run sequentially.
Using a scheduler is straightforward. To create it, users must provide only the type for the elapsed times and no arguments at all:
entt::basic_scheduler<std::uint64_t> scheduler;
Otherwise, the scheduler alias is also available for the most common cases. It
uses std::uint32_t as a default type:
entt::scheduler scheduler;
The class has member functions to query its internal data structures, like
empty or size, as well as a clear utility to reset it to a clean state:
// checks if there are processes still running
const auto empty = scheduler.empty();
// gets the number of processes still running
entt::scheduler::size_type size = scheduler.size();
// resets the scheduler to its initial state and discards all the processes
scheduler.clear();
To attach a process to a scheduler, there are mainly two ways:
If the process inherits from the process class template, it is enough to
indicate its type and submit all the parameters required to construct it to
the attach member function:
scheduler.attach<my_process>(1000u);
Otherwise, in case of a lambda or a functor, it is enough to provide an
instance of the class to the attach member function:
scheduler.attach([](auto...){ /* ... */ });
In both cases, the scheduler is returned and its then member function can be
used to create chains of processes to run sequentially.
As a minimal example of use:
// schedules a task in the form of a lambda function
scheduler.attach([](auto delta, void *, auto succeed, auto fail) {
// ...
})
// appends a child in the form of another lambda function
.then([](auto delta, void *, auto succeed, auto fail) {
// ...
})
// appends a child in the form of a process class
.then<my_process>(1000u);
To update a scheduler and therefore all its processes, the update member
function is the way to go:
// updates all the processes, no user data are provided
scheduler.update(delta);
// updates all the processes and provides them with custom data
scheduler.update(delta, &data);
In addition to these functions, the scheduler offers an abort member function
that is used to discard all the running processes at once:
// aborts all the processes abruptly ...
scheduler.abort(true);
// ... or gracefully during the next tick
scheduler.abort();