Multiplayer-datacompressie en bitmanipulatie

Een multiplayergame maken in Unity is geen triviale taak, maar met behulp van oplossingen van derden, zoals PUN 2 heeft het de netwerkintegratie veel eenvoudiger gemaakt.

Als alternatief, als je meer controle over de netwerkmogelijkheden van het spel nodig hebt, kun je je eigen netwerkoplossing schrijven met behulp van Socket-technologie (bijvoorbeeld gezaghebbende multiplayer, waarbij de server alleen spelersinvoer ontvangt en vervolgens zijn werk doet). eigen berekeningen om ervoor te zorgen dat alle spelers zich op dezelfde manier gedragen, waardoor het aantal gevallen van hacking) wordt verminderd.

Ongeacht of u uw eigen netwerk schrijft of een bestaande oplossing gebruikt, u moet rekening houden met het onderwerp dat we in dit bericht zullen bespreken, namelijk datacompressie.

Basisprincipes van meerdere spelers

In de meeste multiplayerspellen vindt er communicatie plaats tussen spelers en de server, in de vorm van kleine batches gegevens (een reeks bytes), die met een bepaalde snelheid heen en weer worden verzonden.

In Unity (en specifiek in C#) zijn de meest voorkomende waardetypen int, float, bool, en string (je moet ook vermijden string te gebruiken bij het verzenden van vaak veranderende waarden, het meest acceptabele gebruik voor dit type zijn chatberichten of gegevens die alleen tekst bevatten).

  • Alle bovenstaande typen worden opgeslagen in een bepaald aantal bytes:

int = 4 bytes
float = 4 bytes
bool = 1 byte
string = (Aantal bytes gebruikt om één teken te coderen, afhankelijk van het coderingsformaat) x (Aantal tekens)

Laten we, als we de waarden kennen, het minimale aantal bytes berekenen dat nodig is om te worden verzonden voor een standaard multiplayer FPS (First-Person Shooter):

Spelerpositie: Vector3 (3 floats x 4) = 12 bytes
Spelerrotatie: Quaternion (4 floats x 4) = 16 bytes
Spelerlookdoel: Vector3 (3 floats x 4) = 12 bytes
Speler schiet: bool = 1 byte
Speler in de lucht: bool = 1 byte
Speler hurkt: bool = 1 byte
Speler rent: bool = 1 byte

Totaal 44 bytes.

We zullen uitbreidingsmethoden gebruiken om de gegevens in een array van bytes te verpakken, en omgekeerd:

  • Maak een nieuw script, noem het SC_ByteMethods en plak de onderstaande code erin:

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);
    }
}

Voorbeeldgebruik van de bovenstaande methoden:

  • Maak een nieuw script, noem het SC_TestPackUnpack en plak de onderstaande code erin:

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));
    }
}

Het bovenstaande script initialiseert de byte-array met een lengte van 44 (wat overeenkomt met de bytesom van alle waarden die we willen verzenden).

Elke waarde wordt vervolgens geconverteerd naar byte-arrays en vervolgens toegepast in de packData-array met behulp van Buffer.BlockCopy.

Later wordt de packingData terug geconverteerd naar waarden met behulp van uitbreidingsmethoden van SC_ByteMethods.cs.

Technieken voor gegevenscompressie

Objectief gezien is 44 bytes niet veel gegevens, maar als het 10 tot 20 keer per seconde moet worden verzonden, begint het verkeer zich op te tellen.

Als het om netwerken gaat, telt elke byte.

Dus hoe kan de hoeveelheid data worden verminderd?

Het antwoord is eenvoudig: door de waarden die naar verwachting niet zullen veranderen niet te verzenden, en door eenvoudige waardetypen in één byte te stapelen.

Verzend geen waarden waarvan niet wordt verwacht dat ze zullen veranderen

In het bovenstaande voorbeeld voegen we het Quaternion van de rotatie toe, dat uit 4 vlotters bestaat.

In het geval van een FPS-game roteert de speler echter meestal alleen rond de Y-as, wetende dat we de rotatie rond Y alleen kunnen optellen, waardoor de rotatiegegevens worden teruggebracht van 16 bytes naar slechts 4 bytes.

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

Stapel meerdere Booleans in één byte

Een byte is een reeks van 8 bits, elk met een mogelijke waarde van 0 en 1.

Toevallig kan de bool-waarde alleen waar of onwaar zijn. Met een eenvoudige code kunnen we dus maximaal 8 bool-waarden in één byte comprimeren.

Open SC_ByteMethods.cs en voeg de onderstaande code toe vóór de laatste accolade '}'

    //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;
    }

Bijgewerkte SC_TestPackUnpack-code:

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));
    }
}

Met de bovenstaande methoden hebben we de PackData-lengte teruggebracht van 44 naar 29 bytes (reductie van 34%).

Voorgestelde artikelen
Inleiding tot Photon Fusion 2 in Unity
Samen multiplayer-netwerkgames bouwen
Unity Online Leaderboard-tutorial
Maak een multiplayer-autospel met PUN 2
PUN 2 vertragingscompensatie
Unity voegt multiplayer-chat toe aan de PUN 2-kamers
Unity Login-systeem met PHP en MySQL