マルチプレイヤーのデータ圧縮とビット操作
Unity でマルチプレイヤー ゲームを作成するのは簡単な作業ではありませんが、PUN 2 などのサードパーティ ソリューションの助けが必要です。 、ネットワークの統合がはるかに簡単になりました。
あるいは、ゲームのネットワーキング機能をより詳細に制御する必要がある場合は、Socket テクノロジーを使用して独自のネットワーキング ソリューションを作成することもできます (例: サーバーがプレイヤーの入力のみを受信し、その後、サーバーがプレイヤーの入力を受信する権威マルチプレイヤー)すべてのプレイヤーが同じように行動するように独自の計算を行うことで、ハッキング)の発生率が減少します。
独自のネットワークを作成しているか、既存のソリューションを使用しているかに関係なく、この投稿で説明するトピックであるデータ圧縮に留意する必要があります。
マルチプレイヤーの基本
ほとんどのマルチプレイヤー ゲームでは、プレイヤーとサーバーの間で、指定されたレートで送受信される小さなデータ バッチ (一連のバイト) の形式で通信が行われます。
Unity (特に C#) で最も一般的な値の型は int です。 float、bool、、および string (また、頻繁に変化する値を送信する場合は string の使用を避けるべきです。このタイプで最も受け入れられる用途は、チャット メッセージまたはテキストのみを含むデータです)。
- 上記のタイプはすべて、設定されたバイト数で保存されます。
int = 4バイト
float = 4バイト
bool = 1バイト
string = (エンコード形式に応じて、単一文字のエンコードに使用されるバイト数) x (文字数)
値がわかったら、標準的なマルチプレイヤー FPS (一人称視点シューティング) に送信する必要がある最小バイト量を計算してみましょう。
プレーヤーの位置: Vector3 (3 float x 4) = 12 バイト
プレーヤーの回転: クォータニオン (4 float x 4) = 16 バイト
プレーヤーのルックターゲット: Vector3 (3 float 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 バイトはそれほど多くのデータではありませんが、1 秒あたり 10 ~ 20 回送信する必要がある場合、トラフィックが増加し始めます。
ネットワークに関しては、すべてのバイトが重要です。
では、データ量を減らすにはどうすればよいでしょうか?
答えは簡単です。変更が予想されない値を送信しないことと、単純な値の型を 1 バイトにスタックすることです。
変更が予想されない値は送信しないでください
上の例では、4 つの float で構成される回転のクォータニオンを追加しています。
ただし、FPS ゲームの場合、プレーヤーは通常 Y 軸を中心に回転するだけなので、追加できるのは Y を中心とした回転だけであることがわかっているため、回転データは 16 バイトからわずか 4 バイトに減ります。
Buffer.BlockCopy(transform.localEulerAngles.y.toByteArray(), 0, packedData, 12, 4); //Local Y Rotation
複数のブール値を単一バイトにスタックする
バイトは 8 ビットのシーケンスであり、それぞれの値は 0 と 1 です。
偶然にも、ブール値は true または false のみです。したがって、単純なコードで、最大 8 つのブール値を 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% 削減)。