Get SpatialOS

Sites

Menu
You are viewing the docs for 11.0, an old version of SpatialOS. 12.0 is the newest →

Using the C++ worker SDK

Overview

The most important units of functionality in the C++ worker API are:

  • The worker::Locator, which is responsible for querying for and connecting to remote cloud deployments.
  • The worker::Connection, which represents a worker’s connection to a SpatialOS simulated world.
  • The worker::Dispatcher, which is responsible for invoking user-provided callbacks based on data received from SpatialOS via the worker::Connection.
  • The worker::Entity, which is a container for the component data associated with a single entity.
  • The worker::View, which is an optional drop-in replacement for the worker::Dispatcher that additionally maintains a map from entity ID to worker::Entity.

For the full API reference, have a look at <improbable/worker.h>.

Connecting to SpatialOS

Before it can interact with the simulated world, the worker must connect to SpatialOS.

There are two ways of doing this:

  • Instantiate a worker::Connection object directly. This allows to connect a managed worker (i.e running in the cloud and started by the SpatialOS runtime) or a remote worker used for debugging via ([spatial cloud connect external](/reference/11.0/tools/spatial/spatial-cloud-connect-external)). The connection is made through the receptionist service of the targeted deployment for which the IP and port should be passed as arguments to the worker. These values can be filled-in automatically at runtime by using the IMPROBABLE_RECEPTIONIST_HOST and IMPROBABLE_RECEPTIONIST_PORT placeholders in your worker’s launch configuration.
  • Use the worker::Locator object to enumerate cloud deployments and connect to a chosen deployment with authentication. This is typically used to connect an external client to a cloud deployment.

The value given for the UseExternalIp field in the NetworkParameters structure is also relevant. The table below summarizes the connection possibilities:

Using worker::Connection directly Using worker::Locator
UseExternalIp == true Local client connecting via spatial cloud connect external proxy External client connecting to cloud deployment
UseExternalIp == false Managed cloud worker; local client connecting to local deployment N/A

The example below illustrates a very basic connection setup, where the worker takes three command-line arguments specifying the worker’s own ID as well as the receptionist’s IP and port. It will use TCP and connect using the internal IP address.

int main(int argc, char** argv) {
  if (argc != 4) {
    return 1;
  }

  worker::ConnectionParameters parameters;
  parameters.WorkerType = "MyCPlusPlusWorker";
  parameters.Network.ConnectionType = worker::NetworkConnectionType::kTcp;
  parameters.Network.UseExternalIp = false;

  const std::string workerId = argv[1];
  const std::string hostname = argv[2];
  const std::uint16_t port = static_cast<std::uint16_t>(std::stoi(argv[3]));

  worker::Connection connection = worker::Connection::ConnectAsync(hostname, port, workerId, parameters).Get();

  ...
}

Note that worker::Connection objects are not thread-safe.

Handling data received from SpatialOS

It is up to each worker to decide how the basic event loop should be structured, depending on its requirements. The worker should call worker::Connection::GetOpList to get a list of “operations” (for example a new entity, or a component update) from SpatialOS that have been sent since the last time the function was called. This worker::OpList then needs to be passed to a dispatcher in order to invoke user-provided callbacks.

In order to detect that a worker is still responsive, SpatialOS sends periodic heartbeat requests. Responding to these happens automatically, but the buffer for incoming operations can fill up with other messages. If that happens, the heartbeats cannot be processed and your worker may be killed. Therefore, you should never wait multiple seconds between calling GetOpList.

The following snippet shows a (very) simple example implementation of an event loop that processes operations from SpatialOS 60 times per second:

#include <improbable/worker.h>
#include <chrono>
#include <thread>

void RunEventLoop(worker::Connection& connection, worker::Dispatcher& dispatcher) {
  static const unsigned kFramesPerSecond = 60;
  static const std::chrono::duration<double> kFramePeriodSeconds(1. / static_cast<double>(kFramesPerSecond));

  auto time = std::chrono::high_resolution_clock::now();
  while (true) {
    auto op_list = connection.GetOpList(0 /* non-blocking */);

    // Invoke user-provided callbacks.
    dispatcher.Process(op_list);

    // Do other work here...

    time += kFramePeriodSeconds;
    std::this_thread::sleep_until(time);
  }
}

