マルチプレイヤーのデータ圧縮とビット操作

Unity でマルチプレイヤー ゲームを作成するのは簡単な作業ではありませんが、PUN 2 などのサードパーティ ソリューションの助けが必要です。 、ネットワークの統合がはるかに簡単になりました。

あるいは、ゲームのネットワーキング機能をより詳細に制御する必要がある場合は、Socket テクノロジーを使用して独自のネットワーキング ソリューションを作成することもできます (例: サーバーがプレイヤーの入力のみを受信し、その後、サーバーがプレイヤーの入力を受信する権威マルチプレイヤー)すべてのプレイヤーが同じように行動するように独自の計算を行うことで、ハッキング)の発生率が減少します。

独自のネットワークを作成しているか、既存のソリューションを使用しているかに関係なく、この投稿で説明するトピックであるデータ圧縮に留意する必要があります。

マルチプレイヤーの基本

ほとんどのマルチプレイヤー ゲームでは、プレイヤーとサーバーの間で、指定されたレートで送受信される小さなデータ バッチ (一連のバイト) の形式で通信が行われます。

Unity (特に C#) で最も一般的な値の型は int です。 floatbool、、および string (また、頻繁に変化する値を送信する場合は string の使用を避けるべきです。このタイプで最も受け入れられる用途は、チャット メッセージまたはテキストのみを含むデータです)。

  • 上記のタイプはすべて、設定されたバイト数で保存されます。

int = 4バイト
float = 4バイト
bool = 1バイト
string = (エンコード形式に応じて、単一文字のエンコードに使用されるバイト数) x (文字数)

値がわかったら、標準的なマルチプレイヤー FPS (一人称視点シューティング) に送信する必要がある最小バイト量を計算してみましょう。

プレーヤーの位置: Vector3 (3 float x 4) = 12 バイト
プレーヤーの回転: クォータニオン (4 float x 4) = 16 バイト
プレーヤーのルックターゲット: Vector3 (3 float x 4) = 12 バイト
プレイヤーの発砲: bool = 1 バイト
プレイヤーの空中: bool = 1 バイト
プレイヤーのしゃがみ: bool = 1 バイト
プレイヤーの走行: bool = 1 バイト

合計 44 バイト。

拡張メソッドを使用してデータをバイト配列にパックし、その逆も行います。

SC_ByteMethods.cs

using System;
using System.Collections;
using System.Text;

public static class SC_ByteMethods
{
    //Convert value types to byte array
    public static byte[] toByteArray(this float value)
    {
        return BitConverter.GetBytes(value);
    }

    public static byte[] toByteArray(this int value)
    {
        return BitConverter.GetBytes(value);
    }

    public static byte toByte(this bool value)
    {
        return (byte)(value ? 1 : 0);
    }

    public static byte[] toByteArray(this string value)
    {
        return Encoding.UTF8.GetBytes(value);
    }

    //Convert byte array to value types
    public static float toFloat(this byte[] bytes, int startIndex)
    {
        return BitConverter.ToSingle(bytes, startIndex);
    }

    public static int toInt(this byte[] bytes, int startIndex)
    {
        return BitConverter.ToInt32(bytes, startIndex);
    }

    public static bool toBool(this byte[] bytes, int startIndex)
    {
        return bytes[startIndex] == 1;
    }

    public static string toString(this byte[] bytes, int startIndex, int length)
    {
        return Encoding.UTF8.GetString(bytes, startIndex, length);
    }
}

上記のメソッドの使用例:

SC_TestPackUnpack.cs

using System;
using UnityEngine;

public class SC_TestPackUnpack : MonoBehaviour
{
    //Example values
    public Transform lookTarget;
    public bool isFiring = false;
    public bool inTheAir = false;
    public bool isCrouching = false;
    public bool isRunning = false;

    //Data that can be sent over network
    byte[] packedData = new byte[44]; //12 + 16 + 12 + 1 + 1 + 1 + 1

    // Update is called once per frame
    void Update()
    {
        //Part 1: Example of writing Data
        //_____________________________________________________________________________
        //Insert player position bytes
        Buffer.BlockCopy(transform.position.x.toByteArray(), 0, packedData, 0, 4); //X
        Buffer.BlockCopy(transform.position.y.toByteArray(), 0, packedData, 4, 4); //Y
        Buffer.BlockCopy(transform.position.z.toByteArray(), 0, packedData, 8, 4); //Z
        //Insert player rotation bytes
        Buffer.BlockCopy(transform.rotation.x.toByteArray(), 0, packedData, 12, 4); //X
        Buffer.BlockCopy(transform.rotation.y.toByteArray(), 0, packedData, 16, 4); //Y
        Buffer.BlockCopy(transform.rotation.z.toByteArray(), 0, packedData, 20, 4); //Z
        Buffer.BlockCopy(transform.rotation.w.toByteArray(), 0, packedData, 24, 4); //W
        //Insert look position bytes
        Buffer.BlockCopy(lookTarget.position.x.toByteArray(), 0, packedData, 28, 4); //X
        Buffer.BlockCopy(lookTarget.position.y.toByteArray(), 0, packedData, 32, 4); //Y
        Buffer.BlockCopy(lookTarget.position.z.toByteArray(), 0, packedData, 36, 4); //Z
        //Insert bools
        packedData[40] = isFiring.toByte();
        packedData[41] = inTheAir.toByte();
        packedData[42] = isCrouching.toByte();
        packedData[43] = isRunning.toByte();
        //packedData ready to be sent...

        //Part 2: Example of reading received data
        //_____________________________________________________________________________
        Vector3 receivedPosition = new Vector3(packedData.toFloat(0), packedData.toFloat(4), packedData.toFloat(8));
        print("Received Position: " + receivedPosition);
        Quaternion receivedRotation = new Quaternion(packedData.toFloat(12), packedData.toFloat(16), packedData.toFloat(20), packedData.toFloat(24));
        print("Received Rotation: " + receivedRotation);
        Vector3 receivedLookPos = new Vector3(packedData.toFloat(28), packedData.toFloat(32), packedData.toFloat(36));
        print("Received Look Position: " + receivedLookPos);
        print("Is Firing: " + packedData.toBool(40));
        print("In The Air: " + packedData.toBool(41));
        print("Is Crouching: " + packedData.toBool(42));
        print("Is Running: " + packedData.toBool(43));
    }
}

上記のスクリプトは、バイト配列を長さ 44 (送信するすべての値のバイト合計に相当します) で初期化します。

次に、各値はバイト配列に変換され、Buffer.BlockCopy を使用して PackedData 配列に適用されます。

その後、packedData は SC_ByteMethods.cs の拡張メソッドを使用して値に変換されます。

データ圧縮技術

客観的に見て、44 バイトはそれほど多くのデータではありませんが、1 秒あたり 10 ~ 20 回送信する必要がある場合、トラフィックが増加し始めます。

ネットワークに関しては、すべてのバイトが重要です。

では、データ量を減らすにはどうすればよいでしょうか?

答えは簡単です。変更が予想されない値を送信しないことと、単純な値の型を 1 バイトにスタックすることです。

変更が予想されない値は送信しないでください

上の例では、4 つの float で構成される回転のクォータニオンを追加しています。

ただし、FPS ゲームの場合、プレーヤーは通常 Y 軸を中心に回転するだけなので、追加できるのは Y を中心とした回転だけであることがわかっているため、回転データは 16 バイトからわずか 4 バイトに減ります。

Buffer.BlockCopy(transform.localEulerAngles.y.toByteArray(), 0, packedData, 12, 4); //Local Y Rotation

複数のブール値を単一バイトにスタックする

バイトは 8 ビットのシーケンスであり、それぞれの値は 0 と 1 です。

偶然にも、ブール値は true または false のみです。したがって、単純なコードで、最大 8 つのブール値を 1 バイトに圧縮できます。

SC_ByteMethods.cs を開き、最後の閉じ中括弧 '}' の前に以下のコードを追加します。

    //Bit Manipulation
    public static byte ToByte(this bool[] bools)
    {
        byte[] boolsByte = new byte[1];
        if (bools.Length == 8)
        {
            BitArray a = new BitArray(bools);
            a.CopyTo(boolsByte, 0);
        }

        return boolsByte[0];
    }

    //Get value of Bit in the byte by the index
    public static bool GetBit(this byte b, int bitNumber)
    {
        //Check if specific bit of byte is 1 or 0
        return (b & (1 << bitNumber)) != 0;
    }

更新された SC_TestPackUnpack コード:

SC_TestPackUnpack.cs

using System;
using UnityEngine;

public class SC_TestPackUnpack : MonoBehaviour
{
    //Example values
    public Transform lookTarget;
    public bool isFiring = false;
    public bool inTheAir = false;
    public bool isCrouching = false;
    public bool isRunning = false;

    //Data that can be sent over network
    byte[] packedData = new byte[29]; //12 + 4 + 12 + 1

    // Update is called once per frame
    void Update()
    {
        //Part 1: Example of writing Data
        //_____________________________________________________________________________
        //Insert player position bytes
        Buffer.BlockCopy(transform.position.x.toByteArray(), 0, packedData, 0, 4); //X
        Buffer.BlockCopy(transform.position.y.toByteArray(), 0, packedData, 4, 4); //Y
        Buffer.BlockCopy(transform.position.z.toByteArray(), 0, packedData, 8, 4); //Z
        //Insert player rotation bytes
        Buffer.BlockCopy(transform.localEulerAngles.y.toByteArray(), 0, packedData, 12, 4); //Local Y Rotation
        //Insert look position bytes
        Buffer.BlockCopy(lookTarget.position.x.toByteArray(), 0, packedData, 16, 4); //X
        Buffer.BlockCopy(lookTarget.position.y.toByteArray(), 0, packedData, 20, 4); //Y
        Buffer.BlockCopy(lookTarget.position.z.toByteArray(), 0, packedData, 24, 4); //Z
        //Insert bools (Compact)
        bool[] bools = new bool[8];
        bools[0] = isFiring;
        bools[1] = inTheAir;
        bools[2] = isCrouching;
        bools[3] = isRunning;
        packedData[28] = bools.ToByte();
        //packedData ready to be sent...

        //Part 2: Example of reading received data
        //_____________________________________________________________________________
        Vector3 receivedPosition = new Vector3(packedData.toFloat(0), packedData.toFloat(4), packedData.toFloat(8));
        print("Received Position: " + receivedPosition);
        float receivedRotationY = packedData.toFloat(12);
        print("Received Rotation Y: " + receivedRotationY);
        Vector3 receivedLookPos = new Vector3(packedData.toFloat(16), packedData.toFloat(20), packedData.toFloat(24));
        print("Received Look Position: " + receivedLookPos);
        print("Is Firing: " + packedData[28].GetBit(0));
        print("In The Air: " + packedData[28].GetBit(1));
        print("Is Crouching: " + packedData[28].GetBit(2));
        print("Is Running: " + packedData[28].GetBit(3));
    }
}

上記の方法により、packedData の長さを 44 バイトから 29 バイトに削減しました (34% 削減)。

おすすめの記事
Unity の Photon Fusion 2 の概要
Unity でマルチプレイヤー ネットワーク ゲームを構築する
Unity オンライン リーダーボードのチュートリアル
PUN 2 を使用してマルチプレイヤー車ゲームを作成する
PUN 2 ラグ補正
Unity が PUN 2 ルームにマルチプレイヤー チャットを追加
PHP と MySQL を使用した Unity ログイン システム