multiplayer refactor

This commit is contained in:
2025-07-21 09:04:43 +02:00
parent 1054061d91
commit a0a0f6303d
29 changed files with 2186 additions and 603 deletions

View File

@@ -0,0 +1,40 @@
using System;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Netcode;
using UnityEngine.Assertions;
namespace RebootKit.Engine.Network {
public static class DataSerializationUtils {
public const int k_DefaultMessageSize = 256;
public static NativeArray<byte> Serialize<TEntity>(TEntity entity,
Allocator allocator = Allocator.Temp)
where TEntity : ISerializableEntity {
int size = entity.GetMaxBytes();
if (size < 0) {
size = k_DefaultMessageSize;
}
NativeArray<byte> data = new NativeArray<byte>(size, allocator);
using NetworkBufferWriter writer = new NetworkBufferWriter(data, 0);
Assert.IsTrue(writer.WillFit(size));
if (writer.WillFit(size)) {
entity.Serialize(writer);
return data;
}
return default;
}
public static void Deserialize<TEntity>(NativeArray<byte> data, ref TEntity entity)
where TEntity : ISerializableEntity {
using NetworkBufferReader reader = new NetworkBufferReader(data);
if (reader.HasNext(data.Length)) {
entity.Deserialize(reader);
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 07f4afdf6bb24860b2524b3250238533
timeCreated: 1752855533

View File

@@ -0,0 +1,8 @@
namespace RebootKit.Engine.Network {
public interface ISerializableEntity {
void Serialize(NetworkBufferWriter writer);
void Deserialize(NetworkBufferReader reader);
int GetMaxBytes();
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 0f726d33d24d45e7b7cadf610566622d
timeCreated: 1752855518

View File

@@ -0,0 +1,396 @@
using System;
using Unity.Collections;
using UnityEngine;
using UnityEngine.Assertions;
using UnityEngine.Pool;
namespace RebootKit.Engine.Network {
public struct NetworkBufferReader : IDisposable {
class ReaderHandle {
public NativeArray<byte> Data;
public int Position;
public bool IsBigEndian;
}
static readonly IObjectPool<ReaderHandle> s_ReaderPool = new ObjectPool<ReaderHandle>(
() => new ReaderHandle(),
_ => { },
handle => {
handle.Data = default;
handle.Position = 0;
handle.IsBigEndian = false;
},
_ => { },
true,
256
);
ReaderHandle m_Handle;
public NetworkBufferReader(NativeArray<byte> data, int position = 0) {
Assert.IsTrue(data.IsCreated, "Trying to create a NetworkBufferReader with uncreated data.");
Assert.IsTrue(position >= 0 && position <= data.Length,
"Position must be within the bounds of the data array.");
m_Handle = s_ReaderPool.Get();
m_Handle.Data = data;
m_Handle.Position = position;
m_Handle.IsBigEndian = !BitConverter.IsLittleEndian;
}
public void Dispose() {
if (m_Handle != null) {
s_ReaderPool.Release(m_Handle);
m_Handle = null;
}
}
public bool HasNext(int size) {
return m_Handle.Position + size <= m_Handle.Data.Length;
}
public bool Read(out NativeArray<byte> value, int size, Allocator allocator = Allocator.Temp) {
Assert.IsTrue(HasNext(size),
$"Not enough data to read the requested size. Requested: {size}, Available: {m_Handle.Data.Length - m_Handle.Position}");
value = new NativeArray<byte>(size, allocator);
for (int i = 0; i < size; i++) {
value[i] = m_Handle.Data[m_Handle.Position++];
}
return true;
}
public bool Read(out byte value) {
if (!HasNext(1)) {
value = 0;
return false;
}
Assert.IsTrue(HasNext(1), "Not enough data to read a byte.");
value = m_Handle.Data[m_Handle.Position++];
return true;
}
public bool Read(out bool value) {
if (!HasNext(1)) {
value = false;
return false;
}
value = m_Handle.Data[m_Handle.Position++] != 0;
return true;
}
public bool Read(out int value) {
value = 0;
if (!HasNext(4)) {
return false;
}
if (m_Handle.IsBigEndian) {
value |= m_Handle.Data[m_Handle.Position++] << 24;
value |= m_Handle.Data[m_Handle.Position++] << 16;
value |= m_Handle.Data[m_Handle.Position++] << 8;
value |= m_Handle.Data[m_Handle.Position++];
} else {
value |= m_Handle.Data[m_Handle.Position++];
value |= m_Handle.Data[m_Handle.Position++] << 8;
value |= m_Handle.Data[m_Handle.Position++] << 16;
value |= m_Handle.Data[m_Handle.Position++] << 24;
}
return true;
}
public bool Read(out short value) {
value = 0;
if (!HasNext(2)) {
return false;
}
if (m_Handle.IsBigEndian) {
value |= (short) (m_Handle.Data[m_Handle.Position++] << 8);
value |= (short) (m_Handle.Data[m_Handle.Position++]);
} else {
value |= (short) (m_Handle.Data[m_Handle.Position++]);
value |= (short) (m_Handle.Data[m_Handle.Position++] << 8);
}
return true;
}
public bool Read(out ushort value) {
value = 0;
if (!HasNext(2)) {
return false;
}
if (m_Handle.IsBigEndian) {
value |= (ushort) (m_Handle.Data[m_Handle.Position++] << 8);
value |= (ushort) (m_Handle.Data[m_Handle.Position++]);
} else {
value |= (ushort) (m_Handle.Data[m_Handle.Position++]);
value |= (ushort) (m_Handle.Data[m_Handle.Position++] << 8);
}
return true;
}
public bool Read(out long value) {
value = 0;
if (!HasNext(8)) {
return false;
}
if (m_Handle.IsBigEndian) {
value |= (long) m_Handle.Data[m_Handle.Position++] << 56;
value |= (long) m_Handle.Data[m_Handle.Position++] << 48;
value |= (long) m_Handle.Data[m_Handle.Position++] << 40;
value |= (long) m_Handle.Data[m_Handle.Position++] << 32;
value |= (long) m_Handle.Data[m_Handle.Position++] << 24;
value |= (long) m_Handle.Data[m_Handle.Position++] << 16;
value |= (long) m_Handle.Data[m_Handle.Position++] << 8;
value |= (long) m_Handle.Data[m_Handle.Position++];
} else {
value |= (long) m_Handle.Data[m_Handle.Position++];
value |= (long) m_Handle.Data[m_Handle.Position++] << 8;
value |= (long) m_Handle.Data[m_Handle.Position++] << 16;
value |= (long) m_Handle.Data[m_Handle.Position++] << 24;
value |= (long) m_Handle.Data[m_Handle.Position++] << 32;
value |= (long) m_Handle.Data[m_Handle.Position++] << 40;
value |= (long) m_Handle.Data[m_Handle.Position++] << 48;
value |= (long) m_Handle.Data[m_Handle.Position++] << 56;
}
return true;
}
public bool Read(out ulong value) {
value = 0;
if (!HasNext(8)) {
return false;
}
if (m_Handle.IsBigEndian) {
value |= (ulong) m_Handle.Data[m_Handle.Position++] << 56;
value |= (ulong) m_Handle.Data[m_Handle.Position++] << 48;
value |= (ulong) m_Handle.Data[m_Handle.Position++] << 40;
value |= (ulong) m_Handle.Data[m_Handle.Position++] << 32;
value |= (ulong) m_Handle.Data[m_Handle.Position++] << 24;
value |= (ulong) m_Handle.Data[m_Handle.Position++] << 16;
value |= (ulong) m_Handle.Data[m_Handle.Position++] << 8;
value |= (ulong) m_Handle.Data[m_Handle.Position++];
} else {
value |= (ulong) m_Handle.Data[m_Handle.Position++];
value |= (ulong) m_Handle.Data[m_Handle.Position++] << 8;
value |= (ulong) m_Handle.Data[m_Handle.Position++] << 16;
value |= (ulong) m_Handle.Data[m_Handle.Position++] << 24;
value |= (ulong) m_Handle.Data[m_Handle.Position++] << 32;
value |= (ulong) m_Handle.Data[m_Handle.Position++] << 40;
value |= (ulong) m_Handle.Data[m_Handle.Position++] << 48;
value |= (ulong) m_Handle.Data[m_Handle.Position++] << 56;
}
return true;
}
public bool Read(out float value) {
if (Read(out int intValue)) {
value = System.BitConverter.Int32BitsToSingle(intValue);
return true;
}
value = 0.0f;
return false;
}
public bool Read(out Vector2 value) {
Assert.IsTrue(HasNext(sizeof(float) * 2), "Not enough data to read a Vector2.");
if (Read(out float x) && Read(out float y)) {
value = new Vector2(x, y);
return true;
}
value = Vector2.zero;
return false;
}
public bool Read(out Vector3 value) {
Assert.IsTrue(HasNext(sizeof(float) * 3), "Not enough data to read a Vector3.");
if (Read(out float x) && Read(out float y) && Read(out float z)) {
value = new Vector3(x, y, z);
return true;
}
value = Vector3.zero;
return false;
}
public bool Read(out Vector4 value) {
Assert.IsTrue(HasNext(sizeof(float) * 4), "Not enough data to read a Vector4.");
if (Read(out float x) && Read(out float y) && Read(out float z) && Read(out float w)) {
value = new Vector4(x, y, z, w);
return true;
}
value = Vector4.zero;
return false;
}
public bool Read(out Quaternion value) {
Assert.IsTrue(HasNext(sizeof(float) * 4), "Not enough data to read a Quaternion.");
if (Read(out float x) && Read(out float y) && Read(out float z) && Read(out float w)) {
value = new Quaternion(x, y, z, w);
return true;
}
value = Quaternion.identity;
return false;
}
public bool Read(out FixedString32Bytes value) {
Assert.IsTrue(HasNext(32), "Not enough data to read a FixedString32Bytes.");
NativeArray<byte> tempData = new NativeArray<byte>(32, Allocator.Temp);
value = new FixedString32Bytes();
int length = 0;
for (int i = 0; i < 32; i++) {
Read(out byte byteValue);
tempData[i] = byteValue;
if (byteValue != 0) {
length++;
}
}
value.Length = length;
for (int i = 0; i < length; i++) {
value[i] = tempData[i];
}
tempData.Dispose();
return true;
}
public bool Read(out FixedString64Bytes value) {
Assert.IsTrue(HasNext(64), "Not enough data to read a FixedString64Bytes.");
NativeArray<byte> tempData = new NativeArray<byte>(64, Allocator.Temp);
value = new FixedString64Bytes();
int length = 0;
for (int i = 0; i < 64; i++) {
Read(out byte byteValue);
tempData[i] = byteValue;
if (byteValue != 0) {
length++;
}
}
value.Length = length;
for (int i = 0; i < length; i++) {
value[i] = tempData[i];
}
tempData.Dispose();
return true;
}
public bool Read(out FixedString128Bytes value) {
Assert.IsTrue(HasNext(128), "Not enough data to read a FixedString128Bytes.");
NativeArray<byte> tempData = new NativeArray<byte>(128, Allocator.Temp);
value = new FixedString128Bytes();
int length = 0;
for (int i = 0; i < 128; i++) {
Read(out byte byteValue);
tempData[i] = byteValue;
if (byteValue != 0) {
length++;
}
}
value.Length = length;
for (int i = 0; i < length; i++) {
value[i] = tempData[i];
}
tempData.Dispose();
return true;
}
public bool Read(out FixedString512Bytes value) {
Assert.IsTrue(HasNext(512), "Not enough data to read a FixedString512Bytes.");
NativeArray<byte> tempData = new NativeArray<byte>(512, Allocator.Temp);
value = new FixedString512Bytes();
int length = 0;
for (int i = 0; i < 512; i++) {
Read(out byte byteValue);
tempData[i] = byteValue;
if (byteValue != 0) {
length++;
}
}
value.Length = length;
for (int i = 0; i < length; i++) {
value[i] = tempData[i];
}
tempData.Dispose();
return true;
}
public bool Read(out FixedString4096Bytes value) {
Assert.IsTrue(HasNext(4096), "Not enough data to read a FixedString4096Bytes.");
NativeArray<byte> tempData = new NativeArray<byte>(4096, Allocator.Temp);
value = new FixedString4096Bytes();
int length = 0;
for (int i = 0; i < 4096; i++) {
Read(out byte byteValue);
tempData[i] = byteValue;
if (byteValue != 0) {
length++;
}
}
value.Length = length;
for (int i = 0; i < length; i++) {
value[i] = tempData[i];
}
tempData.Dispose();
return true;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 15e108857a064cd28bde3e3a8dfe749e
timeCreated: 1752858133

View File

@@ -0,0 +1,316 @@
using System;
using Unity.Collections;
using UnityEngine;
using UnityEngine.Assertions;
using UnityEngine.Pool;
namespace RebootKit.Engine.Network {
// @NOTE: Data is written in a linear fashion, so the position is always at the end of the written data.
// We are writting everything in little-endian format.
public struct NetworkBufferWriter : IDisposable {
class WriterHandle {
public NativeArray<byte> Data;
public bool IsOwner; // Indicates if this handle owns the data and should dispose it.
public int Position;
public int Capacity;
}
static readonly IObjectPool<WriterHandle> s_WriterPool = new ObjectPool<WriterHandle>(
() => new WriterHandle(),
_ => { },
handle => {
if (handle.Data.IsCreated && handle.IsOwner) {
handle.Data.Dispose();
}
handle.Data = default;
handle.Position = 0;
handle.Capacity = 0;
},
handle => {
if (handle.Data.IsCreated && handle.IsOwner) {
handle.Data.Dispose();
}
},
true,
256
);
WriterHandle m_Handle;
public int Position {
get {
return m_Handle.Position;
}
set {
Assert.IsTrue(value >= 0 && value <= m_Handle.Capacity, "Position must be within the bounds of the buffer.");
m_Handle.Position = value;
}
}
public NetworkBufferWriter(int capacity, Allocator allocator) {
m_Handle = s_WriterPool.Get();
m_Handle.Data = new NativeArray<byte>(capacity, allocator);
m_Handle.IsOwner = true;
m_Handle.Capacity = capacity;
m_Handle.Position = 0;
}
public NetworkBufferWriter(NativeArray<byte> buffer, int position) {
m_Handle = s_WriterPool.Get();
m_Handle.Data = buffer;
m_Handle.IsOwner = false;
m_Handle.Capacity = buffer.Length;
m_Handle.Position = position;
}
public void Dispose() {
if (m_Handle != null) {
s_WriterPool.Release(m_Handle);
m_Handle = null;
}
}
public bool WillFit(int size) {
return m_Handle.Position + size <= m_Handle.Capacity;
}
public void Write(byte value) {
if (m_Handle.Position >= m_Handle.Capacity) {
throw new InvalidOperationException("Buffer overflow: Cannot write beyond capacity.");
}
m_Handle.Data[m_Handle.Position++] = value;
}
public void Write(byte[] values) {
Assert.IsNotNull(values, "Trying to write null byte array to the buffer.");
Assert.IsTrue(WillFit(values.Length), "Buffer overflow: Cannot write beyond capacity.");
if (values.Length == 0) {
return;
}
for (int i = 0; i < values.Length; i++) {
m_Handle.Data[m_Handle.Position++] = values[i];
}
}
public void Write(NativeArray<byte> values) {
Assert.IsTrue(values.IsCreated, "Trying to write uncreated NativeArray to the buffer.");
Assert.IsTrue(WillFit(values.Length), "Buffer overflow: Cannot write beyond capacity.");
if (values.Length == 0) {
return;
}
for (int i = 0; i < values.Length; i++) {
m_Handle.Data[m_Handle.Position++] = values[i];
}
}
public void Write(int value) {
Assert.IsTrue(sizeof(int) == 4, "Size of int must be 4 bytes.");
Assert.IsTrue(WillFit(sizeof(int)), "Buffer overflow: Cannot write beyond capacity.");
if (BitConverter.IsLittleEndian) {
Write((byte) (value & 0xFF));
Write((byte) ((value >> 8) & 0xFF));
Write((byte) ((value >> 16) & 0xFF));
Write((byte) ((value >> 24) & 0xFF));
} else {
Write((byte) ((value >> 24) & 0xFF));
Write((byte) ((value >> 16) & 0xFF));
Write((byte) ((value >> 8) & 0xFF));
Write((byte) (value & 0xFF));
}
}
public void Write(short value) {
Assert.IsTrue(sizeof(short) == 2, "Size of short must be 2 bytes.");
Assert.IsTrue(WillFit(sizeof(short)), "Buffer overflow: Cannot write beyond capacity.");
if (BitConverter.IsLittleEndian) {
Write((byte) (value & 0xFF));
Write((byte) ((value >> 8) & 0xFF));
} else {
Write((byte) ((value >> 8) & 0xFF));
Write((byte) (value & 0xFF));
}
}
public void Write(ushort value) {
Assert.IsTrue(sizeof(ushort) == 2, "Size of ushort must be 2 bytes.");
Assert.IsTrue(WillFit(sizeof(ushort)), "Buffer overflow: Cannot write beyond capacity.");
if (BitConverter.IsLittleEndian) {
Write((byte) (value & 0xFF));
Write((byte) ((value >> 8) & 0xFF));
} else {
Write((byte) ((value >> 8) & 0xFF));
Write((byte) (value & 0xFF));
}
}
public void Write(long value) {
Assert.IsTrue(sizeof(long) == 8, "Size of long must be 8 bytes.");
Assert.IsTrue(WillFit(sizeof(long)), "Buffer overflow: Cannot write beyond capacity.");
if (BitConverter.IsLittleEndian) {
Write((byte) (value & 0xFF));
Write((byte) ((value >> 8) & 0xFF));
Write((byte) ((value >> 16) & 0xFF));
Write((byte) ((value >> 24) & 0xFF));
Write((byte) ((value >> 32) & 0xFF));
Write((byte) ((value >> 40) & 0xFF));
Write((byte) ((value >> 48) & 0xFF));
Write((byte) ((value >> 56) & 0xFF));
} else {
Write((byte) ((value >> 56) & 0xFF));
Write((byte) ((value >> 48) & 0xFF));
Write((byte) ((value >> 40) & 0xFF));
Write((byte) ((value >> 32) & 0xFF));
Write((byte) ((value >> 24) & 0xFF));
Write((byte) ((value >> 16) & 0xFF));
Write((byte) ((value >> 8) & 0xFF));
Write((byte) (value & 0xFF));
}
}
public void Write(ulong value) {
Assert.IsTrue(sizeof(ulong) == 8, "Size of ulong must be 8 bytes.");
Assert.IsTrue(WillFit(sizeof(ulong)), "Buffer overflow: Cannot write beyond capacity.");
if (BitConverter.IsLittleEndian) {
Write((byte) (value & 0xFF));
Write((byte) ((value >> 8) & 0xFF));
Write((byte) ((value >> 16) & 0xFF));
Write((byte) ((value >> 24) & 0xFF));
Write((byte) ((value >> 32) & 0xFF));
Write((byte) ((value >> 40) & 0xFF));
Write((byte) ((value >> 48) & 0xFF));
Write((byte) ((value >> 56) & 0xFF));
} else {
Write((byte) ((value >> 56) & 0xFF));
Write((byte) ((value >> 48) & 0xFF));
Write((byte) ((value >> 40) & 0xFF));
Write((byte) ((value >> 32) & 0xFF));
Write((byte) ((value >> 24) & 0xFF));
Write((byte) ((value >> 16) & 0xFF));
Write((byte) ((value >> 8) & 0xFF));
Write((byte) (value & 0xFF));
}
}
public void Write(float value) {
Assert.IsTrue(sizeof(float) == 4, "Size of float must be 4 bytes.");
Assert.IsTrue(WillFit(sizeof(float)), "Buffer overflow: Cannot write beyond capacity.");
unsafe {
byte* bytes = (byte*) &value;
Write(bytes[0]);
Write(bytes[1]);
Write(bytes[2]);
Write(bytes[3]);
}
}
public void Write(bool value) {
Assert.IsTrue(WillFit(1), "Buffer overflow: Cannot write beyond capacity.");
Write((byte) (value ? 1 : 0));
}
public void Write(Vector2 value) {
Assert.IsTrue(WillFit(sizeof(float) * 2), "Buffer overflow: Cannot write beyond capacity.");
Write(value.x);
Write(value.y);
}
public void Write(Vector3 value) {
Assert.IsTrue(WillFit(sizeof(float) * 3), "Buffer overflow: Cannot write beyond capacity.");
Write(value.x);
Write(value.y);
Write(value.z);
}
public void Write(Vector4 value) {
Assert.IsTrue(WillFit(sizeof(float) * 4), "Buffer overflow: Cannot write beyond capacity.");
Write(value.x);
Write(value.y);
Write(value.z);
Write(value.w);
}
public void Write(Quaternion value) {
Assert.IsTrue(WillFit(sizeof(float) * 4), "Buffer overflow: Cannot write beyond capacity.");
Write(value.x);
Write(value.y);
Write(value.z);
Write(value.w);
}
public void Write(FixedString32Bytes value) {
Assert.IsTrue(WillFit(32));
for (int i = 0; i < 32; i++) {
if (i < value.Length) {
Write(value[i]);
} else {
Write((byte) 0); // Fill with zero if the string is shorter than 32 bytes
}
}
}
public void Write(FixedString64Bytes value) {
Assert.IsTrue(WillFit(64));
for (int i = 0; i < 64; i++) {
if (i < value.Length) {
Write(value[i]);
} else {
Write((byte) 0); // Fill with zero if the string is shorter than 64 bytes
}
}
}
public void Write(FixedString128Bytes value) {
Assert.IsTrue(WillFit(128));
for (int i = 0; i < 128; i++) {
if (i < value.Length) {
Write(value[i]);
} else {
Write((byte) 0); // Fill with zero if the string is shorter than 128 bytes
}
}
}
public void Write(FixedString512Bytes value) {
Assert.IsTrue(WillFit(512));
for (int i = 0; i < 512; i++) {
if (i < value.Length) {
Write(value[i]);
} else {
Write((byte) 0); // Fill with zero if the string is shorter than 512 bytes
}
}
}
public void Write(FixedString4096Bytes value) {
Assert.IsTrue(WillFit(4096));
for (int i = 0; i < 4096; i++) {
if (i < value.Length) {
Write(value[i]);
} else {
Write((byte) 0); // Fill with zero if the string is shorter than 4096 bytes
}
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b2f038461ab549e48a5f3c6d59e92f9d
timeCreated: 1752855725

View File

@@ -0,0 +1,281 @@
using System;
using System.Collections.Generic;
using RebootKit.Engine.Foundation;
using RebootKit.Engine.Simulation;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine.Assertions;
using UnityEngine.Pool;
namespace RebootKit.Engine.Network {
struct NetworkPacketHeader : ISerializableEntity {
public int MagicNumber;
public ushort Version;
public ushort EntityCount;
public static int GetEntityCountOffset() {
return sizeof(int) + sizeof(ushort);
}
public void Serialize(NetworkBufferWriter writer) {
writer.Write(MagicNumber);
writer.Write(Version);
writer.Write(EntityCount);
}
public void Deserialize(NetworkBufferReader reader) {
reader.Read(out MagicNumber);
reader.Read(out Version);
reader.Read(out EntityCount);
}
public int GetMaxBytes() {
return sizeof(int) + sizeof(ushort) * 2; // MagicNumber, Version, EntityCount
}
}
class NetworkPacket : IDisposable {
public static readonly IObjectPool<NetworkPacket> Pool = new ObjectPool<NetworkPacket>(
() => {
NetworkPacket packet = new NetworkPacket();
packet.Data = default;
packet.Writer = default;
return packet;
},
packet => {
// Packet is initialized after being retrieved from the pool
},
packet => {
packet.Dispose();
},
packet => {
packet.Dispose();
},
true,
16
);
public NativeArray<byte> Data;
public NetworkBufferWriter Writer;
public ushort EntityCount { get; private set; }
public void IncrementEntityCount() {
int originalPosition = Writer.Position;
EntityCount += 1;
Writer.Position = NetworkPacketHeader.GetEntityCountOffset(); // Reset position to write the entity count
Writer.Write(EntityCount);
Writer.Position = originalPosition;
}
public void Dispose() {
Data.Dispose();
Writer.Dispose();
EntityCount = 0;
}
}
enum NetworkDataType : byte {
None = 0x00,
ActorCoreState = 0x01,
ActorTransformSync = 0x02,
ActorState = 0x03,
ActorEvent = 0x04,
ActorCommand = 0x05,
SynchronizeActor = 0x07,
SpawnActor = 0x08,
}
struct NetworkDataHeader : ISerializableEntity {
public NetworkDataType Type;
public ulong ActorID;
public byte CommandID;
public byte EventID;
public int DataSize;
public void Serialize(NetworkBufferWriter writer) {
writer.Write((byte) Type);
writer.Write(ActorID);
writer.Write(CommandID);
writer.Write(EventID);
writer.Write(DataSize);
}
public void Deserialize(NetworkBufferReader reader) {
reader.Read(out byte typeByte);
Type = (NetworkDataType) typeByte;
reader.Read(out ActorID);
reader.Read(out CommandID);
reader.Read(out EventID);
reader.Read(out DataSize);
}
public int GetMaxBytes() {
return sizeof(ulong) + sizeof(byte) * 3 + sizeof(int);
}
}
class NetworkPacketQueue : IDisposable {
static readonly Logger s_Logger = new Logger(nameof(NetworkPacketQueue));
readonly int m_PacketMaxSize;
readonly ushort m_Version;
internal readonly List<NetworkPacket> NetworkPackets = new List<NetworkPacket>();
public NetworkPacketQueue(int packetMaxSize, ushort version = 2137) {
m_PacketMaxSize = packetMaxSize;
m_Version = version;
Assert.IsTrue(m_PacketMaxSize > 0, "Packet maximum size must be greater than zero.");
}
public void Dispose() {
foreach (NetworkPacket packet in NetworkPackets) {
packet.Data.Dispose();
}
NetworkPackets.Clear();
}
public void Clear() {
foreach (NetworkPacket packet in NetworkPackets) {
packet.Dispose();
}
NetworkPackets.Clear();
}
public void WriteActorState(ulong actorID, IActorData entity) {
Assert.IsTrue(entity.GetMaxBytes() <= m_PacketMaxSize,
$"Entity size {entity.GetMaxBytes()} exceeds packet max size {m_PacketMaxSize}.");
NetworkDataHeader header = new NetworkDataHeader {
Type = NetworkDataType.ActorState,
ActorID = actorID,
DataSize = entity.GetMaxBytes()
};
int bytesToWrite = header.GetMaxBytes() + entity.GetMaxBytes();
NetworkPacket packet = GetPacketToWriteTo(bytesToWrite);
header.Serialize(packet.Writer);
entity.Serialize(packet.Writer);
packet.IncrementEntityCount();
}
public void WriteActorTransformState(ulong actorID, ActorTransformSyncData transformData) {
NetworkDataHeader header = new NetworkDataHeader {
Type = NetworkDataType.ActorTransformSync,
ActorID = actorID,
DataSize = transformData.GetMaxBytes()
};
int bytesToWrite = header.GetMaxBytes() + transformData.GetMaxBytes();
NetworkPacket packet = GetPacketToWriteTo(bytesToWrite);
header.Serialize(packet.Writer);
transformData.Serialize(packet.Writer);
packet.IncrementEntityCount();
}
public void WriteActorCoreState(ulong actorID, ActorCoreStateSnapshot coreState) {
NetworkDataHeader header = new NetworkDataHeader {
Type = NetworkDataType.ActorCoreState,
ActorID = actorID,
DataSize = coreState.GetMaxBytes()
};
int bytesToWrite = header.GetMaxBytes() + coreState.GetMaxBytes();
NetworkPacket packet = GetPacketToWriteTo(bytesToWrite);
header.Serialize(packet.Writer);
coreState.Serialize(packet.Writer);
packet.IncrementEntityCount();
}
public void WriteSpawnActor(FixedString64Bytes assetGUID,
ulong actorID,
ActorCoreStateSnapshot coreState,
IActorData actorData) {
NetworkDataHeader header = new NetworkDataHeader {
Type = NetworkDataType.SpawnActor,
ActorID = actorID,
DataSize = 0
};
header.DataSize += sizeof(byte) * 64; // assetGUID
header.DataSize += coreState.GetMaxBytes();
header.DataSize += sizeof(ushort);
header.DataSize += actorData.GetMaxBytes();
NetworkPacket packet = GetPacketToWriteTo(header.GetMaxBytes() + header.DataSize);
header.Serialize(packet.Writer);
packet.Writer.Write(assetGUID);
coreState.Serialize(packet.Writer);
packet.Writer.Write((ushort) actorData.GetMaxBytes());
actorData.Serialize(packet.Writer);
packet.IncrementEntityCount();
}
public void WriteActorSynchronize(ulong actorID,
ActorCoreStateSnapshot coreState,
IActorData actorData) {
NetworkDataHeader header = new NetworkDataHeader {
Type = NetworkDataType.SynchronizeActor,
ActorID = actorID,
DataSize = 0
};
header.DataSize += coreState.GetMaxBytes();
header.DataSize += sizeof(ushort);
header.DataSize += actorData.GetMaxBytes();
NetworkPacket packet = GetPacketToWriteTo(header.GetMaxBytes() + header.DataSize);
header.Serialize(packet.Writer);
coreState.Serialize(packet.Writer);
packet.Writer.Write((ushort) actorData.GetMaxBytes());
actorData.Serialize(packet.Writer);
packet.IncrementEntityCount();
}
NetworkPacket GetPacketToWriteTo(int bytesToWrite) {
foreach (NetworkPacket networkPacket in NetworkPackets) {
if (networkPacket.Writer.WillFit(bytesToWrite)) {
return networkPacket;
}
}
Assert.IsTrue(bytesToWrite < m_PacketMaxSize,
$"Packet size {bytesToWrite} exceeds maximum allowed size {m_PacketMaxSize}.");
NetworkPacket packet = NetworkPacket.Pool.Get();
packet.Data = new NativeArray<byte>(m_PacketMaxSize, Allocator.Persistent);
unsafe {
void* ptr = packet.Data.GetUnsafePtr();
UnsafeUtility.MemClear(ptr, sizeof(byte) * packet.Data.Length);
}
packet.Writer = new NetworkBufferWriter(packet.Data, 0);
NetworkPacketHeader header = new NetworkPacketHeader {
MagicNumber = RConsts.k_NetworkPacketMagicNumber,
Version = m_Version,
EntityCount = 0 // Will be updated later
};
header.Serialize(packet.Writer);
NetworkPackets.Add(packet);
return packet;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 8a122573c79c4b3e9ef3bc2da3b09faa
timeCreated: 1752855419

View File

@@ -0,0 +1,81 @@
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using RebootKit.Engine.Foundation;
using RebootKit.Engine.Main;
using RebootKit.Engine.Simulation;
using Unity.Netcode;
namespace RebootKit.Engine.Network {
public abstract class NetworkPlayerController : NetworkBehaviour {
static readonly Logger s_Logger = new Logger(nameof(NetworkPlayerController));
ulong m_ActorIDToPossess;
public Actor PossessedActor { get; private set; }
public void PossessActor(Actor actor) {
if (!IsServer) {
s_Logger.Error("PossessActor can only be called on the server.");
return;
}
if (actor == null) {
s_Logger.Error("Cannot possess a null actor.");
return;
}
PossessActorRpc(actor.ActorID, RpcTarget.Everyone);
}
[Rpc(SendTo.SpecifiedInParams)]
void PossessActorRpc(ulong actorID, RpcParams rpcParams) {
if (PossessedActor is not null) {
OnUnpossessActor(PossessedActor);
}
WaitForActorToSpawnThenPossessAsync(actorID, destroyCancellationToken).Forget();
}
async UniTask WaitForActorToSpawnThenPossessAsync(ulong actorID, CancellationToken cancellationToken) {
Actor actor = null;
while (actor == null) {
actor = RR.FindSpawnedActor(actorID);
await UniTask.WaitForSeconds(0.5f, cancellationToken: cancellationToken);
if (cancellationToken.IsCancellationRequested) {
return;
}
}
PossessedActor = actor;
OnPossessActor(actor);
}
public void UnPossessActor() {
if (!IsServer) {
s_Logger.Error("UnPossessActor can only be called on the server.");
return;
}
if (PossessedActor == null) {
return;
}
UnPossessActorRpc(RpcTarget.Everyone);
}
[Rpc(SendTo.SpecifiedInParams)]
void UnPossessActorRpc(RpcParams rpcParams) {
if (PossessedActor is not null) {
OnUnpossessActor(PossessedActor);
PossessedActor = null;
}
}
protected virtual void OnPossessActor(Actor actor) {
}
protected virtual void OnUnpossessActor(Actor actor) {
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2010b0aabd68415bb4aea2846d2c59b1
timeCreated: 1751208839

View File

@@ -0,0 +1,515 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using Cysharp.Threading.Tasks;
using RebootKit.Engine.Extensions;
using RebootKit.Engine.Foundation;
using RebootKit.Engine.Main;
using RebootKit.Engine.Simulation;
using Unity.Collections;
using Unity.Netcode;
using UnityEngine;
using UnityEngine.Assertions;
using Logger = RebootKit.Engine.Foundation.Logger;
namespace RebootKit.Engine.Network {
enum NetworkClientSyncState {
NotReady,
LoadingWorld,
PreparingForActorsSync,
SyncingActors,
Ready
}
struct NetworkClientState : INetworkSerializable {
public ulong ClientID;
public NetworkClientSyncState SyncState;
public int ActorsSyncPacketsLeft;
public NetworkPacketQueue ReliableQueue;
public NetworkPacketQueue UnreliableQueue;
public bool IsReady {
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get {
return SyncState == NetworkClientSyncState.Ready;
}
}
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter {
serializer.SerializeValue(ref ClientID);
serializer.SerializeValue(ref SyncState);
serializer.SerializeValue(ref ActorsSyncPacketsLeft);
}
}
struct NetworkPacketTarget {
public enum Type {
AllClients,
Single
}
public Type TargetType;
public ulong ClientID;
public static NetworkPacketTarget AllClients() {
return new NetworkPacketTarget {
TargetType = Type.AllClients,
ClientID = 0
};
}
public static NetworkPacketTarget Single(ulong clientID) {
return new NetworkPacketTarget {
TargetType = Type.Single,
ClientID = clientID
};
}
}
public class NetworkSystem : NetworkBehaviour {
[ConfigVar("sv.tick_rate", 32, "Server tick rate in Hz", CVarFlags.Server)]
public static ConfigVar TickRate;
static readonly Logger s_Logger = new Logger(nameof(NetworkSystem));
[field: SerializeField] public ActorsManager Actors { get; private set; }
internal readonly Dictionary<ulong, NetworkClientState> Clients = new Dictionary<ulong, NetworkClientState>();
public FixedString512Bytes WorldID { get; private set; } = new FixedString512Bytes("");
bool m_IsChangingWorld = false;
float m_TickTimer;
public ulong TickCount { get; private set; }
public event Action<ulong> ServerTick = delegate { };
public ulong LocalClientID {
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get {
return NetworkManager.Singleton.LocalClientId;
}
}
NetworkPacketQueue m_ReliablePacketQueue;
NetworkPacketQueue m_UnreliablePacketQueue;
//
// @MARK: Unity callbacks
//
void Awake() {
RR.NetworkSystemInstance = this;
m_ReliablePacketQueue = new NetworkPacketQueue(1024 * 4);
m_UnreliablePacketQueue = new NetworkPacketQueue(1024);
}
void Update() {
float deltaTime = Time.deltaTime;
float serverDeltaTime = 1.0f / TickRate.IndexValue;
m_TickTimer += deltaTime;
while (m_TickTimer >= serverDeltaTime) {
m_TickTimer -= serverDeltaTime;
if (RR.IsServer()) {
Actors.ServerTick(serverDeltaTime);
ServerTick?.Invoke(TickCount);
TickCount++;
FlushNetworkPackets();
}
}
}
//
// @MARK: NetworkBehaviour callbacks
//
public override void OnNetworkSpawn() {
base.OnNetworkSpawn();
NetworkManager.Singleton.OnClientConnectedCallback += OnClientConnected;
NetworkManager.Singleton.OnClientDisconnectCallback += OnClientDisconnect;
}
public override void OnNetworkDespawn() {
base.OnNetworkDespawn();
NetworkManager.Singleton.OnClientConnectedCallback -= OnClientConnected;
NetworkManager.Singleton.OnClientDisconnectCallback -= OnClientDisconnect;
}
void OnClientConnected(ulong clientID) {
if (!IsServer) {
return;
}
s_Logger.Info($"OnClientConnected: {clientID}");
NetworkClientState newClientState = new NetworkClientState {
ClientID = clientID,
SyncState = NetworkClientSyncState.NotReady,
ReliableQueue = new NetworkPacketQueue(1024 * 4),
UnreliableQueue = new NetworkPacketQueue(512)
};
Clients.Add(clientID, newClientState);
if (clientID != NetworkManager.Singleton.LocalClientId) {
foreach (NetworkClientState state in Clients.Values) {
UpdateClientStateRpc(state, RpcTarget.Single(clientID, RpcTargetUse.Temp));
}
}
if (!WorldID.IsEmpty) {
s_Logger.Info($"Synchronizing world load for client {clientID} with world ID '{WorldID}'");
ClientLoadWorldRpc(WorldID.ToString(), RpcTarget.Single(clientID, RpcTargetUse.Temp));
}
}
void OnClientDisconnect(ulong clientID) {
s_Logger.Info($"OnClientDisconnect: {clientID}");
Clients.Remove(clientID);
}
//
// @MARK: Server API
//
public void KickClient(ulong clientID, string reason = "Kicked by server") {
if (!IsServer) {
s_Logger.Error("Only server can kick clients.");
return;
}
if (NetworkManager.Singleton.ConnectedClients.TryGetValue(clientID, out NetworkClient client)) {
NetworkManager.Singleton.DisconnectClient(clientID, reason);
s_Logger.Info($"Kicked client {clientID}: {reason}");
} else {
s_Logger.Error($"Client {clientID} not found.");
}
}
public void SetCurrentWorld(string worldID) {
if (!IsServer) {
s_Logger.Error("Only server can set the current world.");
return;
}
if (m_IsChangingWorld) {
s_Logger.Error($"Already changing world to '{WorldID}'. Please wait until the current world change is complete.");
return;
}
WorldConfigAsset worldConfigAsset = RR.GetWorldConfigAsset(worldID);
if (worldConfigAsset is null) {
s_Logger.Error($"Failed to set current world: World config asset for '{worldID}' not found.");
return;
}
WorldID = worldID;
foreach ((ulong _, NetworkClientState clientState) in Clients.ToList()) {
NetworkClientState state = clientState;
state.SyncState = NetworkClientSyncState.LoadingWorld;
UpdateClientState(state);
}
ServerLoadWorldAsync(worldConfigAsset, destroyCancellationToken).Forget();
}
async UniTask ServerLoadWorldAsync(WorldConfigAsset asset, CancellationToken cancellationToken) {
s_Logger.Info($"ServerLoadWorldAsync: {asset.Config.name}");
m_IsChangingWorld = true;
RR.World.Unload();
RR.CloseMainMenu();
await RR.World.LoadAsync(asset.Config, cancellationToken);
m_IsChangingWorld = false;
if (!TryGetClientState(NetworkManager.Singleton.LocalClientId, out NetworkClientState localClientState)) {
s_Logger.Error($"Local client state not found for client ID {NetworkManager.Singleton.LocalClientId}.");
RR.Disconnect();
return;
}
localClientState.SyncState = NetworkClientSyncState.Ready;
UpdateClientState(localClientState);
RR.GameInstance.PlayerBecameReady(localClientState.ClientID);
ClientLoadWorldRpc(asset.name, RpcTarget.NotMe);
}
[Rpc(SendTo.SpecifiedInParams, Delivery = RpcDelivery.Reliable)]
void ClientLoadWorldRpc(string worldID, RpcParams rpcParams) {
WorldConfigAsset worldConfigAsset = RR.GetWorldConfigAsset(worldID);
if (worldConfigAsset is null) {
s_Logger.Error($"World config asset for '{worldID}' not found.");
RR.Disconnect();
return;
}
ClientLoadWorldAsync(worldID, destroyCancellationToken).Forget();
}
async UniTask ClientLoadWorldAsync(string worldID, CancellationToken cancellationToken) {
s_Logger.Info($"ClientLoadWorldAsync: {worldID}");
WorldConfigAsset worldConfigAsset = RR.GetWorldConfigAsset(worldID);
if (worldConfigAsset is null) {
s_Logger.Error($"World config asset for '{worldID}' not found.");
return;
}
RR.World.Unload();
RR.CloseMainMenu();
await RR.World.LoadAsync(worldConfigAsset.Config, cancellationToken);
WorldID = worldID;
ClientLoadedWorldRpc(worldID);
}
[Rpc(SendTo.Server, Delivery = RpcDelivery.Reliable)]
void ClientLoadedWorldRpc(string worldID, RpcParams rpcParams = default) {
ulong clientID = rpcParams.Receive.SenderClientId;
if (!WorldID.Equals(worldID)) {
s_Logger.Error($"Client {clientID} tried to load world '{worldID}', but server is in world '{WorldID}'.");
NetworkManager.Singleton.DisconnectClient(clientID, "World mismatch!");
return;
}
if (Clients.TryGetValue(clientID, out NetworkClientState clientState)) {
Actors.InitializeActorsForClient(clientID);
} else {
NetworkManager.Singleton.DisconnectClient(clientID, "Client is not registered!");
}
}
//
// @MARK: Internal
//
internal bool TryGetClientState(ulong clientID, out NetworkClientState clientState) {
return Clients.TryGetValue(clientID, out clientState);
}
internal void UpdateClientState(NetworkClientState clientState) {
if (!IsServer) {
s_Logger.Error("UpdateClientState can only be called on the server.");
return;
}
Clients[clientState.ClientID] = clientState;
UpdateClientStateRpc(clientState, RpcTarget.NotServer);
}
[Rpc(SendTo.SpecifiedInParams, Delivery = RpcDelivery.Reliable)]
void UpdateClientStateRpc(NetworkClientState newState, RpcParams rpcParams) {
Clients[newState.ClientID] = newState;
}
internal void ClientSynchronizedActors(ulong clientID) {
if (TryGetClientState(clientID, out NetworkClientState state)) {
state.SyncState = NetworkClientSyncState.Ready;
UpdateClientState(state);
RR.GameInstance.PlayerBecameReady(clientID);
} else {
s_Logger.Error($"Client state for {clientID} not found.");
}
}
internal int GetReadyClientsCount() {
int count = 0;
foreach (NetworkClientState clientState in Clients.Values) {
if (clientState.IsReady) {
count++;
}
}
return count;
}
//
// @MARK: Network packets
//
NetworkPacketQueue GetPacketQueue(NetworkPacketTarget target, bool reliable) {
if (target.TargetType == NetworkPacketTarget.Type.AllClients) {
return reliable ? m_ReliablePacketQueue : m_UnreliablePacketQueue;
}
if (target.TargetType == NetworkPacketTarget.Type.Single) {
if (TryGetClientState(target.ClientID, out NetworkClientState clientState)) {
return reliable ? clientState.ReliableQueue : clientState.UnreliableQueue;
}
s_Logger.Error($"Client state for {target.ClientID} not found.");
return null;
}
s_Logger.Error($"Invalid network packet target type: {target.TargetType}");
return null;
}
internal void WriteActorState(NetworkPacketTarget target, ulong actorID, IActorData actorData) {
NetworkPacketQueue queue = GetPacketQueue(target, true);
queue.WriteActorState(actorID, actorData);
}
internal void WriteActorTransformState(NetworkPacketTarget target,
ulong actorID,
ActorTransformSyncData transformData) {
NetworkPacketQueue queue = GetPacketQueue(target, false);
queue.WriteActorTransformState(actorID, transformData);
}
internal void WriteActorCoreState(NetworkPacketTarget target,
ulong actorID,
ActorCoreStateSnapshot coreData) {
NetworkPacketQueue queue = GetPacketQueue(target, true);
queue.WriteActorCoreState(actorID, coreData);
}
internal void WriteSpawnActor(NetworkPacketTarget target,
string assetGUID,
ulong actorID,
ActorCoreStateSnapshot coreState,
IActorData actorData) {
NetworkPacketQueue queue = GetPacketQueue(target, true);
queue.WriteSpawnActor(assetGUID, actorID, coreState, actorData);
}
internal void WriteActorSynchronize(NetworkPacketTarget target,
ulong actorID,
ActorCoreStateSnapshot coreState,
IActorData actorData) {
NetworkPacketQueue queue = GetPacketQueue(target, true);
queue.WriteActorSynchronize(actorID, coreState, actorData);
}
void FlushNetworkPackets() {
if (!RR.IsServer()) {
return;
}
foreach (NetworkPacket networkPacket in m_ReliablePacketQueue.NetworkPackets) {
foreach ((ulong clientID, NetworkClientState state) in RR.NetworkSystemInstance.Clients) {
if (clientID == NetworkManager.Singleton.LocalClientId) {
continue;
}
if (networkPacket.EntityCount == 0) {
continue;
}
if (state.IsReady) {
ReliableReceiveNetworkPacketRpc(networkPacket.Data,
RpcTarget.Single(clientID, RpcTargetUse.Temp));
}
}
}
foreach (NetworkPacket networkPacket in m_UnreliablePacketQueue.NetworkPackets) {
foreach ((ulong clientID, NetworkClientState state) in RR.NetworkSystemInstance.Clients) {
if (clientID == NetworkManager.Singleton.LocalClientId) {
continue;
}
if (networkPacket.EntityCount == 0) {
continue;
}
if (state.IsReady) {
UnreliableReceiveNetworkPacketRpc(networkPacket.Data,
RpcTarget.Single(clientID, RpcTargetUse.Temp));
}
}
}
m_ReliablePacketQueue.Clear();
m_UnreliablePacketQueue.Clear();
foreach (NetworkClientState clientState in Clients.Values) {
if (clientState.ClientID == NetworkManager.Singleton.LocalClientId) {
continue;
}
foreach (NetworkPacket networkPacket in clientState.ReliableQueue.NetworkPackets) {
if (networkPacket.EntityCount == 0) {
continue;
}
ReliableReceiveNetworkPacketRpc(networkPacket.Data,
RpcTarget.Single(clientState.ClientID, RpcTargetUse.Temp));
}
foreach (NetworkPacket networkPacket in clientState.UnreliableQueue.NetworkPackets) {
if (networkPacket.EntityCount == 0) {
continue;
}
UnreliableReceiveNetworkPacketRpc(networkPacket.Data,
RpcTarget.Single(clientState.ClientID, RpcTargetUse.Temp));
}
clientState.ReliableQueue.Clear();
clientState.UnreliableQueue.Clear();
}
}
[Rpc(SendTo.SpecifiedInParams, Delivery = RpcDelivery.Reliable)]
void ReliableReceiveNetworkPacketRpc(NativeArray<byte> data, RpcParams rpcParams) {
OnReceivedNetworkPacket(data);
}
[Rpc(SendTo.SpecifiedInParams, Delivery = RpcDelivery.Unreliable)]
void UnreliableReceiveNetworkPacketRpc(NativeArray<byte> data, RpcParams rpcParams) {
OnReceivedNetworkPacket(data);
}
void OnReceivedNetworkPacket(NativeArray<byte> data) {
using NetworkBufferReader reader = new NetworkBufferReader(data);
NetworkPacketHeader packetHeader = new NetworkPacketHeader();
packetHeader.Deserialize(reader);
// s_Logger.Info($"Received packet: MagicNumber={packetHeader.MagicNumber}, Version={packetHeader.Version}, EntityCount={packetHeader.EntityCount}");
Assert.IsTrue(packetHeader.MagicNumber == RConsts.k_NetworkPacketMagicNumber,
"Received packet with invalid magic number.");
if (packetHeader.EntityCount == 0) {
s_Logger.Info("Received packet with no entities.\n" + data.ToHexString());
}
for (int i = 0; i < packetHeader.EntityCount; i++) {
NetworkDataHeader dataHeader = new NetworkDataHeader();
dataHeader.Deserialize(reader);
// s_Logger.Info($"Received entity: Type={dataHeader.Type}, ActorID={dataHeader.ActorID}, DataSize={dataHeader.DataSize}");
if (dataHeader.Type == NetworkDataType.None) {
s_Logger.Info("Data of packet with entry with type None:\n" + data.ToHexString());
}
Assert.IsTrue(dataHeader.Type != NetworkDataType.None, "Received packet with invalid data type.");
reader.Read(out NativeArray<byte> entityData, dataHeader.DataSize, Allocator.Temp);
OnReceivedEntity(dataHeader, entityData);
entityData.Dispose();
}
}
void OnReceivedEntity(NetworkDataHeader header, NativeArray<byte> data) {
Actors.OnReceivedEntity(header, data);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1f967d37c17e4704b80849c305a53be9
timeCreated: 1751411566

View File

@@ -0,0 +1,6 @@
using Unity.Netcode;
namespace RebootKit.Engine.Network {
public abstract class NetworkWorldController : NetworkBehaviour {
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: edd521a69a6f4e43b97ec258adf499a6
timeCreated: 1751377120