INetworkSerializable
The INetworkSerializable interface can be used to define custom serializable types.
All examples provided will work with RPCs and custom messages but some examples won't work with NetworkVariable due to the unmanaged type restriction.
NetworkVariable Type Litmus Test for INetworkSerializable Implementations:
- If the implementation itself can be a null (that is, a class), then it can't be used
- If it has any property that can be null (that is, arrays), then it can't be used
The alternative is to create your own NetworkVariableBase derived type specific class.
struct MyComplexStruct : INetworkSerializable
{
public Vector3 Position;
public Quaternion Rotation;
// INetworkSerializable
void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
{
serializer.SerializeValue(ref Position);
serializer.SerializeValue(ref Rotation);
}
// ~INetworkSerializable
}
Types implementing INetworkSerializable are supported by NetworkSerializer, RPCs and NetworkVariables.
[ServerRpc]
void MyServerRpc(MyComplexStruct myStruct) { /* ... */ }
void Update()
{
if (Input.GetKeyDown(KeyCode.P))
{
MyServerRpc(
new MyComplexStruct
{
Position = transform.position,
Rotation = transform.rotation
}); // Client -> Server
}
}
Nested serial types
Nested serial types will be null unless you initialize following one of these methods:
- Manually before calling
SerializeValueifserializer.IsReader(or something like that) - Initialize in the default constructor
This is by design. You may see the values as null until properly initialized. The serializer isn't deserializing them, the null value is simply applied before it can be serialized.
Conditional Serialization
As you have more control over serialization of a struct, you might implement conditional serialization at runtime.
More advanced use-cases are explored in following examples.
Example: Array
The below INetworkSerializable implementation example works only with RPCs and/or custom messages. The below implementation uses an array within an INetworkSerializable implementation. Arrays can be null and aren't supported by the NetworkVariable class. As an alternative, you can write your own NetworkVariableBase derived class that does support managed or unmanaged value types.
Read More About Custom NetworkVariable Implementations
public struct MyCustomStruct : INetworkSerializable
{
public int[] Array;
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
{
// Length
int length = 0;
if (!serializer.IsReader)
{
length = Array.Length;
}
serializer.SerializeValue(ref length);
// Array
if (serializer.IsReader)
{
Array = new int[length];
}
for (int n = 0; n < length; ++n)
{
serializer.SerializeValue(ref Array[n]);
}
}
}
Reading:
- (De)serialize
lengthback from the stream - Iterate over
Arraymembern=lengthtimes - (De)serialize value back into Array[n] element from the stream
Writing:
- Serialize length=Array.Length into stream
- Iterate over Array member n=length times
- Serialize value from Array[n] element into the stream
The BufferSerializer<TReaderWriter>.IsReader flag is being utilized here to determine whether to set length value to prepare before writing into the stream — we then use it to determine whether to create a new int[] instance with length size to set Array before reading values from the stream. There's also an equivalent but opposite BufferSerializer<TReaderWriter>.IsWriting
Example: Move
public struct MyMoveStruct : INetworkSerializable
{
public Vector3 Position;
public Quaternion Rotation;
public bool SyncVelocity;
public Vector3 LinearVelocity;
public Vector3 AngularVelocity;
void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
{
// Position & Rotation
serializer.SerializeValue(ref Position);
serializer.SerializeValue(ref Rotation);
// LinearVelocity & AngularVelocity
serializer.SerializeValue(ref SyncVelocity);
if (SyncVelocity)
{
serializer.SerializeValue(ref LinearVelocity);
serializer.SerializeValue(ref AngularVelocity);
}
}
}
Reading:
- (De)serialize
Positionback from the stream - (De)serialize
Rotationback from the stream - (De)serialize
SyncVelocityback from the stream - Check if
SyncVelocityis set to true, if so:- (De)serialize
LinearVelocityback from the stream - (De)serialize
AngularVelocityback from the stream
- (De)serialize
Writing:
- Serialize
Positioninto the stream - Serialize
Rotationinto the stream - Serialize
SyncVelocityinto the stream - Check if
SyncVelocityis set to true, if so:- Serialize
LinearVelocityinto the stream - Serialize
AngularVelocityinto the stream
- Serialize
Unlike the Array example above, in this example we don't use BufferSerializer<TReaderWriter>.IsReader flag to change serialization logic but to change the value of a serialized flag itself.
- If the
SyncVelocityflag is set to true, both theLinearVelocityandAngularVelocitywill be serialized into the stream - When the
SyncVelocityflag is set tofalse, we will leaveLinearVelocityandAngularVelocitywith default values.
Recursive Nested Serialization
It is possible to recursively serialize nested members with INetworkSerializable interface down in the hierarchy tree.
Review the following example:
public struct MyStructA : INetworkSerializable
{
public Vector3 Position;
public Quaternion Rotation;
void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
{
serializer.SerializeValue(ref Position);
serializer.SerializeValue(ref Rotation);
}
}
public struct MyStructB : INetworkSerializable
{
public int SomeNumber;
public string SomeText;
public MyStructA StructA;
void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
{
serializer.SerializeValue(ref SomeNumber);
serializer.SerializeValue(ref SomeText);
StructA.NetworkSerialize(serializer);
}
}
If we were to serialize MyStructA alone, it would serialize Position and Rotation into the stream using NetworkSerializer.
However, if we were to serialize MyStructB, it would serialize SomeNumber and SomeText into the stream, then serialize StructA by calling MyStructA's void NetworkSerialize(NetworkSerializer) method, which serializes Position and Rotation into the same stream.
Technically, there is no hard-limit on how many INetworkSerializable fields you can serialize down the tree hierarchy. In practice, consider memory and bandwidth boundaries for best performance.
You can conditionally serialize in recursive nested serialization scenario and make use of both features.
While you can have nested INetworkSerializable implementations (that is, an INetworkSerializable implementation with INetworkSerializable implementations as properties) like demonstrated in the example above, you can't have derived children of an INetworkSerializable implementation.
Unsupported Example
/// This isn't supported.
public struct MyStructB : MyStructA
{
public int SomeNumber;
public string SomeText;
void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
{
serializer.SerializeValue(ref SomeNumber);
serializer.SerializeValue(ref SomeText);
serializer.SerializeValue(ref Position);
serializer.SerializeValue(ref Rotation);
}
}