Note that all callbacks provided to the worker::Dispatcher will be invoked only when worker::Dispatcher::Process is called, and only on the thread that is currently calling worker::Dispatcher::Process: the user has complete control over callback timing and threading decisions.

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.

Dispatcher Callbacks

Several kinds of callbacks can be registered on the worker::Dispatcher. Each method takes an arbitrary callable std::function<void(const Param&)>, where the parameter type Param depends on the particular kind of callback being registered. The following table has the details:

Method Parameter type (and fields) Invoked when…
OnDisconnect worker::DisconnectOp (std::string Reason) the Connection is no longer connected and can no longer be used. Check the log for errors relating to the disconnection.
OnLogMessage worker::LogMessageOp (worker::LogLevel Level, std::string Message) the SDK issues a log message for the worker to print. This does not include messages sent using Connection.SendLogMessage.
OnMetrics worker::MetricsOp (worker::Metrics Metrics) the SDK reports built-in internal metrics.
OnCriticalSection worker::CriticalSectionOp (bool InCriticalSection) a critical section is about to be entered or has just been left.
OnAddEntity worker::AddEntityOp (worker::EntityId EntityId) an entity is added to the worker’s view of the simulation.
OnRemoveEntity worker::RemoveEntityOp (worker::EntityId EntityId) an entity is removed from the worker’s view of the simulation.
OnReserveEntityIdResponse worker::ReserveEntityIdResponseOp (worker::RequestId<worker::ReserveEntityIdRequest> RequestId, worker::StatusCode StatusCode, std::string Message, Option<worker::EntityId> EntityId) the worker has received a response for an entity ID reservation it had requested previously.
OnCreateEntityResponse worker::CreateEntityResponseOp (worker::RequestId<worker::CreateEntityRequest> RequestId, worker::StatusCode StatusCode, std::string Message, Option<worker::EntityId> EntityId) the worker has received a response for an entity creation it had requested previously.
OnDeleteEntityResponse worker::DeleteEntityResponseOp (worker::RequestId<DeleteEntityRequest> RequestId, worker::EntityId EntityId, worker::StatusCode StatusCode, std::string Message) the worker has received a response for an entity deletion it had requested previously.
OnEntityQueryResponse worker::EntityQueryResponseOp (worker::RequestId<EntityQueryRequest> RequestId, worker::StatusCode StatusCode, std::string Message, std::size_t ResultCount, worker::Map<worker::EntityId, worker::Entity> Result>) the worker has received a response for an entity query it had requested previously.
OnAddComponent<T> worker::AddComponentOp<T> (worker::EntityId EntityId, T::Data Data) a component is added to an existing entity in the worker’s view of the simulation.
OnRemoveComponent<T> worker::RemoveComponentOp (worker::EntityId EntityId) a component is removed from an existing entity in the worker’s view of the simulation.
OnAuthorityChange<T> worker::AuthorityChangeOp (worker::EntityId EntityId, bool HasAuthority) the worker is granted authority over an entity’s component, or the worker’s authority over an entity’s component is revoked.
OnComponentUpdate<T> worker::ComponentUpdateOp<T> (worker::EntityId EntityId, T::Update Update) a component for an entity in the worker’s view of the simulation has been updated.
OnCommandRequest<T> worker::CommandRequestOp<T> (worker::RequestId<IncomingCommandRequest<T>> RequestId, worker::EntityId EntityId, std::uint32_t TimeoutMillis, std::string CallerWorkerId, List<std::string> CallerAttributeSet, T::Request Request) the worker has received a command request for a component on an entity over which it has authority.
OnCommandResponse<T> worker::CommandResponseOp<T> (worker::RequestId<OutgoingCommandRequest<T>> RequestId, worker::EntityId EntityId, worker::StatusCode StatusCode, Option<T::Response> Response) the worker has received a command response for a request it issued previously.

