Component best practices

GDK for Unreal

If you're using the GDK for Unreal, this page does not apply to you. For information about components, see the SpatialOS component glossary entry in the GDK documentation.

This page gives guidance on the best ways to use components. This will help reduce your game's bandwidth and help your game scale.

When to use events

You should use an event when you want to broadcast information between workers about a transient occurrence (something that has happened which does not need to be persisted).

This information will only be sent to the other workers which have active read access to the same entity component.

Examples of what makes a good event

If you wanted to visualize a player shooting a gun on all of your client-worker instances, you would use an event. The client-worker instance that has write access authority over a component on the player entity could trigger the GunFired event, and all the other client-worker instances that have active read access to a component on this player entity receive updates about this event and play a muzzle flash animation when it is triggered.

An event is appropriate here instead of a property because no persistent state (state which is required after this event has happened) has been changed on the player that is shooting the gun. Metadata used for the visualization, such as bullet type, can also be included in the event.

If you wanted players to be able to communicate using emotes (displays of emotion), events would be suitable to trigger different animations on the player.

You would want to use an event instead of a property because the emote does not need to be persisted. There is no state that needs to be changed on the player.

When to use commands

You should use a command when you want to communicate with the worker that has write access authority over one specific entity.

The command can tell the entity to do something, transmit data, or anything else, but the key point is that it is only to one specified entity.

Commands are always sent to the worker instance that has write access authority over the command's component on the target entity. This means that when the worker receives the command, it has write access authority over the command’s component, so it can make changes to the properties of that component. (If the worker loses write access authority while the command is being sent, the command will respond with a failure.)

Therefore, properties that change in the command's handler should be in the same component as the command.

As an example of what that would look like, say you had entities that could be set on fire:

type Void {}

