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, và 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%).