(Note that some callbacks use a template parameter T which relates to schema-generated code. This is discussed in more detail below.)

Here’s an example of registering callbacks:

void RegisterCallbacks(worker::Dispatcher& dispatcher) {
  dispatcher.OnAddEntity([&](const worker::AddEntityOp& op) {
    // Do something with op.EntityId
  });
}

Of course, you don’t have to use lambdas:

void PositionUpated(const worker::ComponentUpdateOp<Position>& op) {
  // Do something with op.EntityId and op.Position
}

void RegisterCallbacks(worker::Dispatcher& dispatcher) {
  dispatcher.OnComponentUpdate<Position>(&PositionUpdated);
}

If you want to unregister a callback, call Remove() with the Dispatcher::CallbackKey returned from the registration method:

void RegisterAndUnregisterCallbacks(worker::Dispatcher& dispatcher) {
  auto key = dispatcher.OnRemoveEntity(&EntityRemoved);

  // ...

  dispatcher.Remove(key);
}

Using the View

worker::View is subclass of worker::Dispatcher, which adds the functionality of automatically maintaining 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.

Dispatcher invocation order

AddEntityOp callbacks are invoked in the order they were registered with the worker::Dispatcher, while RemoveEntityOp callbacks are invoked in reverse order. This means that pairs of add/remove callbacks can correctly depend on resources managed by pairs of callbacks registered earlier. This is similar to the usual C++ construction/destruction order. Similarly, AuthorityChangeOp callbacks are invoked in the order they were registered when authority is granted, but in reverse order when authority is revoked.

In particular, this invocation order has the side-effect that the final state of an entity removed in a RemoveEntityOp callback can still be inspected in the worker::View.

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.

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 of a Connection:

Option<std::string> GetWorkerFlag(const std::string& flag_name) const;

For example:

auto work_speed_flag = connection->GetWorkerFlag("mycompany_theproject_work_speed");
if (work_speed_flag) {
  SetWorkSpeed(*work_speed_flag);
} else {
  SetDefaultWorkSpeed();
}

Sending data to SpatialOS

A worker can send data such as logging, metrics, and component updates to SpatialOS using the worker::Connection.

Logging and metrics

The worker::Connection has two methods to send diagnostic information to SpatialOS: SendLogMessage, which is used for distributed logging, and SendMetrics, which updates timeseries and histogram data associated with this worker. Logs and metrics are available on deployment dashboards.

The Connection periodically reports several built-in internal metrics using worker::MetricsOp, which you would usually want to send to SpatialOS along with your custom metrics. It might look something like this:

worker::Metrics metrics;
metrics.GaugeMetrics["MyCustomMetric"] = 1.0;
dispatcher.OnMetrics([&](const worker::MetricsOp& op) {
  // Fill in the SDK built-in metrics.
  metrics.Merge(op.Metrics);
  connection.SendMetrics(metrics);
});

The worker::Metrics structure should also be used to set the worker load when using dynamic load balancing for a worker. The load of a worker is a floating-point value, with 0 indicating an unloaded worker and values above 1 corresponding to an overloaded worker. The reported values direct SpatialOS’s load balancing strategy. An example of load reporting could be:

worker::Metrics metrics;
std::queue<Task> taskQueue;
double kMaximumQueueSize = 200;  // An arbitrary maximum value.

dispatcher.OnMetrics([&](const worker::MetricsOp& op) {
  // Update the current load of the worker
  metrics.Load = (static_cast<double>(taskQueue.size()) / kMaximumQueueSize);

  // Combine with the SDK built-in metrics and send to SpatialOS.
  metrics.Merge(op.Metrics);
  connection.SendMetrics(metrics);
});

// Main-event loop
while (true) {
  // Process tasks from the taskQueue.
}

Sending and receiving component updates

When the worker has authority over a particular component for some entity, it can send component updates to SpatialOS. This is done using the worker::Connection method SendComponentUpdate<T> which takes a reference to a T::Update object. The template parameter T should be component metaclass in the schema-generated code. These are discussed further below.

