INetworkSerializable
The INetworkSerializable
interface can be used to define custom serializable types.
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
, RPC
s and NetworkVariable
s.
[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
SerializeValue
ifserializer.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
You can use arrays in one of two ways:
- Via C# arrays
- Via Native Collections (that is,
NativeArray
)
The critical distinction between the two is that C# arrays convert any type that has the arrays to a managed type. This results in garbage collection overhead and makes the arrays somewhat less optimized when you use them with NetworkVariable
. On the other hand, NativeArray
requires manual memory management.
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]);
}
}
}
public struct MyCustomNativeStruct : INetworkSerializable, IDisposable
{
public NativeArray<int> Array;
public void Dispose()
{
Array.Dispose();
}
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)
{
if(Array.IsCreated)
{
// Make sure the existing array is disposed and not leaked
Array.Dispose();
}
Array = new NativeArray<int>(length, Allocator.Persistent);
}
for (int n = 0; n < length; ++n)
{
// NataveArray doesn't have a by-ref index operator
// so we have to read, serialize, write. This works in both
// reading and writing contexts - in reading, `val` gets overwritten
// so the current value doesn't matter; in writing, `val` is unchanged,
// so Array[n] = val is the same as Array[n] = Array[n].
// NativeList also exists which does have a by-ref `ElementAt()` method.
var val = Array[n];
serializer.SerializeValue(ref val);
Array[n] = val;
}
}
}
Reading:
- (De)serialize
length
back from the stream - Iterate over
Array
membern=length
times - (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
Position
back from the stream - (De)serialize
Rotation
back from the stream - (De)serialize
SyncVelocity
back from the stream - Check if
SyncVelocity
is set to true, if so:- (De)serialize
LinearVelocity
back from the stream - (De)serialize
AngularVelocity
back from the stream
- (De)serialize
Writing:
- Serialize
Position
into the stream - Serialize
Rotation
into the stream - Serialize
SyncVelocity
into the stream - Check if
SyncVelocity
is set to true, if so:- Serialize
LinearVelocity
into the stream - Serialize
AngularVelocity
into 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
SyncVelocity
flag is set to true, both theLinearVelocity
andAngularVelocity
will be serialized into the stream - When the
SyncVelocity
flag is set tofalse
, we will leaveLinearVelocity
andAngularVelocity
with 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);
}
}