Networked Properties

Learn how to add and configure networked properties

Adding networked state to an object is always done the same way whether you’re adding a field to an entity, event, reliable command, or player join request. Simply add a variable that derives from FSnapNetProperty—either in C++ or in blueprint—and it will be automatically synchronized.

Data Types

SnapNet provides a comprehensive set of data types out of the box:

  • Boolean
  • Double
  • Enum
  • Float
  • Int32
    • Client Index
    • Entity Index
    • Player Index
  • Primary Asset
  • Quaternion
  • Rotator
  • String
    • Soft Object Path
  • Vector
    • Angular Velocity
    • Position
    • Velocity
  • Vector 2D

Dynamic arrays, fixed-length arrays, and nested structs are also supported.

Should the need arise for a custom data type, game code can derive from FSnapNetProperty directly which provides complete control over serialization, delta compression, and interpolation. In practice, however, the existing data types should be sufficient.

Example

#include "SnapNetEvent.h"
#include "SnapNetPropertyEnum.h"
#include "SnapNetPropertyFloat.h"
#include "SnapNetPropertyInt32.h"

#include "ExampleEvent.generated.h"

UCLASS()
class UExampleEvent : USnapNetEvent
{
    GENERATED_BODY()

public:
    UExampleEvent()
        // Damage can be any value between 0 and 100 inclusive
        : Damage( 0, 100 )
        // DamageType can be any valid value of EDamageType
        , DamageType( StaticEnum<EDamageType>() )
        // Percentage can be any value between 0 and 1 with a precision of at least 0.01
        , Percentage( 0.0f, 1.0f, 0.01f )
        // Uses the "Weapons" string pool for efficient encoding of a weapon asset reference
        , WeaponAsset( "Weapons" )
    {
    }

    UPROPERTY()
    FSnapNetPropertyInt32 Damage;

    UPROPERTY()
    FSnapNetPropertyEnum DamageType;

    UPROPERTY()
    FSnapNetPropertyFloat Percentage;

    UPROPERTY()
    FSnapNetPropertySoftObjectPath WeaponAsset;
}

Common Configuration

Many data types require configuration to specify attributes like range or precision. When using C++, this is best done in the constructor’s member initializer list. When using blueprints, this can be done by editing the variable’s Default Value in the Details panel.

Specifying the range of your data types as narrowly as possible allows SnapNet to optimally encode your data on the wire.

In addition, all networked fields have the following configuration options regardless of data type:

Relevance

A property’s relevance determines which clients it will be sent to. The options are as follows:

Always: Always send the property to clients

Never: Never send the property to clients

Simulated: Send the property to clients that are predicting/simulating the entity

Not Simulated: Send the property to clients that are not predicting/simulating the entity

Spectated: Send the property to clients that are spectating the entity’s owner (including the owner provided they are not spectating someone else)

Not Spectated: Send the property to clients that are not spectating the entity’s owner

Simulated or Spectated: Send the property to clients that are simulating the entity, spectating the entity’s owner, or both

Neither Simulated nor Spectated: Send the property to clients that are neither simulating the entity nor spectating the entity’s owner

Use as Event ID

If a property is contained within an event, enabling Use as Event ID includes the property’s value in the event’s generated identifier. See the Events documentation for more details on event IDs.

Interpolation Configuration

Automatically interpolated properties, like floats or vectors, enable the simulation to run at a fixed tick-rate while rendering at a variable frame rate based on the system’s capabilities and load.

Interpolation uses an extra bit per-field during serialization so it can be manually disabled for properties that don’t require it.

AExampleEntity::AExampleEntity( const FObjectInitializer& ObjectInitializer /* = FObjectInitializer::Get() */ )
  : LastPlaybackPosition( 0.0f, 1.0f, 0.001f )
{
  LastPlaybackPosition.SetInterpolate( false );
}

Looping

Consider a timer for a looped 1 second animation. When the timer property wraps from 0.98 over to 0.01 you’d want the renderer to interpolate the timer forwards to 1 and then wrap over to 0 rather than interpolating backwards from 0.98 downwards to 0.01.