Component updates sent by the worker will appear in the operation list returned by a subsequent call to worker::Connection::GetOpList. This means that ComponentUpdateOp callbacks will also be invoked for component updates triggered by the worker itself, but not immediately.

There are several (related) reasons that callbacks for sent component updates should not be invoked immediately: this can lead to extremely unintuitive control flow when components are recursively updated inside a callback; and it violates the guarantee that callbacks are only invoked as a result of a call to Process method on the worker::Dispatcher.

To be notified when a worker receives a component update on an entity in the worker’s local view of the simulation, use the worker::Dispatcher method OnComponentUpdate with the same template paramater T as for sending updates. Note that the component updates can be partial, i.e. only update some properties of the component, do not necessarily have to contain data that is different from the workers current view of the component, and could have been sent by SpatialOS rather than a worker for synchronization purposes.

Sending and receiving component events

Sending and receiving events works much in the same way as component updates. For a schema like the following,

type SwitchToggled {
  int64 time = 1;
}

component Switch {
  id = 1234;
  bool is_enabled = 1;
  event SwitchToggled toggled;
}

triggering an event can be done as follows:

example::Switch::Update update;
example::SwitchToggled event(1);
update.add_toggled(event);
connection->SendComponentUpdate<example::Switch>(entity_id, update);

If you are not authoritative on the component, your event will be silently ignored.

Receiving an event works just like receiving a component update, by registering a callback on the dispatcher:

dispatcher.OnComponentUpdate<example::Switch>(
  [&](const worker::ComponentUpdateOp<example::Switch>& op) {
    const example::Switch::Update& update = op.Update;
    // `update.toggled()` contains a list of SwitchToggled events.
  });

Sending component interests

For each entity in its view, a worker receives updates for a set of the entity’s components. This set is the union of the set of components the worker has authority over, and a set of components it is explicitly interested in.

The initial set of explicitly interested components can be configured in the bridge settings. This is the default set of explicit interest for every entity. At runtime, the worker can override this set on a per-entity, per-component basis, by using the Connection::SendComponentInterest method.

Whenever the set of interested components for an entity changes, the worker will receive the appropriate OnAddComponent and OnRemoveComponent callbacks to reflect this change.

For example, you might be interested in the local player’s inventory, but not the inventories of other players within the world:

dispatcher.OnAuthorityChange<PlayerControls>([&](const worker::AuthorityChangeOp& op) {
  // If we have authority over this player's controls component, then the entity represents the local player.
  if (op.HasAuthority) {
    connection::SendComponentInterest(
        op.EntityId,
        {{Inventory::ComponentId, worker::InterestOverride{/* IsInterested */ true}}});
  }
});

Note that a worker is always considered to be interested in the components of the entities it is authoritative on, in addition to any others specificed in the bridge settings or using the method described above.

Sending and receiving component commands

To send a command to be executed by the worker currently authoritative over a particular component for a particular entity, use the worker::Connection method SendCommandRequest<T>. In this case the template parameter T should be a command metaclass defined in the schema-generated code. This function takes an entity ID, an optional timeout, a T::Request object and an optional CommandParameters object. The CommandParameters object contains a field called AllowShortCircuiting, which, if set to true, will try to “short-circuit” the command and avoid a round trip to SpatialOS in some cases. See documentation on commands for more information.

Before sending the command, a callback to handle the response should be registered with the worker::Dispatcher with OnCommandResponse<T>. The worker::RequestId<worker::OutgoingCommandRequest<T>> returned by SendCommandRequest can be matched up with the one in the worker::CommandResponseOp to identify the request that is being responded to.

Note that commands may fail, so the worker::StatusCode field in the worker::CommandResponseOp should be checked, and the command can be retried as necessary. The caller will always get a response callback, but it can be one of several failure cases, including:

  • ApplicationError (rejected by the target worker or by SpatialOS)
  • AuthorityLost (target worker lost authority, or no worker had authority)
  • NotFound (target entity, or target component on the entity, didn’t exist)
  • PermissionDenied (sending worker didn’t have permission to send request)
  • Timeout
  • InternalError (most likely indicates a bug in SpatialOS, should be reported)

