Bridge configuration
The bridge
field of the worker configuration file
specifies the worker type’s runtime settings. It has the following overall structure:
"bridge": {
"worker_attribute_set": {},
"entity_interest": {},
"streaming_query": [],
"component_delivery": {},
"component_settings": {}
}
Worker attribute sets
Required field.
A worker attribute set describes which layer a worker type belongs to (in other words, what it’s responsible for simulating). It’s used to work out which components the worker type’s instances can have access to, and it must be a single string. See the worker attribute sets page for details.
For example, a UnityWorker would have the following attribute set:
"worker_attribute_set": {
"attributes": ["physics"]
}
In addition to the attribute set defined here, each worker instance automatically has the attribute
"workerId:<worker ID>"
. For example, the worker instance MyCSharpWorker12
would have the attribute
"workerId:MyCSharpWorker12"
added to it.
Entity interest
Required field.
The entity interest field is about defining which entities a worker instance of this type gets component updates for.
If a worker instance is authoritative over an entity, it gets an update whenever that entity’s components change. But it also needs to know about the entities nearby.
By default, worker instances get component updates for all entities they’re authoritative over, as well as for all other entities in the same chunk (the grid squares of a SpatialOS world). But chunks are square, and so this doesn’t quite work.
You can use the entity interest field to set a radius (in world units, see note below). This is a radius around entities that a worker instance is authoritative over. If there’s another chunk with any point inside this radius, the worker instance will get component updates for entities inside that chunk.
For example, to make sure a worker instance gets updates from entities within 10 world units of any entity the
worker instance has write access to, use the following entity_interest
setting:
"entity_interest": {
"range_entity_interest": {
"radius": 10
}
}
Streaming queries
Optional field.
What streaming queries are
SpatialOS is based on the idea that, usually, worker instances only need to know about in the entities they’re authoritative over, plus entities that are nearby (ie within the entity interest radius). For example, a player might need to see entities up to 50m away from them, but not any further away.
But sometimes, worker instances need information about other entities. For example, entities that are far away but should be visible (such as mountains), or entities for global communication that aren’t physically located anywhere (for example, a global weather system). To allow a worker instance to receive updates to these entities, you can use streaming queries.
Entity queries or streaming queries?
If you want information about an entity, but you don’t need regular updates about it, use an entity query instead.
If you need regular updates about an entity, it’s better to use streaming queries: they return information periodically, and they only return the new information about a component, rather than all the information about it.
Query-based interest
How streaming queries work
Using streaming queries is similar to checking out. If you set up a streaming query for a component, for entities with that component, the worker instance gets a local representation of an entity, and receives updates about it. Effectively, to the worker instance, it looks like the entity is within its checkout area.
The worker instance gets updates to this entity’s components at specific time intervals. You can configure this (see below).
Limitations
Streaming queries have some limitations:
- The worker instance doesn’t receive all of the updates for each component of an entity:
- It only gets updates at the time interval you set (see below).
- It doesn’t receive any event updates.
- You can only set the time interval globally for all streaming queries within a deployment.
- You can’t change streaming queries at runtime.
- You have to specify components to query, rather than entities.
Setting up a streaming query
First, you need to pick a component. By default, for every entity with that component, a streaming query will return updates to all components on that entity. However, it’s good practice to improve efficiency and bandwidth use by narrowing down which updates are returned.
To do this, specify:
- which components (on the entities returned by the streaming query) you want updates for
- a radius (around entities that a worker instance is authoritative over) that an entity must be within
For example, the following snippet will send this worker instance:
- updates to the components
your.game.Status
,your.game.Health
, andyour.game.Flammable
for all entities with the componentyour.game.GloballyVisible
- updates to all components from all entities with the component
your.game.HighlyVisible
, within a radius of 1000 world units around an entity that the worker instance is authoritative over
"streaming_query": [
{
"global_component_streaming_query": {
"component_name": "your.game.GloballyVisible"
},
"components_streamed": {
"specific_components": {
"components": [
"improbable.Position"
"your.game.Status",
"your.game.Health",
"your.game.Flammable"
]
}
}
},
{
"bounded_component_streaming_query": {
"component_name": "your.game.HighlyVisible",
"radius": 1000
}
}
]
Configuring the time interval
To configure the time interval between streaming queries, set streaming_query_interval
in your
launch configuration file.
The default time interval is 4 seconds.
Component delivery
Optional field.
This field configures the set of components a worker instance checks out when they check out an entity, and whether component updates are allowed to be sent in an unreliable manner.
By default:
- only components with
checkout_initially
set totrue
are sent to the worker instance - the transmission for all component updates is
RELIABLE_ORDERED
Query-based interest
Components to check out initially
To specify the set of components a worker instance will get when they check out an entity, use the following
fields of component_delivery
:
checkoutAllInitially
(boolean). By default, this is set tofalse
, which means only the components withcheckout_initially
set to true are sent to the worker instance when it checks out an entity.If set to
true
, all components are sent to the worker instance when it checks out an entity.override
maps fully-qualified component names to component delivery settings.Components without an override use the default message delivery settings as specified in the
default
field. They are only checked out initially ifcheckoutAllInitially
is true.
The 4 permutations of these settings for a particular component C therefore lead to the following behaviour:
checkoutAllInitially
isfalse
or isn’t specified and thecheckout_initially
override isfalse
or isn’t specified for C - C is not checked outcheckoutAllInitially
istrue
and thecheckout_initially
override isfalse
or isn’t specified for C - C is checked outcheckoutAllInitially
isfalse
or isn’t specified and thecheckout_initially
override istrue
for C - C is checked outcheckoutAllInitially
istrue
and thecheckout_initially
override istrue
for C - C is checked out
The worker instance can also change the set of components it is interested in at runtime - see the API documentation (C++, C#, Java) for more details.
Transmitting component updates unreliably (“quality of service”)
Component updates can be transmitted reliably or unreliably.
To prevent worker instances getting overloaded, you can choose to transmit some component updates unreliably. Quality of service describes the concept in more detail.
The default
field of component_delivery
sets the default way to transmit component updates. This
can be either RELIABLE_ORDERED
or UNRELIABLE_ORDERED
.
This configuration applies only to messages sent to worker instances, not for messages sent from worker instances.
Examples
This example reliably transmits updates for all components
except Position
and Rotation
, and mandates that the position component is checked out initially:
"component_delivery": {
"default": "RELIABLE_ORDERED",
"override": {
"improbable.Position": {
"checkout_initially": true,
"message_delivery": "UNRELIABLE_ORDERED"
},
"mygame.coordinates.Rotation": {
"message_delivery": "UNRELIABLE_ORDERED"
}
}
}
This example mandates that all components are checked out initially (using checkoutAllInitially
).
Setting checkoutAllInitially
to true
takes precedence over the individual components’ checkout_initially
settings, so we don’t specify them:
"component_delivery": {
"default": "RELIABLE_ORDERED",
"checkoutAllInitially": true,
"override": {
"improbable.Position": {
"message_delivery": "UNRELIABLE_ORDERED"
},
"mygame.coordinates.Rotation": {
"message_delivery": "UNRELIABLE_ORDERED"
}
}
}
Note that the initial checkouts of mygame.coordinates.Position
and mygame.coordinates.Rotation
components are not affected by the UNRELIABLE_ORDERED
message delivery setting. Only subsequent updates will be unreliable.
Component settings
The component settings field allows you to configure various component settings. For now this allows you to set the authority handover timeout period. For more information, see Handing over authority between workers.
The component settings field allows you to configure settings for each individual component or configure a default which is applied to all other components. Below is an example of a typical configuration.
"componentSettings": {
"perComponentSettings": {
"improbable.MyFancyComponent": {
"authorityHandoverTimeoutMs": 300
}
},
"default": {
"authorityHandoverTimeoutMs": 400
}
}
In the example above, improbable.MyFancyComponent
will have an authorityHandoverTimeoutMs
of
300 and all other components will default to 400.
If no default is set, components will default to having an authorityHandoverTimeoutMs
of 0.
Information about what the authorityHandoverTimeoutMs
does can be found in
Handing over authority between workers.
Delta-compressed component updates
This lets you configure which components you want to delta-compress when they are updated. If you’re sending large component updates, you might want to enable delta-compressed component updates for a component. This means that, instead of sending the entire new state, the component update will be sent as a description of how the state has changed.
This is a tradeoff: it saves bandwidth at the cost of CPU overhead and latency.
By default, no worker types have delta compression applied to component updates.
How delta compression works
Delta compression works using diff caches on both the SpatialOS Runtime and worker instances that store the last component state sent and received for a given component. Updates are sent as a binary diff across the network which, for some property types, can have large bandwidth savings.
This works best for small changes to larger property types, e.g. when appending an element to a long list, as opposed to sending the whole list again.
Usage
You enable delta compression on a per-worker type, per-component basis. For example, to turn on delta-compressed component updates for improbable.ants.ToNest
and improbable.ants.ToFood
you would need to modify your worker.json like so:
"component_settings": {
"per_component_settings": {
"improbable.ants.ToNest": {
"encoding": {
"type": "BINARY_DELTA_COMPRESSION"
},
},
"improbable.ants.ToFood": {
"encoding": {
"type": "BINARY_DELTA_COMPRESSION"
},
}
}
}
Best practices
You should only delta-compress component properties that are large with small, infrequent changes, e.g. appending an item to a long list as opposed to changing all items at once. For example, any of the following:
- list
- map
- string
- large object types contained within components
We recommend you don’t enable delta compression on:
- any of the above types that may change dramatically, for example a List that would have half of the contents replaced.
- smaller types, such as single int fields, due to the overhead of calculating the diff.
You can enable delta compression for other component types, but diffs may be abandoned because:
- the computed diff actually exceeds the size of the update.
- the process hits the approximate cutoff time (default 5ms - we’re looking into making this configurable).
- the process detects before diffing starts that one of the above cases is highly likely to occur.
Diffs that aren’t abandoned can still use CPU on the SpatialOS Runtime or worker instance side to compute.
We would strongly recommend you closely monitor behaviour and metrics (e.g. SpatialOS Runtime CPU usage and latency) after enabling this functionality, as it can cause worker instances to stop responding to pings.
Metrics
You can measure delta compression through worker instance metrics via the Inspector. Use this to confirm delta compression is working as intended, and get insight into the bandwidth you’re saving and your CPU/lag overhead due to diffs abandoned.