Nén dữ liệu nhiều người chơi và thao tác bit

Tạo trò chơi nhiều người chơi trong Unity không phải là một nhiệm vụ đơn giản nhưng với sự trợ giúp của các giải pháp của bên thứ ba, chẳng hạn như PUN 2, nó đã giúp việc tích hợp mạng trở nên dễ dàng hơn nhiều.

Ngoài ra, nếu bạn cần kiểm soát nhiều hơn khả năng kết nối mạng của trò chơi, bạn có thể viết giải pháp mạng của riêng mình bằng cách sử dụng công nghệ Socket (ví dụ: nhiều người chơi có thẩm quyền, trong đó máy chủ chỉ nhận thông tin đầu vào của người chơi và sau đó thực hiện các phép tính riêng để đảm bảo rằng tất cả người chơi đều hành xử theo cùng một cách, do đó làm giảm tỷ lệ xảy ra hacking).

Bất kể bạn đang viết mạng của riêng mình hay sử dụng giải pháp hiện có, bạn nên lưu ý đến chủ đề mà chúng ta sẽ thảo luận trong bài đăng này, đó là nén dữ liệu.

Thông tin cơ bản về nhiều người chơi

Trong hầu hết các trò chơi nhiều người chơi, có sự giao tiếp xảy ra giữa người chơi và máy chủ, dưới dạng các lô dữ liệu nhỏ (một chuỗi byte), được gửi qua lại với tốc độ xác định.

Trong Unity (và cụ thể là C#), các loại giá trị phổ biến nhất là int, float, bool, string (đồng thời, bạn nên tránh sử dụng chuỗi khi gửi các giá trị thay đổi thường xuyên, cách sử dụng được chấp nhận nhất đối với loại này là tin nhắn trò chuyện hoặc dữ liệu chỉ chứa văn bản).

  • Tất cả các loại trên được lưu trữ trong một số byte nhất định:

int = 4 bytes
float = 4 bytes
bool = 1 byte
string = (Số byte được sử dụng để mã hóa một ký tự đơn, tùy thuộc vào định dạng mã hóa) x (Số ký tự)

Biết các giá trị, hãy tính số byte tối thiểu cần được gửi cho FPS nhiều người chơi tiêu chuẩn (Bắn súng góc nhìn thứ nhất):

Vị trí của người chơi: Vector3 (3 float x 4) = 12 byte
Xoay vòng người chơi: Quaternion (4 float x 4) = 16 byte
Mục tiêu nhìn của người chơi: Vector3 (3 float x 4) = 12 byte
Player bắn: bool = 1 byte
Người chơi trên không: bool = 1 byte
Người chơi cúi mình: bool = 1 byte
Người chơi đang chạy: bool = 1 byte

Tổng cộng 44 byte.

Chúng tôi sẽ sử dụng các phương thức mở rộng để đóng gói dữ liệu thành một mảng byte và ngược lại:

  • Tạo một tập lệnh mới, đặt tên là SC_ByteMethods rồi dán đoạn mã bên dưới vào trong tập lệnh đó:

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

Ví dụ sử dụng các phương pháp trên:

  • Tạo một tập lệnh mới, đặt tên là SC_TestPackUnpack rồi dán mã bên dưới vào trong tập lệnh đó:

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

Đoạn script trên khởi tạo mảng byte có độ dài 44 (tương ứng với tổng byte của tất cả các giá trị mà chúng ta muốn gửi).

Sau đó, mỗi giá trị được chuyển đổi thành mảng byte, rồi áp dụng vào mảng dữ liệu đóng gói bằng cách sử dụng Buffer.BlockCopy.

Sau đó, dữ liệu đóng gói được chuyển đổi trở lại thành giá trị bằng các phương thức mở rộng từ SC_ByteMethods.cs.

Kỹ thuật nén dữ liệu

Về mặt khách quan, 44 byte không phải là nhiều dữ liệu, nhưng nếu cần gửi 10 - 20 lần mỗi giây, lưu lượng sẽ bắt đầu tăng lên.

Khi nói đến kết nối mạng, mỗi byte đều có giá trị.

Vậy làm cách nào để giảm lượng dữ liệu?

Câu trả lời rất đơn giản, bằng cách không gửi các giá trị không được mong đợi sẽ thay đổi và bằng cách xếp chồng các loại giá trị đơn giản thành một byte đơn.

Không gửi các giá trị dự kiến ​​sẽ không thay đổi

Trong ví dụ trên, chúng ta đang thêm Quaternion của phép quay, bao gồm 4 phao.

Tuy nhiên, trong trường hợp game FPS, người chơi thường chỉ xoay quanh trục Y, biết rằng, chúng ta chỉ có thể thêm phép quay quanh Y, giảm dữ liệu xoay từ 16 byte xuống chỉ còn 4 byte.

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

Xếp chồng nhiều Boolean thành một byte đơn

Một byte là một chuỗi gồm 8 bit, mỗi bit có thể có giá trị là 0 và 1.

Thật trùng hợp, giá trị bool chỉ có thể đúng hoặc sai. Vì vậy, với một mã đơn giản, chúng ta có thể nén tối đa 8 giá trị bool thành một byte.

Mở SC_ByteMethods.cs sau đó thêm mã bên dưới trước dấu ngoặc nhọn đóng cuối cùng '}'

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

Mã SC_TestPackUnpack đã cập nhật:

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

Với các phương pháp trên, chúng tôi đã giảm độ dài dữ liệu đóng gói từ 44 xuống 29 byte (giảm 34%).

Bài viết được đề xuất
Xây dựng các trò chơi nối mạng nhiều người chơi trong Unity
Tạo trò chơi nhiều người chơi trong Unity bằng PUN 2
Giới thiệu về Photon Fusion 2 trong Unity
Tạo trò chơi ô tô nhiều người chơi với PUN 2
Unity Thêm trò chuyện nhiều người chơi vào phòng PUN 2
Hướng dẫn cho người mới bắt đầu về Mạng Photon (Cổ điển)
Hướng dẫn về bảng xếp hạng trực tuyến Unity