To handle commands issued by another worker, the opposite flow is used. Register a callback with the worker::Dispatcher with OnCommandRequest<T>; when the callback is executed, the worker should make sure to call the worker::Connection method SendCommandResponse<T>, supplying the worker::RequestId<worker::IncomingCommandRequest<T>> provided by the worker::CommandRequestOp and an appropriate T::Response response object. Alternatively, the worker can call SendCommandFailure<T> to fail the command instead.

Note that a RequestId only uniquely identifies a single command invocation from the perspective of a single worker; that is, the RequestId used by the calling worker and the RequestId used by the caller worker for handling that same command are unrelated.

Entity queries

Note: In order to send an entity query, a worker must have permission to do so. For more information, see the Worker permissions page.

A worker can run remote entity queries against the simulation by using the worker::Connection method SendEntityQueryRequest. This takes a worker::query::EntityQuery object and an optional timeout.

The query object is made of an worker::query::Constraint and a worker::query::ResultType. The constraint determines which entities are matched by the query, and the result type determines what data is returned for matched entities. Available constraints and result types are described below.

Constraint Description
EntityIdConstraint Matches a specific entity ID.
ComponentConstraint Matches entities with a particular component.
SphereConstraint Matches entities contained in the given sphere.
AndConstraint Matches entities that match all of the given sub-constraints.
OrConstraint Matches entities that match any of the given sub-constraints.
Result type Description
CountResultType Returns the number of entities that matched the query.
SnapshotResultType Returns a snapshot of component data for entities that matched the query. To select all components, use worker::query::SnapshotResultType snapshotResultType{{}};. To select every component whose ID is contained in the given set, use worker::query::SnapshotResultType snapshotResultType{{componentIdSet}}; (thus, pass an empty set to get no components but entity IDs only).

Important: You should keep entity queries as limited as possible. All queries hit the network and cause a runtime lookup, which is expensive even in the best cases. This means you should:

  • always limit queries to a specific sphere of the world
  • only return the information you need from queries (eg the specific components you care about)
  • if you’re looking for entities that are within your worker’s checkout radius, search internally on the worker instead of using a query

Like other request methods, this returns a worker::RequestId, which can be used to match a request with its response. The response is received via a callback registered with the worker::Dispatcher using the OnEntityQueryResponse method.

The EntityQueryResponseOp contains a std::size_t ResultCount field (for CountResultType requests) and a Map<EntityId, Entity> (for SnapshotResultType) requests. Again, success or failure of the request is indicated by the StatusCode field of the response object, but in the failure case the result may still contain some data: the count or snapshot map might still contain the data for some entities that matched the query, but won’t necessarily contain all matching entities. This is because the worker might still be able to do something useful with a partial result.

Creating and deleting entities

Note: In order to create or delete an entity, a worker must have permission to do so. For more information, see the Worker permissions page.

A worker can request SpatialOS to reserve an entity ID, create an entity, or delete an entity, by using the worker::Connection methods SendReserveEntityIdRequest, SendCreateEntityRequest, and SendDeleteEntityRequest, respectively.

These methods return a worker::RequestId, which can be used to match a request with its response. The response is received via a callback registered with the worker::Dispatcher using OnReserveEntityIdResponse, OnCreateEntityResponse, and OnDeleteEntityResponse, respectively.

Note that these operations may fail, so the StatusCode field in the ReserveEntityIdResponseOp, CreateEntityResponseOp, or DeleteEntityResponseOp argument of the respective callback should be checked, and the command can be retried as necessary. The caller will always get a response callback, but it might be a timeout.

SendReserveEntityIdRequest takes an optional timeout. If the operation succeeds, the response contains an entity ID, which is guaranteed to be unused in the current deployment.

SendCreateEntityRequest takes a worker::Entity representing the initial state of the entity, an optional entity ID (which, if provided, must have been obtained by a previous call to SendReserveEntityIdRequest), and an optional timeout. If the operation succeeds, the response contains the ID of the newly created entity.

