Багатокористувацьке стиснення даних і маніпулювання бітами

Створення багатокористувацької гри в Unity не є тривіальним завданням, але за допомогою сторонніх рішень, таких як PUN 2, це значно спростило інтеграцію в мережу.

Крім того, якщо вам потрібен більший контроль над мережевими можливостями гри, ви можете написати власне мережеве рішення, використовуючи технологію Socket (наприклад, авторитетна багатокористувацька гра, де сервер лише отримує дані від гравця, а потім виконує власні обчислення, щоб гарантувати щоб усі гравці поводилися однаково, таким чином зменшуючи випадки злому).

Незалежно від того, створюєте ви власну мережу чи використовуєте існуюче рішення, вам слід пам’ятати про тему, яку ми обговорюватимемо в цій публікації, а саме стиснення даних.

Основи багатокористувацької гри

У більшості багатокористувацьких ігор існує зв’язок, який відбувається між гравцями та сервером у формі невеликих пакетів даних (послідовність байтів), які надсилаються туди й назад із заданою швидкістю.

У UnityC# зокрема) найпоширенішими типами значень є int, float, bool, і рядок (також вам слід уникати використання рядка під час надсилання значень, які часто змінюються; найбільш прийнятним використанням цього типу є повідомлення чату або дані, які містять лише текст).

  • Усі перераховані вище типи зберігаються в заданій кількості байтів:

int = 4 байти
float = 4 байти
bool = 1 байт
рядок = (кількість байтів, використаних для кодувати один символ, залежно від формату кодування) x (Кількість символів)

Знаючи значення, давайте обчислимо мінімальну кількість байтів, які необхідно надіслати для стандартного багатокористувацького FPS (шутер від першої особи):

Позиція гравця: Vector3 (3 floats x 4) = 12 байт
Ротація гравців: Quaternion (4 floats x 4) = 16 байт
Цільовий вигляд гравця: Vector3 (3 floats 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.

За випадковим збігом, логічне значення може бути лише істинним або хибним. Отже, за допомогою простого коду ми можемо стиснути до 8 логічних значень в один байт.

Відкрийте 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%).

Рекомендовані статті
Знайомство з Photon Fusion 2 в Unity
Система входу Unity з PHP і MySQL
Створення багатокористувацьких мережевих ігор в Unity
Підручник з онлайн-таблиці лідерів Unity
Створіть багатокористувацьку автомобільну гру за допомогою PUN 2
Компенсація відставання PUN 2
Unity Додавання чату для кількох гравців до кімнат PUN 2