Multiplayer datakomprimering og bitmanipulering

Å lage et flerspillerspill i Unity er ikke en triviell oppgave, men ved hjelp av tredjepartsløsninger, for eksempel PUN 2, har det gjort nettverksintegrasjon mye enklere.

Alternativt, hvis du trenger mer kontroll over spillets nettverksfunksjoner, kan du skrive din egen nettverksløsning ved å bruke Socket-teknologi (f.eks. autoritativ flerspiller, der serveren bare mottar spillerinndata og deretter gjør sin egne beregninger for å sikre at alle spillere oppfører seg på samme måte, og dermed redusere forekomsten av hacking).

Uansett om du skriver ditt eget nettverk eller bruker en eksisterende løsning, bør du være oppmerksom på emnet som vi skal diskutere i dette innlegget, som er datakomprimering.

Grunnleggende om flerspiller

I de fleste flerspillerspill er det kommunikasjon som skjer mellom spillere og serveren, i form av små batcher med data (en sekvens av bytes), som sendes frem og tilbake med en spesifisert hastighet.

I Unity (og spesifikt C#), er de vanligste verditypene int, float, bool, og string (du bør også unngå å bruke streng når du sender verdier som endrer seg ofte, den mest akseptable bruken for denne typen er chat-meldinger eller data som bare inneholder tekst).

  • Alle typene ovenfor er lagret i et bestemt antall byte:

int = 4 byte
float = 4 byte
bool = 1 byte
string = (Antall byte brukt til å kode et enkelt tegn, avhengig av kodingsformat) x (Antall tegn)

Når vi kjenner verdiene, la oss beregne minimumsmengden byte som er nødvendig for å sendes for en standard flerspiller FPS (First-Person Shooter):

Spillerposisjon: Vector3 (3 flyter x 4) = 12 byte
Spillerrotasjon: Quaternion (4 flyter x 4) = 16 byte
Spillerutseende mål: Vector3 (3 flyter x 4) = 12 byte
Spiller avfyring: bool = 1 byte
Spiller i luften: bool = 1 byte
Spiller som huker: bool = 1 byte
Spiller som kjører: bool = 1 byte

Totalt 44 byte.

Vi vil bruke utvidelsesmetoder for å pakke dataene inn i en rekke byte, og omvendt:

  • Opprett et nytt skript, navngi det SC_ByteMethods og lim inn koden nedenfor i det:

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

Eksempel på bruk av metodene ovenfor:

  • Opprett et nytt skript, navngi det SC_TestPackUnpack og lim inn koden nedenfor i det:

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

Skriptet ovenfor initialiserer byte-matrisen med en lengde på 44 (som tilsvarer bytesummen av alle verdiene vi ønsker å sende).

Hver verdi blir deretter konvertert til byte-matriser, og deretter brukt i packedData-matrisen ved hjelp av Buffer.BlockCopy.

Senere konverteres packedData tilbake til verdier ved å bruke utvidelsesmetoder fra SC_ByteMethods.cs.

Datakomprimeringsteknikker

Objektivt sett er 44 byte ikke mye data, men hvis det er nødvendig å sendes 10 - 20 ganger per sekund, begynner trafikken å øke.

Når det kommer til nettverk, teller hver byte.

Så hvordan redusere mengden data?

Svaret er enkelt, ved å ikke sende verdiene som ikke forventes å endre seg, og ved å stable enkle verdityper i en enkelt byte.

Ikke send verdier som ikke forventes å endres

I eksemplet ovenfor legger vi til Quaternion av rotasjonen, som består av 4 flottører.

Men når det gjelder et FPS-spill, roterer spilleren vanligvis bare rundt Y-aksen, vel vitende om at vi bare kan legge til rotasjonen rundt Y, noe som reduserer rotasjonsdata fra 16 byte til bare 4 byte.

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

Stable flere boolske i en enkelt byte

En byte er en sekvens på 8 biter, hver med en mulig verdi på 0 og 1.

Tilfeldigvis kan bool-verdien bare være sann eller usann. Så, med en enkel kode, kan vi komprimere opptil 8 bool-verdier til en enkelt byte.

Åpne SC_ByteMethods.cs og legg til koden nedenfor før den siste avsluttende klammeparentesen '}'

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

Oppdatert SC_TestPackUnpack-kode:

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

Med metodene ovenfor har vi redusert packedData-lengden fra 44 til 29 byte (34 % reduksjon).

Foreslåtte artikler
Introduksjon til Photon Fusion 2 i Unity
Bygg flerspillernettverksspill i Unity
Unity Online Leaderboard-opplæring
Lag et bilspill med flere spillere med PUN 2
PUN 2 Lagkompensasjon
Unity legger til flerspillerchat til PUN 2-rommene
Unity Login System med PHP og MySQL