AExampleEntity::AExampleEntity( const FObjectInitializer& ObjectInitializer /* = FObjectInitializer::Get() */ )
  : AnimationTimer( 0.0f, 1.0f, 0.001f )
{
    AnimationTimer.SetLoop( true );
}

Discontinuities

There are times when the value of a field should snap directly to a new value and not interpolate from the previous value. Consider the animation timer in the previous example. If you were to start a new animation, you’d want to reset the timer to 0 immediately and not interpolate from the playback position of the previous animation that had been playing. This can be specified when setting the value.

void AExampleEntity::ResetAnimationTimer()
{
    AnimationTimer.SetValue( 0.0f, ESnapNetInterpolation::SnapToValue );
}

Real Number Encodings

Floating point data types such as floats and doubles support three different encodings: fixed range, signed range, and floating point.

Fixed Range Encoding

This encoding is configured using a minimum and maximum value for the property as well as a requested precision. For example, a minimum of 0, a maximum of 1, and a requested precision of 0.01 would transmit the property as a 7 bit integer (0-127). SnapNet will always guarantee that the minimum precision is as good or better than what’s requested. In this case, because 6 bits is too few to reach the requested precision, 7 bits are used and the minimum precision would actually be 1/127.

Signed Range Encoding

One common issue with the fixed range encoding above is that zero is not always cleanly represented for ranges that span zero. For example, a fixed range encoding with a minimum of -1, a maximum of 1, and a requested precision of 0.02 would use 7 bits (0-127). In that case, a value of 63 represents ~-0.008 and a value of 64 represents ~0.008 which is often undesirable in games when synchronizing a signed magnitude.

The signed range encoding addresses this issue directly. It is configured using a maximum magnitude and requested precision and it is equivalent to using a fixed range encoding for the magnitude plus one bit to encode the sign—ensuring that zero is always representable.

Floating Point Encoding

When using this encoding, floats and doubles are encoded as floating point numbers using the specified number of bits for the exponent and the significand. For example, a float specified to be encoded with 8 bits for the base and 23 bits for the significand will be sent with full precision, using 32 bits on the wire (1 bit is implicitly used for the sign).

This encoding is most useful for properties that take on values that are many orders of magnitude apart e.g., it’s often a good choice for transmitting the velocities of physics objects.

String Pools

Strings typically consume a lot of bandwidth when networked naively as they use at least a byte per character. Given that developers should try to keep state changes below one kilobyte or so, even a few strings can end up taking up a large amount of that budget! However, when networked string properties are used to reference things like assets on disk or pre-placed actors in a map they can only ever take on a finite set of values at runtime. By using string pools, SnapNet can exploit this to significantly optimize the encoding.

A string pool is an append-only list of strings with a maximum count. This allows SnapNet to serialize entire strings as a single integer—a compact index into the list. Since strings can be added to a string pool while a session is running but never removed, be sure to set the maximum limit conservatively. Even a pool of 4 billion strings will use only 32 bits of bandwidth—the equivalent of only a four character string—so the savings will be significant regardless of how large a pool is required. However, for strings that don’t belong to a finite set, such as player names or automatically generated IDs, string pools should not be used as there is no way to prevent the number of strings from eventually exceeding the string pool’s maximum count.

Configuration

String pools can be registered via Project Settings → SnapNet → Common → Registration → String Pools. Each string pool requires a name and a maximum string count. For example, you may have one string pool for animations, another for actor references, etc. When using FSnapNetPropertyString or FSnapNetPropertySoftObjectPath, you can configure it to use a string pool by passing the string pool’s name to the property’s constructor.

String Registration

Strings will automatically be added to string pools the first time they are used. Doing so at runtime is supported but means that the string must be sent in full to clients the first time it is used and only afterwards can it be encoded using a compact index. For this reason, it is recommended that strings be pre-registered by servers after they’ve started but before any clients have connected. Clients will receive the full set of pre-registered strings during the initial connection process, allowing any future usage to be encoded using only their compact form. Strings can be pre-registered by calling USnapNetServer::RegisterString.