Handling data received from SpatialOS
Processing operations
It is up to you to decide how the basic event loop of receiving data from SpatialOS should be structured, depending on the worker’s requirements.
The worker needs to:
- Call
worker::Connection::GetOpList
to get a list of “operations” (for example entity creation, or a component update) that have been sent since the last time the function was called. - Pass the
worker::OpList
to aworker::Dispatcher
, in order to invoke callbacks.
If the connection fails (when
Connection.IsConnected() == false
),
any error messages explaining the cause
will be returned by GetOpList()
.
Make sure to process this list in order to receive all available diagnostic information.
Example of processing operations
The following snippet shows a (very) simple example implementation of an event loop that processes operations from SpatialOS 60 times per second:
void RunEventLoop(worker::Connection& connection, const worker::Dispatcher& dispatcher) {
constexpr unsigned kFramesPerSecond = 60;
constexpr std::chrono::duration<double> kFramePeriodSeconds(
1. / static_cast<double>(kFramesPerSecond));
while (true) {
auto start_time = std::chrono::steady_clock::now();
auto op_list = connection.GetOpList(0 /* non-blocking */);
// Invoke user-provided callbacks.
dispatcher.Process(op_list);
// Do other work here...
auto stop_time = std::chrono::steady_clock::now();
auto wait_for = kFramePeriodSeconds - (stop_time - start_time);
std::this_thread::sleep_for(wait_for);
}
}
Dispatcher callbacks
To respond to operations, register callbacks on the
worker::Dispatcher
. Each method takes an
arbitrary callable System.Action<Param>
, where the parameter type Param
depends on the particular
kind of callback being registered.
Timing and threading
Caveats about receiving component updates
Registering callbacks
Here’s an example of registering callbacks:
void RegisterLambdaCallbacks(worker::Dispatcher& dispatcher) {
dispatcher.OnAddEntity([&](const worker::AddEntityOp& op) {
std::cout << "Entity " << op.EntityId << " added." << std::endl;
});
dispatcher.OnComponentUpdate<example::Switch>(
[&](const worker::ComponentUpdateOp<example::Switch>& op) {
// `op.Update.toggled()` contains a list of SwitchToggled events.
for (auto it : op.Update.toggled()) {
std::cout << "Switch toggled at " << it.time() << std::endl;
}
});
}
Of course, you don’t have to use lambdas:
void RegisterCallbacks(worker::Dispatcher& dispatcher) {
dispatcher.OnComponentUpdate<improbable::Position>(&PositionUpdated);
}
void PositionUpdated(const worker::ComponentUpdateOp<improbable::Position>& op) {
const auto coords = op.Update.coords().data();
std::cout << "Updated position of entity " << op.EntityId << " to "
<< " x: " << coords->x() << " y: " << coords->y() << " z: " << coords->z() << std::endl;
}
Unregistering callbacks
To unregister a callback, call Remove()
with the
Dispatcher::CallbackKey
returned from
the registration method:
void UnregisterCallbacks(worker::Dispatcher& dispatcher) {
auto key = dispatcher.OnComponentUpdate<improbable::Position>(
[&](const worker::ComponentUpdateOp<improbable::Position>& op) {
std::cout << "Updated position of entity " << op.EntityId << std::endl;
});
// ...
dispatcher.Remove(key);
}
Dispatcher callbacks reference
Some callbacks use a template parameter T
which relates to schema-generated code. This is
discussed in more detail on the Generated code page.
Method | Invoked when… | Parameter type |
---|---|---|
OnDisconnect |
The Connection is no longer connected and can no longer be used. Check the log for errors relating to the disconnection. |
worker::DisconnectOp |
OnFlagUpdate |
A worker flag has been created, deleted or when its value has changed. | worker::FlagUpdateOp |
OnLogMessage |
The SDK issues a log message for the worker to print. This does not include messages sent using Connection.SendLogMessage . |
worker::LogMessageOp |
OnMetrics |
The SDK reports built-in internal metrics. | worker::MetricsOp |
OnCriticalSection |
A critical section is about to be entered or has just been left. | worker::CriticalSectionOp |
OnAddEntity |
An entity is added to the worker’s view of the simulation. | worker::AddEntityOp |
OnRemoveEntity |
An entity is removed from the worker’s view of the simulation. | worker::RemoveEntityOp |
OnReserveEntityIdResponse |
The worker receives a response for an entity ID reservation it requested. | worker::ReserveEntityIdResponseOp |
OnReserveEntityIdsResponse |
The worker receives a response for the reservation of an entity ID range it requested. | worker::ReserveEntityIdsResponseOp |
OnCreateEntityResponse |
The worker receives a response for an entity creation it requested. | worker::CreateEntityResponseOp |
OnDeleteEntityResponse |
The worker receives a response for an entity deletion it requested. | worker::DeleteEntityResponseOp |
OnEntityQueryResponse |
The worker receives a response for an entity query. | worker::EntityQueryResponseOp |
OnAddComponent<T> |
A component is added to an entity in the worker’s view of the simulation. | worker::AddComponentOp<T> |
OnRemoveComponent<T> |
A component is removed from an entity in the worker’s view of the simulation. | worker::RemoveComponentOp |
OnAuthorityChange<T> |
The worker’s authority state over a component changes (to Authoritative, Not Authoritative or Authority Loss Imminent). | worker::AuthorityChangeOp |
OnComponentUpdate<T> |
A component on an entity in the worker’s view of the simulation is updated. | worker::ComponentUpdateOp<T> |
OnCommandRequest<T> |
The worker receives a command request for a component on an entity over which it has write access authority. | worker::CommandRequestOp<T> |
OnCommandResponse<T> |
The worker receives a command response for a command request. | worker::CommandResponseOp<T> |
Dispatcher invocation order
worker::OpList
objects that the
worker::Dispatcher
receives are handled in
the order these object are received over the connection. The next object in the OpList
is handled
only after the previous object is processed. For a given object in the OpList
, user-defined
callbacks are processed in the order in which they were registered with the worker::Dispatcher
.
The exception to this rule are the callbacks pertaining to operations that come in “start” and “end” pairs. When an “end” operation is received, callbacks are processed in the reverse order of their registration. This means that pairs of callbacks registered for the “start” and “end” operations can correctly depend on resources managed by pairs of callbacks registered previously, similar to the usual C++ construction and destruction order.
For example:
AddEntityOp
andAddComponentOp
callbacks are invoked in the order they were registered with theworker::Dispatcher
.RemoveEntityOp
andRemoveComponentOp
callbacks are invoked in reverse order.AuthorityChangeOp
callbacks are invoked in the order they were registered when write access authority is granted, but in reverse order when write access authority is revoked (or authority loss is imminent).CriticalSectionOp
s are invoked in usual order when a critical section is entered but reverse order when a critical section is left.FlagUpdateOp
callbacks are invoked in the order they were registered when a worker flag is created or its value is changed, but in reverse order when a worker flag is deleted.
In particular, this invocation order has the side-effect that the final state of a component removed
in a RemoveComponentOp
callback can
still be inspected in the worker::View
.
Using the View
worker::View
is subclass of
worker::Dispatcher
defined in a separate header file,
<improbable/view.h>
, which automatically
maintains the current state of the worker’s view of the simulated world.
It has a field called
Entities
, which is a map from entity ID to
worker::Entity
objects. When a
View
processes an
OpList
, it automatically updates the state of this
map as appropriate. Any user-defined callbacks are also invoked, as per the
Dispatcher
contract.
Note that the View
can be implemented from scratch
using the worker::ForEachComponent
functionality, and is intended mostly as an example.
Critical sections
The protocol supports the concept of critical sections. A critical section is a block of operations that should be processed atomically by the worker. For example, a set of entities representing a stack of cubes should be added to the scene in its entirety before any physics simulation is done.
This notion of a critical section does not provide any guarantees.
The CriticalSectionOp
callback is
invoked with InCriticalSection == true
just before the first operation of the critical section is delivered,
and is invoked again with InCriticalSection == false
just after the last operation of the critical section is delivered.
Worker flags
To access the value of a worker flag, use the
GetWorkerFlag
method:
void SetWorkSpeedFlag(worker::Connection& connection) {
auto work_speed_flag = connection.GetWorkerFlag("mycompany_theproject_work_speed");
if (work_speed_flag) {
SetWorkSpeed(*work_speed_flag);
} else {
SetWorkSpeed("default_work_speed");
}
}
Worker flags are updated before the registered callbacks are invoked, so it is recommended to store the worker flag values in user code when using callbacks and only use GetWorkerFlag for checking the initial flag values.