Chapter 16

🌶 通信できるデータ型とカスタムタイプ

o8que
o8que
2021.03.13に更新

Photonがサポートするデータ型

後日加筆修正予定...

https://doc.photonengine.com/ja-jp/pun/current/reference/serialization-in-photon

カスタムタイプ

Photonがデフォルトでサポートしていないデータ型でも、シリアライズ(データ型からバイト列への変換)処理とデシリアライズ(バイト列からデータ型への変換)処理を実装して登録することで、カスタムタイプ(Custom Types)として通信することができます。Vector3型などはカスタムタイプとして登録されていて、以下の場所から実装コードを確認できます。

Color型のシリアライズ処理

Color型をカスタムタイプとして登録してみましょう。Color型のRGBA値はfloat型なので、そのままバイト列に書き込むためには、それぞれ4バイト(合計で16バイト)が必要になります。それに対して、Color32型のRGBA値はbyte型でそれぞれ1バイト(合計で4バイト)で済むため、型変換することによって通信量を削減しています。

using ExitGames.Client.Photon;
using UnityEngine;

public static class MyCustomTypes
{
    private static readonly byte[] bufferColor = new byte[4];

    // カスタムタイプを登録するメソッド(起動時に一度だけ呼び出す)
    public static void Register() {
        PhotonPeer.RegisterType(typeof(Color), 1, SerializeColor, DeserializeColor);
    }

    // Color型をバイト列に変換して送信データに書き込むメソッド
    private static short SerializeColor(StreamBuffer outStream, object customObject) {
        Color32 color = (Color)customObject;
        lock (bufferColor) {
            bufferColor[0] = color.r;
            bufferColor[1] = color.g;
            bufferColor[2] = color.b;
            bufferColor[3] = color.a;
            outStream.Write(bufferColor, 0, 4);
        }
        return 4; // 書き込んだバイト数を返す
    }

    // 受信データからバイト列を読み込んでColor型に変換するメソッド
    private static object DeserializeColor(StreamBuffer inStream, short length) {
        Color32 color = new Color32();
        lock (bufferColor) {
            inStream.Read(bufferColor, 0, 4);
            color.r = bufferColor[0];
            color.g = bufferColor[1];
            color.b = bufferColor[2];
            color.a = bufferColor[3];
        }
        return (Color)color;
    }
}

Vector2Int型のシリアライズ処理

Vector2Int型もカスタムタイプとして登録してみましょう。xyの値はint型で、それぞれ4バイト(合計で8バイト)になります。Photonでは、int型の値をバイト列に書き込むProtocol.Serialize()が利用できるので、それを使いましょう。

using ExitGames.Client.Photon;
using UnityEngine;

public static class MyCustomTypes
{
    public static readonly byte[] bufferColor = new byte[4];
    public static readonly byte[] bufferVector2Int = new byte[8];

    public static void Register() {
        PhotonPeer.RegisterType(typeof(Color), 1, SerializeColor, DeserializeColor);
        PhotonPeer.RegisterType(typeof(Vector2Int), 2, SerializeVector2Int, DeserializeVector2Int);
    }

    // 省略

    private static short SerializeVector2Int(StreamBuffer outStream, object customObject) {
        Vector2Int v = (Vector2Int)customObject;
        int index = 0;
        lock (bufferVector2Int) {
            Protocol.Serialize(v.x, bufferVector2Int, ref index);
            Protocol.Serialize(v.y, bufferVector2Int, ref index);
            outStream.Write(bufferVector2Int, 0, index);
        }
        return (short)index;
    }

    private static object DeserializeVector2Int(StreamBuffer inStream, short length) {
        int x, y;
        int index = 0;
        lock (bufferVector2Int) {
            inStream.Read(bufferVector2Int, 0, length);
            Protocol.Deserialize(out x, bufferVector2Int, ref index);
            Protocol.Deserialize(out y, bufferVector2Int, ref index);
        }
        return new Vector2Int(x, y);
    }
}

