-
Notifications
You must be signed in to change notification settings - Fork 70
Tundra protocol
The wire protocol used between a realXtend Tundra server and client to propagate scene changes mirrors Tundra's Entity-Component-Attribute scene model. The communication is bidirectional: a server can push changes to client(s), and a client can push changes to the server. If the server accepts the changes sent by a client, the server then proceeds to forward those changes to all the rest of the clients.
The scene model supports also the concept of local entities or components, that are not synchronized across the network. This is used for objects for which synchronizing doesn't make sense (for example purely client-side particle or physics effects, or the HUD of each player in a game)
The synchronization protocol is implemented by a class called SyncManager in Tundra. Tundra relies heavily on the Qt library, and the scene's Entities & Components are derived from Qt's QObject class, so that they can emit signals as entities or components are created, or attributes change. The SyncManager connects to these signals to be able to detect the scene changes.
The network protocol encodes a scene ID in all the messages. This would allow one client to be connected to multiple scenes at one time, but is not yet actually used.
For identifying the entities over the network or when serializing the scene to disk, entities are assigned scene-unique integer IDs starting from 1. Within an entity, components are also assigned integer IDs starting from 1.
The attributes of a component are also referred to with integer IDs, these start from 0. The sync protocol supports creating and removing attributes in components on the fly, but for most components such as EC_Mesh or EC_Placeable, the set of attributes is fixed.
The network messages used in scene synchronization with their basic binary structure follow below. Many such messages can be packed to a UDP packet; in WebSocket terms one message is one data frame. A key concept in the messages is encoding unsigned integers such as entity ID's as variable-length: either 8, 16 or 32 bits depending on the magnitude, to optimize for size. This is marked as VLE8/16/32 below. As for other data types, U8 is an unsigned byte, and String is a string with length prepended as an unsigned byte, so maximum length is 255.
All values are transmitted as little-endian.
First thing in each network message is the message ID as U16. This ID will tell you the structure of the rest of the data, which can be found below.
Application logic can add any login properties before the connection is attempted, these properties are sent to the server for inspection. Tundra core does not validate any login properties, this is left for the application developer.
<U16> Byte length of string data.
<n bytes> Login properties as UTF8-encoded string.
Example of the login properties XML syntax:
<login>
<propertyName value="propertyValue"></propertyName>
...
</login>
Login reply will give a boolean for success and the all important connection ID for the client. This ID is the unique network identifier for this particular client on the server. Additionally application logic can add login reply data that gets sent back to the client.
<U8> >0 means login was successful
<VLE8/16/32> Client connection ID.
<U16> Length of string data.
<String> Login reply data as XML.
Synchronizes the full state of an entity, including its components and their attribute values. When a client connects to a scene, these messages are sent for each entity in the scene to synchronize the full initial scene state.
<VLE8/16/32> Scene ID
<VLE8/16/32> Entity ID
<U8> Entity's temporary flag, 0 or 1. When serializing the scene, temporary entities are not to be saved to file
<VLE8/16/32> Number of synchronized (non-local) components in the entity
Repeat for each synchronized component:
<VLE8/16/32> Component ID within the entity
<VLE8/16/32> Component type ID. These are constants in the Tundra codebase, for example EC_Placeable's typeid is 20
<String> Component name (can be empty)
<VLE8/16/32> Attribute data block size
<n bytes> Attribute data block
The attribute data block contains first the component's fixed set of attributes, concatenated one after another in a binary representation. The attribute order is specified in the component's C++ header file. After that, dynamically created attributes follow in the fashion indicated below:
For each dynamically created attribute:
<U8> Attribute index within component. Fixed attributes will occupy slots 0 - x-1, where x is the number of fixed attributes
<U8> Attribute type ID. 1=string, 2=int, 3=32-bit float ...
<n bytes> Attribute binary data, size depends on the type ID. String attributes have a 16-bit length prepended.
Note that specifying the attribute data block size allows clients to skip components they don't know of.
Synchronizes the creation of one or more components to an entity.
<VLE8/16/32> Scene ID
<VLE8/16/32> Entity ID
Repeat for each component:
<VLE8/16/32> Component ID within the entity
<VLE8/16/32> Component type ID
<String> Component name (can be empty)
<VLE8/16/32> Attribute data block size
<n bytes> Attribute data block, like in CreateEntity
Synchronizes the creation of dynamic attributes into an entity's components.
<VLE8/16/32> Scene ID
<VLE8/16/32> Entity ID
Repeat for each attribute:
<VLE8/16/32> Component ID within the entity
<U8> Attribute index within component
<U8> Attribute type ID
<String> Attribute name
<n bytes> Attribute binary data, size depends on type ID
Synchronizes the modification of attribute values in an existing entity. After a client has connected and the initial scene has been sent, these messages will form the majority of the data payload it receives.
<VLE8/16/32> Scene ID
<VLE8/16/32> Entity ID
Repeat for each component that has changed attributes in that entity:
<VLE8/16/32> Component ID within the entity
<VLE8/16/32> Attribute data block size
<n bytes> Attribute data block
The attribute data block is to be interpreted in the following way. Read one bit from the stream first, whose value decides one of the two indexing methods to use. The indexing method will be chosen during runtime to get the smallest data size.
Bit value 0: Individual attribute indexing - repeat for each attribute, until end of data block reached:
<U8> Attribute index
<n bytes> Attribute binary data
Bit value 1: Flag indexing - repeat until end of data block reached:
<Bit> Attribute changed flag, starting from attribute index 0
<n bytes> If changed flag is 1, attribute binary data follows. If flag is 0, attribute stayed same and no data is present - proceed to reading the next flag
Synchronizes the removal of dynamic attributes from an entity's components.
<VLE8/16/32> Scene ID
<VLE8/16/32> Entity ID
Repeat for each attribute:
<VLE8/16/32> Component ID within the entity
<U8> Attribute index within component
Synchronizes the removal of one or more components from an entity.
<VLE8/16/32> Scene ID
<VLE8/16/32> Entity ID
Repeat for each component:
<VLE8/16/32> Component ID within the entity
Synchronizes the removal of an entity from the scene.
<VLE8/16/32> Scene ID
<VLE8/16/32> Entity ID
Synchronizes arbitrary remote procedure call-like actions with string parameters between the server and the client. The action is always connected to a single scene entity. Both the server and the client can send actions.
<U32> Entity ID
<U8> Length of action name
<n bytes> Action name as an ASCII / LATIN1 string
<U8> Execution type (where action should be executed) which is a bitmask of 1 = locally, 2 = on server, 4 = on peers (all other connected clients)
<U8> Number of parameters
Repeat for each parameter:
<VLE8/16/32> Length of parameter
<n bytes> Parameter as an ASCII / LATIN1 string
Here is the full list of the attribute types supported by the Tundra protocol.
String = 1;
Int = 2;
Real = 3;
Color = 4;
Float2 = 5;
Float3 = 6;
Float4 = 7;
Bool = 8;
UInt = 9;
Quat = 10;
AssetReference = 11;
AssetReferenceList = 12;
EntityReference = 13;
QVariant = 14;
QVariantList = 15;
Transform = 16;
QPoint = 17;
Attribute encodings:
String
<U16> Length of UTF8-encoded data in bytes
<n bytes> UTF8 representation of the string
Int
Stored as a signed 32-bit value, little endian.
Real
Stored as a 32-bit floating point value according to the IEEE 754 standard.
Color
Stored as four consecutive 32-bit floating point values: r, g, b, a.
Float2
Stored as two consecutive 32-bit floating point values: x, y.
Float3
Stored as three consecutive 32-bit floating point values: x, y, z.
Float4
Stored as four consecutive 32-bit floating point values: x, y, z, w.
Bool
Stored as a 8-bit byte. Zero for false, nonzero for true.
UInt
Stored as an unsigned 32-bit value, little endian.
Quat
Stored as four consecutive 32-bit floating point values: x, y, z, w. Same as Float4 but represents a rotation.
AssetReference
<U8> Length of asset reference in bytes
<n bytes> Asset reference as an ASCII / LATIN1 string
AssetReferenceList
<U8> Number of asset references in the list
Repeat for each asset reference:
<U8> Length of asset reference in bytes
<n bytes> Asset reference as an ASCII / LATIN1 string
EntityReference
<U8> Length of entity reference in bytes
<n bytes> Entity reference as an ASCII / LATIN1 string
QVariant
<U8> Length of variant in bytes
<n bytes> Variant as an ASCII / LATIN1 string
QVariantList
<U8> Number of variants in the list
Repeat for each variant:
<U8> Length of variant in bytes
<n bytes> Variant as an ASCII / LATIN1 string
Transform
Stored as nine consecutive 32-bit floating point values. Rotation is in degrees & Euler angles.
posX, posY, posZ, rotX, rotY, rotZ, scaleX, scaleY, scaleZ
QPoint
Stored as two consecutive 32-bit little-endian signed integer values: x, y.
As the movement of objects (position/orientation change), and velocities (for physically simulated objects, containing the physics component EC_RigidBody) comprise the majority of network bandwidth for a typical 3D simulation scene, a dedicated network message exists for replicating these. It achieves smaller data size by grouping together both the position (attribute changes in EC_Placeable component) and velocity (attribute changes in EC_RigidBody component) updates, so that for example entity ID is not needed to be sent twice, and quantizes the position/rotation/velocity coordinates to smaller binary size if applicable, and/or removes unnecessary coordinate axes.
Interest management can also be optionally enabled. The SyncManager will in this case utilize another object, the InterestManager, to filter entity updates for only those clients who find it relevant. There can be different filters (eg. euclidean distance metric) for determining relevancy. This depends on the clients sending regular updates of their viewing position in the 3D scene.
Some aspects of the synchronization protocol, such as a component's fixed attributes, and their order, or the concept of fixed component & attribute type ID's, depend on the C++ class headers specifying these values. In other words generality was sacrificed for bit-efficiency of the data. New components obeying this sync protocol can not be readily created in eg. script. Instead Tundra provides the EC_DynamicComponent, which contains only non-fixed (dynamic) attributes, and which a script can access to create an arbitrary attribute structure. This attribute structure will then be synchronized over the network as expected.
In the future, all ASCII / LATIN1 encoded strings (with 8-bit length prepended) should be eliminated from the protocol and be replaced with UTF8-encoded strings (with 16-bit, or preferable VLE encoded length prepended). However this is a protocol breaking change and at the same time protocol versioning should be introduced to be able to handle compatibility with older clients.