SendDeleteEntityRequest takes an entity ID and an optional timeout. The response contains no additional data.

Example

Here’s an example of reserving an entity ID, creating an entity with that ID and some initial state, and finally deleting it:

const std::uint32_t kTimeoutMillis = 500;

// Reserve an entity ID.
worker::RequestId<worker::ReserveEntityIdRequest> entity_id_reservation_request_id
    = connection->SendReserveEntityIdRequest(kTimeoutMillis);

// When the reservation succeeds, create an entity with the reserved ID.
worker::RequestId<worker::CreateEntityRequest> entity_creation_request_id;
dispatcher.OnReserveEntityIdResponse([&](const worker::ReserveEntityIdResponseOp& op) {
  if (op.RequestId == entity_id_reservation_request_id && op.StatusCode == worker::StatusCode::kSuccess) {
    // ID reservation was successful - create an entity with the reserved ID.
    worker::Entity entity;
    // Empty ACL - should be customised.
    entity.Add<EntityAcl>({{{}}, {}});
    // Needed for the entity to be persisted in snapshots.
    entity.Add<Persistence>({});
    entity.Add<Position>({{1, 2, 3}});
    entity_creation_request_id = connection->SendCreateEntityRequest(entity, *op.EntityId, kTimeoutMillis);
  }
});

// When the creation succeeds, delete the entity.
worker::RequestId<worker::DeleteEntityRequest> entity_deletion_request_id;
dispatcher.OnCreateEntityResponse([&](const worker::CreateEntityResponseOp& op) {
  if (op.RequestId == entity_creation_request_id && op.StatusCode == worker::StatusCode::kSuccess) {
    entity_deletion_request_id = connection->SendDeleteEntityRequest(*op.EntityId, kTimeoutMillis);
  }
});

// When the deletion succeeds, we're done.
dispatcher.OnDeleteEntityResponse([&](const worker::DeleteEntityResponseOp& op) {
  if (op.RequestId == entity_deletion_request_id && op.StatusCode == worker::StatusCode::kSuccess) {
    // Test successful!
  }
});

Entity ACLs

Entity ACLs are exposed to the worker as a component, and can be manipulated as any other component. ACLs can be set at entity creation time, or modified dynamically at runtime by the worker that has authority over the EntityAcl component.

This example adds an EntityAcl to an Entity given a CommandRequestOp, which is currently the only way to make a specific worker (as opposed to, potentially, a set of workers) to qualify for authority of a component. Specifically:

  • The entity will be visible to workers that have the “client” or “physics” worker attribute.
  • Any “physics” worker (i.e. a worker with “physics” as one of its attributes) can be authoritative over the entity’s Position and EntityAcl components.
  • The worker, which issued the CommandRequestOp, is the only worker that can be authoritative over the PlayerControls component.

This can be used as part of creating a new entity in response to a command request.

template <typename C>
void AddComponentDelegations(const worker::CommandRequestOp<C>& op, worker::Entity& entity) {
  // This requirement set matches only the command caller, i.e. the worker that issued the command,
  // since attribute set includes the caller's unique attribute.
  improbable::WorkerAttributeSet callerWorkerAttributeSet{op.CallerAttributeSet};
  improbable::WorkerRequirementSet callerWorkerRequirementSet{{callerWorkerAttributeSet}};

  // This requirement set matches any worker with the attribute "physics".
  improbable::WorkerRequirementSet physicsWorkerRequirementSet{
      worker::List<improbable::WorkerAttributeSet>{
          {worker::List<std::string>{"physics"}}}};

  // This requirement set matches any worker with the attribute "client" or "physics".
  auto clientOrPhysicsRequirementSet = improbable::WorkerRequirementSet{{
      improbable::WorkerAttributeSet{{"client"}},
      improbable::WorkerAttributeSet{{"physics"}}
  }};

  // Give authority over Position and EntityAcl to any physics worker, and over PlayerControls to
  // the caller worker.
  worker::Map<worker::ComponentId, improbable::WorkerRequirementSet> componentWriteAcl = {
      {Position::ComponentId, physicsWorkerRequirementSet},
      {EntityAcl::ComponentId, physicsWorkerRequirementSet},
      {PlayerControls::ComponentId, callerWorkerRequirementSet}
  };

  entity.Add<EntityAcl>(
      EntityAcl::Data{/* read */ clientOrPhysicsRequirementSet, /* write */ componentWriteAcl});
}

