Kompresi Data Multi Pemain dan Manipulasi Bit

Membuat game multipemain di Unity bukanlah tugas yang mudah, tetapi dengan bantuan solusi pihak ketiga, seperti PUN 2, integrasi jaringan menjadi lebih mudah.

Atau, jika Anda memerlukan kontrol lebih besar atas kemampuan jaringan game, Anda dapat menulis solusi jaringan Anda sendiri menggunakan teknologi Socket (mis. multipemain otoritatif, di mana server hanya menerima masukan pemain dan kemudian melakukan penghitungannya sendiri untuk memastikan bahwa semua pemain berperilaku dengan cara yang sama, sehingga mengurangi kejadian hacking).

Terlepas dari apakah Anda membuat jaringan Anda sendiri atau menggunakan solusi yang sudah ada, Anda harus memperhatikan topik yang akan kita bahas dalam posting ini, yaitu kompresi data.

Dasar-dasar Multipemain

Di sebagian besar game multipemain, terdapat komunikasi yang terjadi antara pemain dan server, dalam bentuk kumpulan data kecil (urutan byte), yang dikirim bolak-balik dengan kecepatan tertentu.

Dalam Unity (dan C# secara khusus), tipe nilai yang paling umum adalah int, float, bool, dan string (juga, Anda harus menghindari penggunaan string saat mengirim nilai yang sering berubah, penggunaan yang paling dapat diterima untuk jenis ini adalah pesan chat atau data yang hanya berisi teks).

  • Semua tipe di atas disimpan dalam sejumlah byte:

int = 4 bytes
float = 4 bytes
bool = 1 byte
string = (Jumlah byte yang digunakan untuk mengkodekan satu karakter, tergantung pada format pengkodean) x (Jumlah karakter)

Mengetahui nilainya, mari kita hitung jumlah minimum byte yang perlu dikirim untuk FPS multipemain standar (First-Person Shooter):

Posisi pemain: Vector3 (3 float x 4) = 12 byte
Rotasi pemain: Quaternion (4 float x 4) = 16 byte
Target tampilan pemain: Vector3 (3 float x 4) = 12 byte
Pemain menembak: bool = 1 byte
Pemain di udara: bool = 1 byte
Pemain berjongkok: bool = 1 byte
Pemain berlari: bool = 1 byte

Total 44 byte.

Kami akan menggunakan metode ekstensi untuk mengemas data ke dalam array byte, dan sebaliknya:

  • Buat skrip baru, beri nama SC_ByteMethods lalu tempel kode di bawah ini di dalamnya:

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

Contoh penggunaan metode di atas:

  • Buat skrip baru, beri nama SC_TestPackUnpack lalu tempel kode di bawah ini di dalamnya:

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

Script di atas menginisialisasi array byte dengan panjang 44 (yang sesuai dengan jumlah byte dari semua nilai yang ingin kita kirim).

Setiap nilai kemudian diubah menjadi array byte, kemudian diterapkan ke dalam arraypackedData menggunakan Buffer.BlockCopy.

NantinyapackedData dikonversi kembali ke nilai menggunakan metode ekstensi dari SC_ByteMethods.cs.

Teknik Kompresi Data

Secara obyektif, 44 byte bukanlah data yang banyak, namun jika perlu dikirim 10 - 20 kali per detik, lalu lintas mulai bertambah.

Dalam hal jaringan, setiap byte berarti.

Lalu bagaimana cara mengurangi jumlah data?

Jawabannya sederhana, dengan tidak mengirimkan nilai yang diperkirakan tidak akan berubah, dan dengan menumpuk tipe nilai sederhana ke dalam satu byte.

Jangan Kirim Nilai yang Diperkirakan Tidak Akan Berubah

Pada contoh di atas kita menambahkan angka empat dari rotasi yang terdiri dari 4 pelampung.

Namun, dalam kasus game FPS, pemain biasanya hanya memutar di sekitar sumbu Y, mengetahui bahwa, kita hanya dapat menambahkan rotasi di sekitar Y, sehingga mengurangi data rotasi dari 16 byte menjadi hanya 4 byte.

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

Tumpuk Beberapa Boolean Menjadi Satu Byte

Satu byte adalah urutan 8 bit, masing-masing dengan kemungkinan nilai 0 dan 1.

Secara kebetulan, nilai bool hanya bisa benar atau salah. Jadi, dengan kode sederhana, kita dapat mengompresi hingga 8 nilai bool menjadi satu byte.

Buka SC_ByteMethods.cs lalu tambahkan kode di bawah ini sebelum kurung kurawal penutup terakhir '}'

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

Kode SC_TestPackUnpack yang diperbarui:

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

Dengan metode di atas, kami telah mengurangi panjangpackedData dari 44 menjadi 29 byte (pengurangan 34%).

Artikel yang Disarankan
Pengantar Foton Fusion 2 di Unity
Buat Game Mobil Multi Pemain dengan PUN 2
Tutorial Papan Peringkat Online Unity
Sistem Login Unity Dengan PHP dan MySQL
Buat Game Multiplayer di Unity menggunakan PUN 2
Panduan Pemula Jaringan Foton (Klasik).
Membangun Game Jaringan Multipemain dalam Unity