דחיסת נתונים מרובה משתתפים ועיבוד סיביות

יצירת משחק מרובה משתתפים ב-Unity היא לא משימה של מה בכך, אבל בעזרת פתרונות של צד שלישי, כמו PUN 2, היא הקלה בהרבה על שילוב הרשתות.

לחלופין, אם אתה זקוק ליותר שליטה על יכולות הרשת של המשחק, אתה יכול לכתוב פתרון רשת משלך באמצעות טכנולוגיית Socket (לדוגמה, מרובה משתתפים סמכותי, שבו השרת מקבל רק קלט של שחקן ואז מבצע חישובים משלו כדי להבטיח שכל השחקנים מתנהגים באותו אופן, ובכך מפחיתים את השכיחות של פריצה).

לא משנה אם אתה כותב רשת משלך או משתמש בפתרון קיים, עליך להיות מודע לנושא בו נדון בפוסט זה, שהוא דחיסת נתונים.

יסודות מרובי משתתפים

ברוב המשחקים מרובי משתתפים, יש תקשורת המתרחשת בין שחקנים לשרת, בצורה של אצווה קטנה של נתונים (רצף של בתים), הנשלחים הלוך ושוב בקצב מוגדר.

ב-Unity (ובאופן ספציפי C#), סוגי הערכים הנפוצים ביותר הם int, float, bool, ו-string (כמו כן, כדאי להימנע משימוש במחרוזת בעת שליחת ערכים המשתנים לעתים קרובות, השימוש המקובל ביותר עבור סוג זה הם הודעות צ'אט או נתונים המכילים רק טקסט).

  • כל הסוגים לעיל מאוחסנים במספר מוגדר של בתים:

int = 4 בתים
float = 4 בתים
bool = 1 בייט
מחרוזת = (מספר בתים בשימוש כדי קידוד תו בודד, בהתאם לפורמט הקידוד) x (מספר תווים)

לדעת את הערכים, בואו לחשב את הכמות המינימלית של בתים הדרושים כדי להישלח עבור FPS מרובה משתתפים סטנדרטי (יורה בגוף ראשון):

מיקום שחקן: וקטור3 (3 צפים x 4) = 12 בתים
סיבוב שחקן: קווטרניון (4 מצופים x 4) = 16 בתים
יעד מראה שחקן: וקטור3 (3 מצופים x 4) = 12 בתים
שחקן ירי: bool = 1 בייט
שחקן באוויר: bool = 1 בייט
שחקן כפוף: bool = 1 בייט
שחקן פועל: bool = 1 בייט

סה"כ 44 בתים.

אנו נשתמש בשיטות הרחבה כדי לארוז את הנתונים למערך של בתים, ולהיפך:

  • צור סקריפט חדש, שם לו SC_ByteMethods ואז הדבק בתוכו את הקוד למטה:

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 ואז הדבק בתוכו את הקוד למטה:

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 (המתאים לסכום הבתים של כל הערכים שאנו רוצים לשלוח).

לאחר מכן כל ערך מומר למערכים של בתים, ולאחר מכן מוחל במערך packedData באמצעות Buffer.BlockCopy.

מאוחר יותר ה-packedData מומר בחזרה לערכים באמצעות שיטות הרחבה מ-SC_ByteMethods.cs.

טכניקות דחיסת נתונים

אובייקטיבית, 44 בתים הם לא הרבה נתונים, אבל אם צריך לשלוח אותם 10 - 20 פעמים בשנייה, התעבורה מתחילה להצטבר.

כשזה מגיע לרשת, כל בייט נחשב.

אז איך לצמצם את כמות הנתונים?

התשובה פשוטה, על ידי אי שליחת הערכים שלא צפויים להשתנות, ועל ידי ערימת סוגי ערכים פשוטים לבייט בודד.

אל תשלח ערכים שלא צפויים להשתנות

בדוגמה למעלה אנו מוסיפים את הקווטרניון של הסיבוב, המורכב מ-4 מצופים.

עם זאת, במקרה של משחק FPS, השחקן בדרך כלל מסתובב רק סביב ציר ה-Y, בידיעה שאנו יכולים להוסיף רק את הסיבוב סביב Y, ולהפחית את נתוני הסיבוב מ-16 בתים ל-4 בתים בלבד.

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

ערימת מספר בוליאנים לבייט בודד

בית הוא רצף של 8 ביטים, שלכל אחד מהם ערך אפשרי של 0 ו-1.

במקרה, ערך bool יכול להיות רק נכון או שקר. אז, עם קוד פשוט, נוכל לדחוס עד 8 ערכי bool לבייט בודד.

פתח את 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%).