Serialization in Photon

Photon and its clients are using a highly optimized binary protocol to communicate. It's compact, yet easy to parse.

Photon must convert all data into this binary protocol before it can be sent. This is done automatically for a range of commonly used data types. On most client APIs, you can also register your own serialization methods for other classes you might need. See below.

Photon Supported Types

Each Photon supported type needs some reserved bytes for the type_info.

  • Primitive types need 1 byte for type_info.
  • Most collections need 1 extra byte for the type of the collection. This is not the case for Hashtable, Object array and Byte array as the collection type is part of the type_info.
  • Strongly typed collections send the element type once instead of sending type_info per element.
  • All collections need 2 bytes to store their length. This is because the type of the length is short. Byte array is the exception to this rule. Its length is of type int and it needs 4 bytes to store the length.

The following types are generally supported and known by Photon's binary protocol. As some languages don't provide every listed type, some SDKs support less types.

type (C#) size [bytes] (photon_sizeof) description
byte 2 8 bit unsigned
2 = type_info(byte) + sizeof(byte)
bool (boolean) 2 true or false
2 = type_info(bool) + sizeof(bool)
short 3 16 bit
3 = type_info(short) + sizeof(short)
int (integer) 5 32 bit
5 = type_info(int) + sizeof(int)
long 9 64 bit
9 = type_info(long) + sizeof(long)
float 5 32 bit
5 = type_info(float) + sizeof(float)
double 9 64 bit
9 = type_info(double) + sizeof(double)
String 3 + sizeof( UTF8.GetBytes(string_value) ) length ≤ short.MaxValue
3 = type_info(String) + length_size;
length_size = sizeof(short)
Object[] (Object-array) 3 + photon_sizeof(elements) length ≤ short.MaxValue
3 = type_info(Object[]) + length_size;
length_size = sizeof(short)
byte[] (byte-array) 5 + length length ≤ int.MaxValue
5 = type_info(byte[]) + length_size;
length_size = sizeof(int)
array (array of type T, T[]) 4 + photon_sizeof(elements) - length * type_info(T) length ≤ short.MaxValue
T-type can be any of the types listed in this table except byte.
4 = type_info(array) + type_info(T) + length_size;
length_size = sizeof(short)
Hashtable 3 + photon_sizeof(keys) + photon_sizeof(values) pairs count ≤ short.MaxValue
3 = type_info(Hashtable) + length_size;
length_size = sizeof(short)
Dictionary<Object,Object> 5 + photon_sizeof(keys) + photon_sizeof(values) pairs count ≤ short.MaxValue
5 = type_info(Dictionary) + 2 * type_info(Object) + length_size;
length_size = sizeof(short)
Dictionary keys should not be of type Dictionary.
Dictionary<Object,V> 5 + photon_sizeof(keys) + photon_sizeof(values) - count(keys) * type_info(V) pairs count ≤ short.MaxValue
V-type can be any of the types listed in this table.
5 = type_info(Dictionary) + type_info(Object) + type_info(V) + length_size;
length_size = sizeof(short)
Dictionary keys should not be of type Dictionary.
Dictionary<K,Object> 5 + photon_sizeof(keys) + photon_sizeof(values) - count(keys) * type_info(K) pairs count ≤ short.MaxValue
K-type can be any of the types listed in this table.
5 = type_info(Dictionary) + type_info(K) + type_info(Object) + length_size;
length_size = sizeof(short)
Dictionary keys should not be of type Dictionary.
Dictionary<K,V> 5 + photon_sizeof(keys) + photon_sizeof(values) - count(keys) * (type_info(K) + type_info(V)) pairs count ≤ short.MaxValue
K- and V-types can be any of the types listed in this table.
5 = type_info(Dictionary) + type_info(K) + type_info(V) + length_size; length_size = sizeof(short)
Dictionary keys should not be of type Dictionary.

Custom Types

For any type not listed above, Photon will need your help to de/serialize important values.

The basic idea is that you write two methods to convert your class to a byte-array and back, then register those with the Photon API. When that's done you can include instances of that type in any message you send.

Custom types have 2 bytes for type_info: one byte to tell that it's a custom type plus one more for the custom type code. Photon supports up to 256 custom types. We recommend choosing custom type codes from 255 and downward.

Photon will call the serialization methods for a registered type and automatically prefix the created byte-arrays with 4 bytes: 2 bytes for the necessary type information and 2 bytes for payload length. Due to the 4 bytes overhead, you might want to avoid registering types that are just a few bytes of data.

The Photon Server is able to forward unknown custom types "as is". This is why you don't need to register your types in the Photon Cloud.

Make sure to register your custom types on all communicating clients. Register custom types on server side or on plugins when needed.

RegisterType method returns a boolean result, which tells you if the type could be registered. If any error occurs during custom type registration the method will return false and nothing will be changed. Otherwise registration should be successful and the returned value is true. If the custom code is already used the registration will fail and the method will return false. Overriding registered serialization and deserialization methods for a same custom type will fail and the old ones will still be used.

Custom Types in C#

All of our C#-based APIs (.NET, Unity, Xamarin, etc) provide the same way to register classes. There are two ways of doing this which depends on whether or not you use our custom Steam class or just a "plain" byte array.

Byte Array Method

The static method to call is:

    PhotonPeer.RegisterType(Type customType, byte code, SerializeMethod serializeMethod, DeserializeMethod constructor)

SerializeMethod and DeserializeMethod are defined delegates with the following respective signatures:

    public delegate byte[] SerializeMethod(object customObject);
    public delegate object DeserializeMethod(byte[] serializedCustomObject);

Example

As example, we implemented a simple basic MyCustomType:

public class MyCustomType
{
  public byte Id { get; set; }

  public static object Deserialize(byte[] data)
  {
    var result = new MyCustomType();
    result.Id = data[0];
    return result;
  }

  public static byte[] Serialize(object customType)
  {
    var c = (MyCustomType)customType;
    return new byte[] { c.Id };
  }
}

And to register it:

    PhotonPeer.RegisterType(typeof(MyCustomType), myCustomTypeCode, MyCustomType.Serialize, MyCustomType.Deserialize);

StreamBuffer Method

StreamBuffer is our custom implementation of the Stream class. It gives you all the benfits of a byte array wrapper and supports Photon's built-in serializable types.

The static method to call is:

    RegisterType(Type customType, byte code, SerializeStreamMethod serializeMethod, DeserializeStreamMethod deserializeMethod)

SerializeStreamMethod and DeserializeStreamMethod are defined delegates with the following respective signatures:

    public delegate byte[] SerializeMethod(StreamBuffer outStream, object customobject);
    public delegate object DeserializeMethod(StreamBuffer inStream, short length);

Example

Let's have a look at how PUN implements support for Unity's Vector2. You can find all of PUN's custom types implementation under "Assets\Photon Unity Networking\Plugins\PhotonNetwork\CustomTypes.cs".

A Vector2 has 2 floats: Vector2.x and Vector2.y. While floats are supported by Photon, Vector2 is not.

Basically, using any C#-way to get the 4 bytes that resemble the float is ok. Photon's Protocol class has a few Serialize methods that effectively write several types to byte-arrays.

    public static readonly byte[] memVector2 = new byte[2 * 4];
    private static short SerializeVector2(StreamBuffer outStream, object customobject)
    {
        Vector2 vo = (Vector2)customobject;
        lock (memVector2)
        {
            byte[] bytes = memVector2;
            int index = 0;
            Protocol.Serialize(vo.x, bytes, ref index);
            Protocol.Serialize(vo.y, bytes, ref index);
            outStream.Write(bytes, 0, 2 * 4);
        }

        return 2 * 4;
    }

Note that SerializeVector2 gets an object and has to cast this to the expected Vector2 type first.

Conversely, we also return just an object in DeserializeVector2:

    private static object DeserializeVector2(StreamBuffer inStream, short length)
    {
        Vector2 vo = new Vector2();
        lock (memVector2)
        {
            inStream.Read(memVector2, 0, 2 * 4);
            int index = 0;
            Protocol.Deserialize(out vo.x, memVector2, ref index);
            Protocol.Deserialize(out vo.y, memVector2, ref index);
        }

        return vo;
    }

And finally, we have to register the Vector2:

    PhotonPeer.RegisterType(typeof(Vector2), (byte)'W', SerializeVector2, DeserializeVector2);

Additional information

When doing custom serializion of complex data, it's important to understand how data is transported and how to monitor traffic to keep it under control.

 To Document Top