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
Improbable.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
Improbable.Worker.OpList
to aDispatcher
, in order to invoke callbacks.
If you provide a positive timeout (in milliseconds) to
GetOpList
,
the function will block until there is at least one operation to return, or until the timeout has been exceeded.
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 simple example implementation of an event loop that processes operations from SpatialOS 60 times per second:
const int FramesPerSecond = 60;
private static void RunEventLoop(Connection connection, Dispatcher dispatcher)
{
var maxWait = System.TimeSpan.FromMilliseconds(1000f / FramesPerSecond);
var stopwatch = new System.Diagnostics.Stopwatch();
while (true)
{
stopwatch.Reset();
stopwatch.Start();
var opList = connection.GetOpList(0 /* non-blocking */);
// Invoke callbacks.
dispatcher.Process(opList);
// Do other work here...
stopwatch.Stop();
var waitFor = maxWait.Subtract(stopwatch.Elapsed);
System.Threading.Thread.Sleep(waitFor.Milliseconds > 0 ? waitFor : System.TimeSpan.Zero);
}
}
Dispatcher callbacks
To respond to operations, register callbacks on the
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:
private static System.Collections.Generic.List<ulong> RegisterCallbacks(Connection connection, Dispatcher dispatcher)
{
var callbackKeys = new System.Collections.Generic.List<ulong>
{
dispatcher.OnAddEntity(op =>
{
// Do something with op.EntityId.
}),
dispatcher.OnComponentUpdate<Switch>(op =>
{
var update = op.Update.Get();
foreach (var toggleEvent in update.toggled)
{
System.Console.WriteLine("Switch has been toggled at {0}.", toggleEvent.time);
}
})
};
return callbackKeys;
}
Unregistering callbacks
To unregister a callback, call
Remove()
with the ulong
returned
from the registration method:
private static void UnregisterCallbacks(Dispatcher dispatcher, System.Collections.Generic.List<ulong> callbackKeys)
{
foreach (var key in callbackKeys)
{
dispatcher.Remove(key);
}
}
Dispatcher callbacks reference
Dispatcher callback | 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. |
DisconnectOp |
OnFlagUpdate |
A worker flag has been created or deleted, or its value has changed. | FlagUpdateOp |
OnLogMessage |
The SDK issues a log message for the worker to print. This does not include messages sent using Connection.SendLogMessage . |
LogMessageOp |
OnMetrics |
The SDK reports built-in internal metrics. | MetricsOp |
OnCriticalSection |
A critical section is about to be entered or has just been left. | CriticalSectionOp |
OnAddEntity |
An entity is added to the worker’s view of the simulation. | AddEntityOp |
OnRemoveEntity |
An entity is removed from the worker’s view of the simulation. | RemoveEntityOp |
OnReserveEntityIdResponse |
The worker receives a response for an entity ID reservation it requested. | ReserveEntityIdResponseOp |
OnReserveEntityIdsResponse |
The worker has receives a response for the reservation of an entity ID range it requested. | ReserveEntityIdsResponseOp |
OnCreateEntityResponse |
The worker receives a response for an entity creation it requested. | CreateEntityResponseOp |
OnDeleteEntityResponse |
The worker receives a response for an entity deletion it requested. | DeleteEntityResponseOp |
OnEntityQueryResponse |
The worker receives a response for an entity query. | EntityQueryResponseOp |
OnAddComponent<C> |
A component is added to an entity in the worker’s view of the simulation. | AddComponentOp<C> |
OnRemoveComponent<C> |
A component is removed from an entity in the worker’s view of the simulation. | RemoveComponentOp |
OnAuthorityChange<C> |
The worker’s write access authority state over a component changes (to Authoritative, Not Authoritative or Authority Loss Imminent). | AuthorityChangeOp |
OnComponentUpdate<C> |
A component on an entity in the worker’s view of the simulation is updated. | ComponentUpdateOp<C> |
OnCommandRequest<C> |
The worker receives a command request for a component on an entity over which it has write access authority. | CommandRequestOp<C> |
OnCommandResponse<C> |
The worker receives a command response for a command request. | CommandResponseOp<C> |
Dispatcher invocation order
OpList
objects that the
Dispatcher
receives are
handled in the order these objects are received over the connection. The next object in the OpList
is handled only after the previous object has been processed. For a given object in the OpList
,
user-defined callbacks are processed in the order in which they were registered with the
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 construction and destruction order found in, for example, C++.
For example:
AddEntityOp
andAddComponentOp
callbacks are invoked in the order they were registered with theDispatcher
.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
callbacks 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 View
.
Using the View
Improbable.Worker.View
is a subclass of
Improbable.Worker.Dispatcher
, 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
Improbable.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.
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
Connection.GetWorkerFlag()
method:
const double DefaultWorkSpeed = 10;
private static void GetSpeedWorkerFlag(Connection connection)
{
var speedFlag = connection.GetWorkerFlag("mycompany_theproject_speed");
if (speedFlag.HasValue)
{
SetWorkSpeed(System.Convert.ToDouble(speedFlag.Value));
}
else
{
SetWorkSpeed(DefaultWorkSpeed);
}
}
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.