組み込み型のシリアライズ処理

Protocol.Serialize()で用意されているバイト列に変換できるデータ型は、short型、int型、float型のみです。それ以外のデータ型をバイト列に変換したい場合は、独自にシリアライズ処理を実装する必要があります。

独自の組み込み型のシリアライズ処理を定義するクラス(MyProtocol)を作成して、カスタムタイプのシリアライズ処理で使用できるようにしましょう。テンプレートとしてbyte型のシリアライズ処理のメソッドは、以下のようになります。

public static partial class MyProtocol
{
    public static void Serialize(byte value, byte[] target, ref int offset) {
        target[offset] = value;
        offset++;
    }

    public static void Deserialize(out byte value, byte[] source, ref int offset) {
        value = source[offset];
        offset++;
    }
}

double型など、2バイト以上のデータ型をバイト列へ変換する際にはBitConverterクラスを利用すると良いでしょう。BitConverterは自身の環境のバイトオーダーでしか処理を行えないため、IPAddressクラスのバイトオーダーを変換するメソッドを使って、異なるバイトオーダーの環境の間でも正しく通信できるようにする必要があります。

using System;
using System.Net;

public static partial class MyProtocol
{
    private const int SizeDouble = sizeof(double);

    public static void Serialize(double value, byte[] target, ref int offset) {
        long host = BitConverter.DoubleToInt64Bits(value);
        long network = IPAddress.HostToNetworkOrder(host);
        byte[] bytes = BitConverter.GetBytes(network);
        Buffer.BlockCopy(bytes, 0, target, offset, SizeDouble);
        offset += SizeDouble;
    }

    public static void Deserialize(out double value, byte[] source, ref int offset) {
        long host = BitConverter.ToInt64(source, offset);
        long network = IPAddress.NetworkToHostOrder(host);
        value = BitConverter.Int64BitsToDouble(network);
        offset += SizeDouble;
    }
}

string型は、送信する文字数によってサイズが変わります。可変長のデータは、バイト列の最初の1~4バイトにサイズを入れて、バイト列からサイズを取得できるようにしましょう。以下のコードでは、それほど長い文字列は通信しない想定で、最初の1バイトのみにサイズを入れるようにしています。これで最大255バイト分の文字列が通信できます。

using System.Text;
using UnityEngine;

public static partial class MyProtocol
{
    // UTF-8でエンコード・デコードできない文字は空文字に置き換える設定にしておく
    private static readonly Encoding encoding = Encoding.GetEncoding(
        "utf-8",
        new EncoderReplacementFallback(string.Empty),
        new DecoderReplacementFallback(string.Empty)
    );

    public static void Serialize(string value, byte[] target, ref int offset) {
        int byteCount = encoding.GetBytes(value, 0, value.Length, target, offset + 1);
        byte size = (byte)Mathf.Min(byteCount, byte.MaxValue);
        target[offset] = size;
        offset += size + 1;
    }

    public static void Deserialize(out string value, byte[] source, ref int offset) {
        byte size = source[offset];
        value = encoding.GetString(source, offset + 1, size);
        offset += size + 1;
    }
}

カスタムタイプのメリット・デメリット

Photonがデフォルトでサポートしているデータ型の型情報は1バイトに対して、カスタムタイプの型情報は4バイトが必要になります。カスタムタイプでシリアライズする値が少ない場合は、無駄にデータサイズが増えてしまう可能性があります。
例えば、Photonでカスタムタイプとして登録されているPlayer型は、型情報を含めて8バイトですが、内部的にはint型のプレイヤーIDしかシリアライズしていないので、直接int型でプレイヤーIDを通信すれば5バイトで済みます。
逆にカスタムタイプでシリアライズする値が多い場合は、値の数にかかわらず型情報が4バイトで済むため、データのサイズを減らせる可能性があります。ただし、カスタムタイプを登録して多くの値を通信することを検討する前に、そもそも多くの値を通信せずに済ませる方法がないかを、まず模索してみることをオススメします。