No matter how fast the internet becomes, conserving bandwidth will always be important. Some Game Clients might be on poor quality mobile networks with low upload and download speeds, or have high ping to the Replication Server and/or other Clients, etc.
Additionally, sending more data than is required consumes more memory and unnecessarily burdens the CPU and potentially GPU, which could add to performance issues, and even to quicker battery drainage.
In order to optimize the data we are sending over the network, we can employ various techniques built into the core of coherence.
Delta-compression (automatic). When possible, only send differences in data, not the entire state every frame.
Compression and quantization (automatic and configurable). Various data types can be compressed to consume less bandwidth that they naturally would.
Simulation frequency (configurable). Most Entities do not need to be simulated at 60+ frames per second.
Levels of detail (configurable). Entities need to consume less and less bandwidth the farther away they move from the observer.
Area of interest. Only replicate what we can see.
An integration with the Unity Profiler provides basic statistics on networking events and bandwidth.
The module is only available in Unity 2021.2 and newer.
To view the module, open the Unity Profiler by selecting Window > Analysis > Profiler. Open the Profiler Modules dropdown menu in the top left, and select the coherence module.
To hide unneeded graph lines, select the colored square next to the item you do not wish to see.
This feature requires baking.
coherence can support large game worlds with many objects. Since the amount of data that can be transmitted over the network is limited, it's very important to only send the most important things.
You already know a very efficient tool for enabling this – the LiveQuery. It ensures that a client is only sent data when an object in its vicinity has been updated.
Often though, there is a possibility for an even more nuanced and optimized approach. It is based on the fact that we might not need to send as much data for an entity that is far away, compared to a close one. A similar technique is often used in 3D-programming to show a simpler model when something is far away, and a more detailed when close-up.
This idea works really well for networking too. For example, when another player is close to you it's important to know exactly what animation it is playing, what it's carrying around, etc. When the same player is far off in the horizon, it might suffice to only know it's position and orientation, since nothing else will be discernible anyways.
To use this technique we must learn about something called archetypes.
Any Prefab with the CoherenceSync component can be optimized to use a various levels of details (LODs).
There must always exist a LOD 0, this is the default level and it always has all components enabled (it can have per-field overrides though, see below.)
There can be any number of subsequent LODs (e.g. LOD 1, LOD 2, etc.) and each one must have a distance threshold higher than the previous one. The coherence SDK will try to use the LOD with the highest number, but that is still within the distance threshold.
Example
An object has three LODs, like this:
LOD 0 (threshold 0)
LOD 1 (threshold 10)
LOD 2 (threshold 20)
If this object is 15 units away, it will use LOD 1.
Confusingly, the highest numbered LOD is usually called the lowest one, since it has the least detail.
On each LOD, there are two options for optimizing data being transferred:
Components can be turned off, meaning you won't receive any updates from them.
Its fields can be configured to use fewer bits, usually leading to less fine-grained information. The idea is that this won't be noticeable at the distance of the LOD.
coherence allows us to define the range of numeric fields and how many bits we want to allocate to them.
Here are some terms we will be using:
Bits. The number of bits (octets) used for the field. When used for vectors, the number defined the number of bits used for each component (x
, y
and z
). A vector3
set to 24 bits
will consume 3 * 24 = 72
bits.
Range. For integer values and fixed-point floats, we define a minimum and maximum possible value (e.g. Health
can lie between 0
and 100
).
More bits mean more precision. Increasing the range while leaving the bit count the same will lower the precision of the field.
The maximum number of bits used for any field/component is currently 32.
coherence allows us to define these values for specific components and fields. Furthermore, we can define levels of detail so that precision and therefore bandwidth consumption falls with the distance of the object to the point of observation.
Levels of detail are calculated from the distance between the entity and the center of the LiveQuery.
On each LOD you can configure the individual fields of any component to use less data. You can only decrease the fidelity, so a field can't use more data on a lower (more far away) LOD. The Archetype editor interface will help you to follow these rules.
In order to define levels of detail, we have to click the Optimize button on a Prefab's CoherenceSync
component with defined field bindings.
That opens the Optimization window. We can override the base component settings even without defining further levels of detail.
Clicking on Add new Level Of Detail will add a new LOD. We can now define the distance at which the LOD starts. This is the minimum distance between the entity and the center of the LiveQuery at which the new level of detail becomes active (i.e. the Replicator will start sending data as defined here at this distance).
You can also disable components at later LOD levels if they are not needed. In the example above, you can see that in LOD2 the entire Transform and Animator components are disabled beyond the distance of 20 units. At 100 units (a.k.a. meters), we usually do not see animation details, so we can save a lot of bandwidth and processing power by not replicating this data.
The Data Cost Overview shows us that this takes the original 913 bits down to just 372 bits at LOD level 2.
The primitive types that coherence supports can be configured in different ways:
These three types can all be configured in the same way, using different compression types:
None
No compression will be used, a full 32-bit float will be transmitted every time.
Truncated
Allows for specifying the number of bits for compression. Less bits means lower bandwidth usage but at the cost of precision loss. The minimum number of bits is 10. Using 22 bits will result in around half of the precision of the full float, while 16 will result in the quarter of the precision.
Fixed point
Allows for specifying the range of values used together with either number of bits or a desired precision.
Range affects the maximum and minimum value that the data type can take on. For example, a range of 100 to 200 means only values within that range can be sent - any value outside of this range will be clamped to the nearest correct value.
Precision defines the greatest deviation allowed for the data type. For example, a precision of 0.1 means that a float of value 10.0 can be transmitted as anything from 9.9 to 10.1 over the network. The minimum allowed precision is 0.1, while the maximum precision depends on the range. Changing precision automatically recalculates the number of bits required for given range.
Bits dictate how many bits to use when calculating the precision for a given range. When set manually, it will trigger recalculation of the precision for a given range. Mind that the number of bits can be rounded down if the calculated precision uses less, e.g. for a range of [0, 1] setting the number of bits to 6 will result in precision of 0.1 and a final bit count of 4, since 4 bits suffice to represent this range with a calculated precision.
When using these range settings for vectors, it affects each axis of the vector separately. Imagine shrinking its bounding box, rather than a sphere.
Integers can be configured to any span (that fits within a 32-bit integer) by setting its minimum and maximum value.
For example, the member variable age
in a game about ancient trolls might use a minimum of 100 and a maximum of 2000. Based on the size of the range (1900 in this case) a bit-count will be calculated for you.
For integers, it usually make sense to not decrease the range on lower LODs since it will overflow (and wrap-around) any member on an entity that switches to a lower LOD. Instead, use this setting on LOD 0 to save data for the whole Archetype.
Quaternions and Colors can be configured using the number of bits per component. Quaternions require sending 3 components while Colors require 4 components.
All other types (strings, booleans, entity references) have no settings that can be overridden, so your only option for optimizing those are to turn them off completely at lower LODs.
If a LODed game object is parented to another synced object, the child will base its LOD level on the World position of its parent. This means that the (local) position of the LODed child does not have any effect on its LOD, until it is unparented.
Also – to save bandwidth, detection of LOD changes on the client only happens when the entity sends a component update. This means that a child object might appear to be using a nonsensical LOD until it changes in some way, for example by modifying its position.
When we bake, information from the CoherenceArchetype
component gets written into our schema. Below, you can see the setup presented earlier reflected in the resulting schema file.
If you want to know more about how LODs work inside the schema files, take a look at Archetypes.
The most unintuitive thing about archetypes and LOD-ing is that it doesn't affect the sending of data. This means that a "fat" object with tons of fields will still tax the network and the Replication Server if it is constantly updated, even if it uses a very optimized Archetype.
Also, it's important to realize that the exact LOD used on an entity varies for each other client, depending on the position of their query (or the closest one, if several are used.)
Areas of interest (or queries) are not only a way to optimise, but a fundamental tool for Clients to specify what part(s) of the online world they are interested in.
With them, the Replication Server can filter the information to send based on each Client's interest, and thus greatly optimise network traffic.
At the moment, coherence offers two ways to express this interest: and .
You need at least one query in your scene, or you won't see anything update over the network.
When a non-authoritative object falls outside of all queries, it gets destroyed (or returned to an object pool). When it gets back in, it gets reinstantiated (or taken out of the pool). If the right properties are synced, the object's state will be automatically restored by coherence, making the player feel like that object never disappeared.
Queries only filter network entities that are non-authoritative. Your own entities will never be destroyed for falling outside of a query.
When using queries and adding more than one, they act in an additive way.
So for instance, two overlapping LiveQueries will define a bigger area.
Similarly, a LiveQuery + a TagQuery will add up, looking for entities both within a range but also for the ones that have a certain tag, regardless of position.
Non-additive filtering will come in a future version of coherence.
It is a very common pattern to move a LiveQuery around, following a player character or the camera, to ensure the visible objects are updated.
In addition to this, queries can be turned on/off (simply by disabling the GameObject that hosts them), or their properties can be changed at runtime (like radius, position, or tag), making for a very dynamic tool to optimise bandwidth.
Queries are per-Client, meaning that each Client (or Simulator!) has its own queries and thus sees different parts of the simulation.
Without a special configuration, Entity data is captured at the highest possible frequency and sent to the Replication Server. This often generates more data than is needed to efficiently replicate the Entity's state across the network.
On a Simulator, we can limit the framerate globally using Unity's built-in static variable targetFrameRate.
coherence will automatically limit the target framerate of uploaded Simulators to 30 frames per second. We plan to make it possible to lift this restriction in the future. Check back for updates in the next couple of releases.
Replication frequency can be configured for each binding individually in the Prefab Optimize window. The Sample Rate controls how many times per second values are sampled and synced over the network.
Since the default packet send frequency of the Replication Server is 20Hz, sample rates above that value won't have any benefits unless you increase the Replication Server send frequency, too. See here how to .
High sample rates increase replication accuracy and reduce latency, but consume more bandwidth. The upper limit at which samples can be quantized is 60hz, so sample rates beyond that are generally not recommended. It is not possible to change sampling frequency at runtime.
Values that don't change over time do not consume any bandwidth. Only bindings with updated values will be synced over the network.
With the filtering is volume-based, kind of like moving a torch to look around in a dark cave.
With even distant objects can be seen, provided they have the right tag.
Queries can also be used for cheat prevention, see for more information.