멀티플레이어 데이터 압축 및 비트 조작

Unity에서 멀티플레이어 게임을 만드는 것은 간단한 작업이 아니지만 PUN 2와 같은 타사 솔루션의 도움을 받습니다., 네트워킹 통합이 훨씬 쉬워졌습니다.

또는 게임의 네트워킹 기능에 대한 더 많은 제어가 필요한 경우 소켓 기술을 사용하여 고유한 네트워킹 솔루션을 작성할 수 있습니다(예: 서버가 플레이어 입력만 수신한 다음 해당 작업을 수행하는 권한 있는 멀티플레이어). 모든 플레이어가 동일한 방식으로 행동하도록 자체 계산하여 해킹) 발생률을 줄입니다.

자체 네트워킹을 작성하든 기존 솔루션을 사용하든 관계없이 이 게시물에서 논의할 주제인 데이터 압축에 유의해야 합니다.

멀티플레이어 기본 사항

대부분의 멀티플레이어 게임에서는 지정된 속도로 앞뒤로 전송되는 소규모 데이터 배치(일련의 바이트) 형태로 플레이어와 서버 간에 통신이 이루어집니다.

Unity(특히 C#)에서 가장 일반적인 값 유형은 int입니다. float, bool,string (또한 자주 변경되는 값을 보낼 때는 문자열을 사용하지 않아야 합니다. 이 유형에 가장 적합한 용도는 채팅 메시지 또는 텍스트만 포함된 데이터입니다.

  • 위의 모든 유형은 설정된 바이트 수에 저장됩니다.

int = 4바이트
float = 4바이트
bool = 1바이트
string = (인코딩 형식에 따라 단일 문자를 인코딩하는 데 사용되는 바이트 수) x (문자 수)

값을 알고 표준 멀티플레이어 FPS(1인칭 슈팅 게임)에 전송해야 하는 최소 바이트 양을 계산해 보겠습니다.

플레이어 위치: Vector3(3 부동 x 4) = 12바이트
플레이어 회전: 쿼터니언(4 부동 x 4) = 16바이트
플레이어 보기 대상: Vector3(3 부동 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(보내려는 모든 값의 바이트 합계에 해당)로 바이트 배열을 초기화합니다.

그런 다음 각 값은 바이트 배열로 변환된 다음 Buffer.BlockCopy를 사용하여 PackedData 배열에 적용됩니다.

나중에 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의 값을 가질 수 있습니다.

우연히도 bool 값은 true 또는 false만 될 수 있습니다. 그래서 간단한 코드로 최대 8개의 bool 값을 1바이트로 압축할 수 있습니다.

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% 감소).

추천 기사
Unity의 Photon Fusion 2 소개
Unity에서 멀티플레이어 네트워크 게임 구축
PUN 2로 멀티플레이어 자동차 게임 만들기
Unity, PUN 2 룸에 멀티플레이어 채팅 추가
PUN 2를 사용하여 Unity에서 멀티플레이어 게임 만들기
PHP 및 MySQL을 사용한 Unity 로그인 시스템
PUN 2를 사용하여 네트워크를 통해 강체 동기화