The worker authoritative over the EntityAcl component can later decide to give the authority over position (or any other component) to a different worker, e.g. the client. In order to do this, EntityAclData needs to be modified in order to map Position::ComponentId to callerWorkerRequirementSet (created above). This change can be made using the method below:

// Take a copy of the EntityAclData argument, so that this method doesn't modify the original one.
EntityAclData DelegateComponent(EntityAclData acl, worker::ComponentId componentId,
                                const improbable::WorkerRequirementSet& requirementSet) {
  // Set the write ACL for the specified component to the specified attribute set,
  // assuming the componentAcl option is not empty.
  acl.component_write_acl().emplace(componentId, requirementSet);
  return acl;
}

For these changes to take effect, the worker authoritative over the EntityAcl component needs send the changes through a component update.

Please read Worker attributes and worker requirements for more information.

Generated code

The C++ schema-generated code consists of two main parts: data classes and component classes. Data classes are used to represent data at rest and correspond to the schemalang type definitions; component classes correspond to schemalang component definitions and contain metadata and classes for sending and receiving component data (updates, command requests and command responses).

A data class will be generated for each type defined in the schema, with fields corresponding to each field in the schema definition.

For each component, we generate:

  • a metaclass. These metaclasses are used when referring to specific components when using the API - every template parameter T expects a metaclass argument.
  • an Update class nested inside the metaclass. This has an optional field for each field in the component (since it represents a diff).
  • command Request and Response classes nested inside the metaclass. These have an optional field for each command defined by the component (using the request type and response type of the command respectively); however, only one field can be set at a time. Note that the behaviour of responding to a command request using the response field for a different command is undefined.

Data representation

  • Strings are represented as UTF-8 encoded std::string members. Make sure to use this same encoding when updating a component.
  • list<T> fields are represented as a worker::List<T> of the repeated type. This type is very similar to std::vector<T> and has most of the methods you’d expect.
  • map<Key, Value> fields are represented as worker::Map<Key, Value>. This type is very similar to std::unordered_map<Key, Value> and has most of the methods you’d expect. The biggest difference is it has a well-defined iteration order, which is insertion order.
  • option<T> fields are represented as worker::Option<T>. First, check if the option contains a value with !option.empty() or if (option) { ... }, then dereference the option to access the contained value. This type is very similar to the std::optional<T> type introduced in C++17.

Example

Consider the following simple schema:

package example;

type StatusEffect {
  string name = 1;
  int32 multiplier = 2;
}

type EntityStatus {
  int32 health = 1;
  list<StatusEffect> effects = 2;
}

component Status {
  id = 12345;
  data EntityStatus;
}

Here is the interface that will be generated for this schema (roughly - it has been simplified for clarity):

namespace example {

// The data class for the StatusEffect message.
class StatusEffect {
public:
  StatusEffect(const string& name, int32_t multiplier);

  const string& name() const;
  string& name();
  StatusEffect& set_name(const string&);

  int32_t multiplier() const;
  int32_t& multiplier();
  EntityStatus& set_multiplier(int32_t);
};

// The data class for the EntityStatus message.
class EntityStatus {
public:
  EntityStatus(int32_t health, const Range<const StatusEffect>& status_effect);

  int32_t health() const;
  int32_t& health();
  EntityStatus& set_health(int32_t);

  const List<StatusEffect>& effects() const;
  List<StatusEffect>& effects();
  EntityStatus& set_effects(const List<StatusEffect>&);
};

// The metaclass for the status component.
struct Status {
  using Data = EntityStatus;

  // The update class for the status component.
  class Update {
    const Option<int32_t>& health() const;
    Option<int32_t>& health();
    Update& set_health(int32_t);