component Ignitable {
    id = 1001;
    bool is_on_fire = 1;
    command Void ignite(Void);

is_on_fire and ignite should be in the same component, because when the ignite command is received, the worker will have write access authority over the Ignitable component and can therefore
set is_on_fire to true.

When using command short-circuiting, the worker responding to the command may report that the short-circuited command succeeded even if it just lost authority over that component.

See the documentation on commands for more details.

Properties that change atomically

When you change a component, you specify which properties have changed, and then send the update in a ComponentUpdate operation. The operation will contain all of the properties that have changed.

Therefore, if you need to make an atomic change to an entity, where multiple properties need to be changed at the same time, you must send these properties in the same ComponentUpdate operation. You must therefore put these properties in the same component.

Optimizing bandwidth

The vast majority of worker communication comes from keeping the state of the entities synchronized across workers. This takes the form of ComponentUpdate operations. Therefore, it is important to reduce the bandwidth used by component updates if you want to optimize your game.

There are two main ways to do this: reduce the rate of component updates and reduce the size of each component update.

You can use metrics to measure the bandwidth being used by your game.

1. Reducing the rate of component updates

Only send component updates when you need to

If a component's data is only needed by the worker instance that has write access authority,
you don't necessarily need to send every update to SpatialOS. Instead, you can just change the data locally and send the up-to-date component at a low frequency, for example, every few seconds. You can also use the AuthorityLossImminent state to send all of the component updates when the worker is about to lose write access authority.

For example, an NPC might have a large NPCStateMachine component, with many AI-related properties. Other workers that have active read access to a component on this entity do not need to know about these properties or receive updates whenever the properties change, but the data must be persisted when the NPC crosses a worker boundary or when a snapshot is taken.

Sending the up-to-date NPCStateMachine component every few seconds, instead of whenever it changes, means that in the case of a worker failure or if a snapshot is taken, SpatialOS contains data that is no older then a few seconds.

You can also use the AuthorityLossImminent state to determine if the worker is about to lose authority of the NPCStateMachine component. If the worker is about to lose write access authority, you should keep the NPCStateMachine component synchronized with SpatialOS by sending every property change in a component update. This ensures that SpatialOS can send the most up-to-date data to the worker which gains write access authority.

You should not solely rely on sending component updates when authority loss is imminent, as the component updates may be received by SpatialOS after the worker actually loses write access authority.

Update components as infrequently as possible

Typically, there are a few components which will make up most of the component updates.
These are often components that encode transform properties such as the position
and rotation of the characters in your game.

There are techniques you can use to require less frequent transform updates, for example:

Client-side interpolation

Interpolate the position and rotation of the characters between component updates.

Client-side prediction
When you interpolate between component updates, you need to visualize your characters with a delay. To avoid this, you can instead predict the current position and rotation of your characters based on the previous component updates.

When a new component update comes in, you can then correct your prediction. The effectiveness of this technique will depend on how accurate your predictions are.

Both of these techniques allow for smooth movement on the client-worker instances with a lower frequency of transform updates.

These techniques should also be used on other non-authoritative managed workers which require smooth movement, such as physics workers.

Limit the components that each worker instance has interest in

A worker instance gets updates from the SpatialOS Runtime about any entity components that it has active read access to. There are two prerequisites for active read access:

By default, a workers is sent all of the component updates for every component it has interest in. Therefore, one way to reduce the number of component update operations is to make sure every worker instance has interest in only the components that it needs to know about.

For example, you may have an NPCStateMachine component on each NPC responsible for storing the information used by the NPC's state machine. Client-worker instances do not need to know this information, so to prevent this information being sent to every client-worker instance, you need to make sure client-worker instances don't have interest in the NPCStateMachine component.

You can configure which worker types have interest in which components using the static component filter. This is the component_delivery field of the worker type's bridge configuration.

A worker instance can also tell SpatialOS that it has interest (or no longer has interest) in a component during runtime using the dynamic component filter (see the API documentation for C++/C#/Java.

Merging components

Every time you update a component, a ComponentUpdate operation is sent to SpatialOS. This is then propagated to all other workers that have interest in this component.

This means that ideally, properties that change together should be in the same components.

If for example, you had

component NPCRotation {
    id = 1002;
    Quaternion rotation = 1;

component NPCPosition {
    id = 1003;
    improbable.Coordinates position = 1;

Then periodically, you update the position and rotation of each NPC:

SendComponentUpdate(NPCRotation.rotation, npc.rotation);
SendComponentUpdate(NPCPosition.position, npc.position);

This will send two component updates, each with an overhead, to SpatialOS. Instead, if you
merge these components into one, you can use one component update instead:

component NPCTransform {
    id = 1002;
    Quaternion rotation = 1;
    improbable.Coordinates position = 2;

2. Reducing the size of each component update

Component updates contain the properties of the component that have been changed. Therefore, to reduce the size of each component update, you need to reduce the size in bytes of the component's properties.

For example, say each character in your game has a Rotation component, with the rotation encoded as a Quaternion:

type Quaternion {
    double x = 1;
    double y = 2;
    double z = 3;
    double w = 4;

component Rotation {
    id = 1002;

    // 32 bytes
    Quaternion rotation = 1;

With this implementation, each rotation value in the component updates will contain 4 doubles, which has a size of 32 bytes.

Now it may be the case that in your game, characters can only rotate around one axis, for example, the y-axis. In this case, you can just transmit the y Euler angle. This can be encoded and decoded into a Quaternion locally by each worker. You can also make this a float instead of a double, as you may not need very high accuracy.

The Rotation component now becomes:

component Rotation {
    id = 1002;

    // 4 bytes
    float y_rotation = 1;

Now, each rotation value in the component updates contains only 1 float, which has a size of 4 bytes.

Protobuf encoding

We can do better than 4 bytes though.

Component properties get encoded into a protobuf message, which is then sent to and from
SpatialOS. In a protobuf message, every value a float or double takes will have a size of 4 or 8 bytes respectively. For integers, however, the smaller the integer value, the less bits the integer will use.

When using uint32 or uint64, smaller values will use fewer bits.

When using int32 or int64, negative numbers will use the maximum number of bits.

When using sint32 or sint64, smaller absolute values will use fewer bits.

When using fixed32, fixed64, sfixed32 or sfixed64, all values will use 4 or 8 bytes.

This is more space-efficient when values are likely to be very large.

See the schemalang primitive types for more details.

Therefore, if you represent your Euler angle as a variable size integer, the value will be encoded using fewer bytes. You can choose the accuracy that you want, for example, 1 integer value representing 0.1 degrees. This, therefore, means that our integer value will range from 0 to 3600. Protobuf can then encode this value as a variable size integer, using 1 or 2 bytes.

component Rotation {
    id = 1002;

    // 1 or 2 bytes
    uint32 y_rotation_x10 = 1;

Custom transform component

As you've seen above, you can use techniques such as integer quantisation to reduce the size
of rotation and position components. If you are updating these at a high frequency, it is important to optimize the encoding for these as much as possible. You have also seen that properties that change together, such as position and rotation, should be in the same component.

Therefore, it is important to create and use your own custom, game-specific, transform component for high-frequency transform updates instead of the built-in Position component, which is 24 bytes.

However, SpatialOS uses the Position component for tasks such as load balancing your game and updating the inspector. Therefore, it is important to still keep this component updated at a low frequency, for example, every two seconds.

Redundant properties

Removing redundant properties is another way to reduce the size of component updates. Often, there are properties that can be inferred from other properties, or the local state of the entity.

Take this simple example:

component PlayerInfo {
    id = 1001;

    Date birthday = 1;
    int32 age = 2;

The player's age can be inferred from the other properties, therefore you should not include it in the component.

This example sounds trivial, but often there are properties such as an explicit state machine state that can be inferred from other data.

Updated about a year ago

Component best practices

Suggested Edits are limited on API Reference Pages

You can only suggest edits to Markdown body content, but not to the API spec.