Çok Oyunculu Veri Sıkıştırma ve Bit İşleme

Unity'te çok oyunculu bir oyun oluşturmak önemsiz bir iş değildir, ancak PUN 2 gibi üçüncü taraf çözümlerin yardımıyla ağ entegrasyonunu çok daha kolay hale getirmiştir.

Alternatif olarak, oyunun ağ oluşturma özellikleri üzerinde daha fazla kontrole ihtiyacınız varsa, Socket teknolojisini kullanarak kendi ağ oluşturma çözümünüzü yazabilirsiniz (örneğin, sunucunun yalnızca oyuncu girişini aldığı ve daha sonra kendi hesaplamalarını yaptığı yetkili çok oyunculu mod). tüm oyuncuların aynı şekilde davranması, böylece hackleme olasılığının azalması.

İster kendi ağınızı yazıyor olun ister mevcut bir çözümü kullanıyor olun, bu yazıda tartışacağımız konu olan veri sıkıştırma konusuna dikkat etmelisiniz.

Çok Oyunculu Temeller

Çok oyunculu oyunların çoğunda, oyuncular ile sunucu arasında, belirli bir hızda ileri geri gönderilen küçük veri yığınları (bir bayt dizisi) biçiminde gerçekleşen bir iletişim vardır.

Unity'te (ve özellikle C#), en yaygın değer türleri şunlardır: int, float, bool, ve string (ayrıca sık sık değişen değerleri gönderirken dize kullanmaktan kaçınmalısınız; bu türün en kabul edilebilir kullanımı sohbet mesajları veya yalnızca metin içeren verilerdir).

  • Yukarıdaki türlerin tümü belirli sayıda baytta saklanır:

int = 4 bayt
float = 4 bayt
bool = 1 bayt
string = (Kullanılan bayt sayısı kodlama formatına bağlı olarak tek bir karakteri kodlayın) x (Karakter sayısı)

Değerleri bilerek, standart bir çok oyunculu FPS (First-Person Shooter) için gönderilmesi gereken minimum bayt miktarını hesaplayalım:

Oyuncu konumu: Vector3 (3 kayan nokta x 4) = 12 bayt
Oyuncu rotasyonu: Quaternion (4 kayan nokta x 4) = 16 bayt
Oyuncu görünüm hedefi: Vector3 (3 kayan nokta x 4) = 12 bayt
Oyuncu Ateşleme: bool = 1 bayt
Oyuncu havada: bool = 1 bayt
Çömelmiş oyuncu: bool = 1 bayt
Oyuncu koşuyor: bool = 1 bayt

Toplam 44 bayt.

Verileri bir bayt dizisine paketlemek için uzantı yöntemlerini kullanacağız (veya tam tersi):

  • yeni bir komut dosyası oluşturun, onu SC_ByteMethods olarak adlandırın ve içine aşağıdaki kodu yapıştırın:

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

Yukarıdaki yöntemlerin örnek kullanımı:

  • yeni bir komut dosyası oluşturun, SC_TestPackUnpack olarak adlandırın ve içine aşağıdaki kodu yapıştırın:

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

Yukarıdaki betik, bayt dizisini 44 uzunluğunda başlatır (bu, göndermek istediğimiz tüm değerlerin bayt toplamına karşılık gelir).

Her değer daha sonra bayt dizilerine dönüştürülür ve ardından Buffer.BlockCopy kullanılarak packageedData dizisine uygulanır.

Daha sonra, packageData, SC_ByteMethods.cs'deki uzantı yöntemleri kullanılarak değerlere geri dönüştürülür.

Veri Sıkıştırma Teknikleri

Nesnel olarak, 44 bayt çok fazla veri değildir, ancak saniyede 10 - 20 kez gönderilmesi gerekiyorsa trafik artmaya başlar.

Ağ oluşturma söz konusu olduğunda her bayt önemlidir.

Peki veri miktarı nasıl azaltılır?

Değişmesi beklenmeyen değerleri göndermemek ve basit değer türlerini tek bir bayta yığmak için cevap basittir.

Değişmesi Beklenmeyen Değerleri Göndermeyin

Yukarıdaki örnekte, 4 kayan noktadan oluşan rotasyonun Kuaterniyonunu ekliyoruz.

Bununla birlikte, bir FPS oyunu söz konusu olduğunda, oyuncu genellikle yalnızca Y ekseni etrafında döner, bunu bilerek, yalnızca Y etrafındaki dönüşü ekleyebileceğimizi ve dönüş verilerini 16 bayttan sadece 4 bayta düşürebileceğimizi biliriz.

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

Birden Fazla Boolean'ı Tek Bir Bayta Yığınlayın

Bir bayt, her birinin olası değeri 0 ve 1 olan 8 bitlik bir dizidir.

Tesadüfen bool değeri yalnızca doğru veya yanlış olabilir. Yani basit bir kodla 8'e kadar bool değerini tek bir byte'a sıkıştırabiliriz.

SC_ByteMethods.cs dosyasını açın ve aşağıdaki kodu son kapanış parantezi '}'nin önüne ekleyin

    //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 kodu güncellendi:

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

Yukarıdaki yöntemlerle, packageData uzunluğunu 44 byte'tan 29 byte'a düşürdük (%34 azalma).

Önerilen Makaleler
Unity'de Çok Oyunculu Ağ Bağlantılı Oyunlar Oluşturma
PUN 2 ile Çok Oyunculu Araba Oyunu Yapın
Unity, PUN 2 Odalarına Çok Oyunculu Sohbet Ekliyor
PUN 2'yi kullanarak Unity'de Çok Oyunculu Bir Oyun Yapın
PUN 2'yi Kullanarak Sert Cisimleri Ağ Üzerinden Senkronize Etme
Photon Network (Klasik) Başlangıç ​​Kılavuzu
Unity Çevrimiçi Skor Tablosu Eğitimi