    const Option<List<StatusEffect>>& effects() const;
    Option<List<StatusEffect>>& effects();
    Update& set_effects(const List<StatusEffect>&);
  };
};

}

Note that we have data classes EntityStatus and StatusEffect corresponding to the two types defined in the schema, and also a Status metaclass containing a Status::Update class for the status component. The generated code uses some helper types like Range which are defined in <improbable/schema_lib.h>.

The Status metaclass can be passed as a template parameter to any templated API function, for example to send an update for the Status component. Note that the Status metaclass is also used with the worker::Entity class (which the View uses to represent entities) to inspect component data “at rest”.

Here are some ways that these classes can be used with the API:

#include <improbable/worker.h>
#include <schema/example.h>

using namespace worker;
using namespace example;

void Examples(Connection& connection) {
  Dispatcher dispatcher;

  dispatcher.OnAuthorityChange<Status>([&](const AuthorityChangeOp& op) {
    if (op.HasAuthority) {
      // We were granted authority over the status component. Send an update.
      Status::Update update;
      update.set_health(10);
      connection.SendComponentUpdate<Status>(op.EntityId, update);
    } else {
      // Authority was revoked.
    }
  });

  dispatcher.OnComponentUpdate<Status>([&](const ComponentUpdateOp<Status>& op) {
    const Status::Update& update = op.Update;
    if (update.has_effects()) {
      // The `effects` field was updated.
    }
  });
}

Snapshots

The SDK provides two methods to manipulate snapshots stored in files:

/** Load a snapshot from a file. Returns an error message if an error occured. */
Option<std::string> LoadSnapshot(const std::string& path,
                                 std::unordered_map<EntityId, Entity>& entities_output);

/** Saves a snapshot to a file. Returns an error message if an error occured. */
Option<std::string> SaveSnapshot(const std::string& path,
                                 const std::unordered_map<EntityId, Entity>& entities);

Note that, unlike the rest of the API described in this document, snapshot manipulation does not require a Connection, making it possible to develop standalone, offline snapshot manipulation tools. However, we recommend using the build infrastructure provided by SpatialOS for workers to build your standalone tools.

Here is an example of loading a snapshot, performing some manipulation on it, and saving it back. It uses the types and components defined in the example above.

void AddLowHealthEffectToEntities(const std::string& snapshot_filename) {
  // Load a snapshot from a file.
  std::unordered_map<worker::EntityId, worker::Entity> entities;
  auto load_error_opt = worker::LoadSnapshot(snapshot_filename, entities);
  if (load_error_opt) {
    std::cerr << "Error loading snapshot: " << *load_error_opt << std::endl;
    return;
  }

  // Add the "LowHealth" effect to all entities that have a Status component and less than 10 health points.
  for (const auto& pair : entities) {
    auto entity = pair.second;
    auto status = entity.Get<Status>();
    if (status && status->Health() < 10) {
      status->Effects().append({"LowHealth", 100});
    }
  }

  // Save the snapshot back to the file.
  auto save_error_opt = worker::SaveSnapshot(snapshot_filename, entities);
  if (save_error_opt) {
    std::cerr << "Error saving snapshot: " << *save_error_opt << std::endl;
    return;
  }
}

Currently, the generated code for every component present in the snapshot must be linked into the binary that operates on it. Generated code for components that are present in the snapshot, but that your code doesn’t refer to, may be optimized away by the linker.

This generally is not a problem when writing tools designed to create seed snapshots from which to start a new deployment, since such tools must explicitly create each entity and component in the snapshot.

For migration tools which are intended to modify an existing snapshot, it might be a problem, since components not explicitly mentioned by the code itself might not be recognised. This could trigger error messages reporting these components as unknown. An easy workaround is to add code similar to the following to a program experiencing these errors, to force the generated code to be linked:

worker::Entity entity;
entity.Get<UnreferencedComponent>();

Otherwise, the set of active components can be set explicitly before loading the snapshot using the worker::SetComponentMetaclasses function.

Was this page helpful?

Thanks for letting us know!

Thanks for your feedback

Need more help? Ask on the forums