Get SpatialOS

Sites

Menu

Component best practices

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 also have the same entity checked out, and are interested in the same component.

Examples of what makes a good 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.

Another situation where you would use an event is if you wanted to visualise a player shooting a gun on all of your clients. The client authoritative on the player could trigger the GunFired event, and all the other clients that have this player checked out can listen to 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 has been changed on the player that is shooting the gun. Metadata used for the visualisation, such as bullet type, can also be included in the event.

When to use commands

You should use a command when you want to communicate with 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 which is authoritative over the command’s component on the target entity. This means that when the worker receives the command, it will have write authority over the command’s component, so it can make changes to the properties of that component. (If the worker loses authority while the command is being sent, the command will respond with a failure.)

Therefore, it is a good idea to keep the properties that change in the command’s handler and the command itself in the same component.

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 authority on 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 despite the fact that updates sent by the command handler were dropped by SpatialOS, since the worker could have lost write access before the updates were delivered.

See the short-circuiting caveats 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.

Optimising bandwidth

The vast majority of worker communication comes from keeping the state of the entities synchronised 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 optimise your game.

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

1. Reducing the rate of component updates

Only update components when you need to

In some cases, entities will have data which does not need to be shared between workers, but does need to be persisted when the entity crosses a worker boundary.

If you always kept this data in a component, any property change would be sent to SpatialOS and propagated to the other interested workers, which do not need to know about this data.

However, if you just kept the data locally in the worker’s memory, when the entity crosses a worker-boundary, the new worker will not know the up-to-date data.

A compromise between these is to keep the data locally in the worker’s memory, and only write the data to a component when the worker is about to lose authority. You can be notified when a worker is about to lose authority by using the AuthorityLossImminent notifications. It is also a good idea to save the worker’s local data to a component at a low frequency, for example every ten seconds, in case a worker is shut down or crashes, or a snapshot is taken.

For example, an NPC might have a large NPCStateMachine component, with lots of AI-related properties. Other workers that have this entity checked out do not need to know about these properties or receive updates when the properties change, but the data must be persisted when the NPC crosses a worker boundary.

Keeping the AI data in the worker’s local memory and only updating the NPCStateMachine component when the worker is about to lose authority prevents component updates being sent to other workers whenever the AI data changes.

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 visualise 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 clients without extremely high-frequency component updates.

Limit the components that each worker is interested in

Workers will be sent all of the component updates for every component the worker is interested in.

Therefore, one way to reduce the number of component update operations is to make sure every worker is only interested in the components which they need to know about.

For example, you may have an NPCStateMachine component on each NPC responsible for storing the information used by the NPCs state machine. The clients do not need to know this information, so to prevent this information being sent to every client, you need to make sure the clients are not interested in the NPCStateMachine component.

The components which a worker is interested in consists of:

  • The components which the worker has authority over.

  • The components that are set to be “checked out initially” in the bridge settings.

    This is the most important place that you can limit the components that each worker is interested in.

  • Components that the worker tells SpatialOS it is interested in at runtime. The worker can also tell SpatialOS it is no longer interested in a component at runtime.

    The Unity SDK has some specific behaviour here that is different to the other SDKs:

    The Unity SDK tells SpatialOS at runtime that it is interested in all of the components that have a reader or writer in some MonoBehaviour on the prefab, even if that reader or writer is not used.

    Therefore, when using the Unity SDK, it is important to only have readers and writers for the components which you want to receive updates for.

Merging components

Every time you update a component, a ComponentUpdate operation is sent to SpatialOS. This is then propagated to all other workers that are interested 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;
    Vector3f 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;
    Vector3f 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 do 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 int32 or int64, negative numbers will use the maximum number of bits.

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

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 tricks 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 compress 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 actually be inferred from other data.

Was this page helpful?

Thanks for letting us know!

Thanks for your feedback

Need more help? Ask on the forums