multiplayer refactor
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
using System.Text;
|
||||
using RebootKit.Engine.Main;
|
||||
using RebootKit.Engine.Network;
|
||||
using RebootKit.Engine.Simulation;
|
||||
using RebootKit.Engine.UI;
|
||||
using UnityEngine;
|
||||
@@ -48,6 +49,8 @@ namespace RebootKit.Engine.Development {
|
||||
m_StringBuilder.Append(resolution.height);
|
||||
m_StringBuilder.Append("@");
|
||||
m_StringBuilder.Append(resolution.refreshRateRatio);
|
||||
m_StringBuilder.Append("Hz | IsLittleEndian: ");
|
||||
m_StringBuilder.Append(System.BitConverter.IsLittleEndian ? "true" : "false");
|
||||
m_StringBuilder.AppendLine();
|
||||
}
|
||||
|
||||
@@ -60,6 +63,7 @@ namespace RebootKit.Engine.Development {
|
||||
}
|
||||
|
||||
m_StringBuilder.Append($"IsServer: {RR.IsServer().ToString()}");
|
||||
m_StringBuilder.Append($" | TickRate: {NetworkSystem.TickRate.IndexValue.ToString()}");
|
||||
m_StringBuilder.Append($" | IsClient: {RR.IsClient().ToString()}");
|
||||
m_StringBuilder.Append($" | WorldID: {network.WorldID.ToString()}");
|
||||
m_StringBuilder.Append($" | Clients: {network.Clients.Count.ToString()}");
|
||||
|
||||
19
Runtime/Engine/Code/Extensions/NativeArrayEx.cs
Normal file
19
Runtime/Engine/Code/Extensions/NativeArrayEx.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System.Text;
|
||||
using Unity.Collections;
|
||||
|
||||
namespace RebootKit.Engine.Extensions {
|
||||
public static class NativeArrayEx {
|
||||
public static string ToHexString(this NativeArray<byte> array) {
|
||||
if (array.IsCreated) {
|
||||
StringBuilder sb = new StringBuilder(array.Length * 3);
|
||||
for (int i = 0; i < array.Length; i++) {
|
||||
sb.AppendFormat("{0:X2} ", array[i]);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
3
Runtime/Engine/Code/Extensions/NativeArrayEx.cs.meta
Normal file
3
Runtime/Engine/Code/Extensions/NativeArrayEx.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2921b0badb164eef8dae6f95a6e4c002
|
||||
timeCreated: 1753068953
|
||||
@@ -1,273 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using RebootKit.Engine.Simulation;
|
||||
using Unity.Collections;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
using Logger = RebootKit.Engine.Foundation.Logger;
|
||||
|
||||
namespace RebootKit.Engine.Main {
|
||||
enum NetworkClientSyncState {
|
||||
NotReady,
|
||||
LoadingWorld,
|
||||
PreparingForActorsSync,
|
||||
SyncingActors,
|
||||
Ready
|
||||
}
|
||||
|
||||
struct NetworkClientState : INetworkSerializable {
|
||||
public ulong ClientID;
|
||||
public NetworkClientSyncState SyncState;
|
||||
public int ActorsSyncPacketsLeft;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
public class NetworkSystem : NetworkBehaviour {
|
||||
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;
|
||||
|
||||
public ulong LocalClientID {
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get {
|
||||
return NetworkManager.Singleton.LocalClientId;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// @MARK: Unity callbacks
|
||||
//
|
||||
void Awake() {
|
||||
RR.NetworkSystemInstance = this;
|
||||
}
|
||||
|
||||
//
|
||||
// @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
|
||||
};
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ using R3;
|
||||
using RebootKit.Engine.Console;
|
||||
using RebootKit.Engine.Foundation;
|
||||
using RebootKit.Engine.Input;
|
||||
using RebootKit.Engine.Network;
|
||||
using RebootKit.Engine.Simulation;
|
||||
using RebootKit.Engine.Steam;
|
||||
using Unity.Netcode;
|
||||
@@ -26,19 +27,16 @@ namespace RebootKit.Engine.Main {
|
||||
static readonly Logger s_Logger = new Logger("RR");
|
||||
|
||||
[ConfigVar("con.write_log", 1, "Enables writing game log to console output")]
|
||||
static ConfigVar s_writeLogToConsole;
|
||||
|
||||
[ConfigVar("sv.tick_rate", 24, "Server tick rate in Hz")]
|
||||
public static ConfigVar TickRate;
|
||||
|
||||
static ConfigVar s_WriteLogToConsole;
|
||||
|
||||
internal static EngineConfigAsset EngineConfig;
|
||||
|
||||
static DisposableBag s_disposableBag;
|
||||
static DisposableBag s_servicesBag;
|
||||
static DisposableBag s_DisposableBag;
|
||||
static DisposableBag s_ServicesBag;
|
||||
|
||||
static AsyncOperationHandle<SceneInstance> s_mainMenuSceneHandle;
|
||||
static AsyncOperationHandle<SceneInstance> s_MainMenuSceneHandle;
|
||||
|
||||
static NetworkSystem s_networkSystemPrefab;
|
||||
static NetworkSystem s_NetworkSystemPrefab;
|
||||
internal static NetworkSystem NetworkSystemInstance;
|
||||
|
||||
internal static ConsoleService Console { get; private set; }
|
||||
@@ -49,10 +47,6 @@ namespace RebootKit.Engine.Main {
|
||||
|
||||
public static Game GameInstance { get; internal set; }
|
||||
|
||||
public static ulong TickCount { get; private set; }
|
||||
public static event Action<ulong> ServerTick = delegate { };
|
||||
public static event Action ClientTick = delegate { };
|
||||
|
||||
// Lifecycle API
|
||||
|
||||
// @NOTE: This method is called at the very start of the game, when boot scene loaded.
|
||||
@@ -62,13 +56,13 @@ namespace RebootKit.Engine.Main {
|
||||
EngineConfig = configAsset;
|
||||
|
||||
s_Logger.Info("Initializing");
|
||||
s_servicesBag = new DisposableBag();
|
||||
s_disposableBag = new DisposableBag();
|
||||
s_ServicesBag = new DisposableBag();
|
||||
s_DisposableBag = new DisposableBag();
|
||||
|
||||
s_Logger.Info("Registering core services");
|
||||
Console = CreateService<ConsoleService>();
|
||||
Input = new InputService(EngineConfig.inputConfig);
|
||||
s_servicesBag.Add(Input);
|
||||
s_ServicesBag.Add(Input);
|
||||
World = CreateService<WorldService>();
|
||||
|
||||
await InitializeAssetsAsync(cancellationToken);
|
||||
@@ -80,7 +74,7 @@ namespace RebootKit.Engine.Main {
|
||||
|
||||
// @NOTE: This method is called after the main scene is loaded.
|
||||
internal static async UniTask RunAsync(CancellationToken cancellationToken) {
|
||||
s_networkSystemPrefab =
|
||||
s_NetworkSystemPrefab =
|
||||
Resources.Load<NetworkSystem>(RConsts.k_CoreNetworkGameSystemsResourcesPath);
|
||||
|
||||
NetworkManager.Singleton.OnConnectionEvent += OnConnectionEvent;
|
||||
@@ -97,7 +91,7 @@ namespace RebootKit.Engine.Main {
|
||||
|
||||
Observable.EveryUpdate()
|
||||
.Subscribe(_ => Tick())
|
||||
.AddTo(ref s_disposableBag);
|
||||
.AddTo(ref s_DisposableBag);
|
||||
|
||||
await OpenMainMenuAsync(cancellationToken);
|
||||
|
||||
@@ -133,8 +127,8 @@ namespace RebootKit.Engine.Main {
|
||||
SteamManager.Shutdown();
|
||||
#endif
|
||||
|
||||
s_servicesBag.Dispose();
|
||||
s_disposableBag.Dispose();
|
||||
s_ServicesBag.Dispose();
|
||||
s_DisposableBag.Dispose();
|
||||
}
|
||||
|
||||
// Assets API
|
||||
@@ -176,16 +170,16 @@ namespace RebootKit.Engine.Main {
|
||||
return;
|
||||
}
|
||||
|
||||
s_mainMenuSceneHandle = Addressables.LoadSceneAsync(EngineConfig.mainMenuScene, LoadSceneMode.Additive);
|
||||
await s_mainMenuSceneHandle;
|
||||
s_MainMenuSceneHandle = Addressables.LoadSceneAsync(EngineConfig.mainMenuScene, LoadSceneMode.Additive);
|
||||
await s_MainMenuSceneHandle;
|
||||
}
|
||||
|
||||
internal static void CloseMainMenu() {
|
||||
if (!s_mainMenuSceneHandle.IsValid()) {
|
||||
if (!s_MainMenuSceneHandle.IsValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Addressables.UnloadSceneAsync(s_mainMenuSceneHandle);
|
||||
Addressables.UnloadSceneAsync(s_MainMenuSceneHandle);
|
||||
}
|
||||
|
||||
public static void SetServerWorld(string worldID) {
|
||||
@@ -253,13 +247,13 @@ namespace RebootKit.Engine.Main {
|
||||
}
|
||||
|
||||
TService service = asset.Create();
|
||||
s_servicesBag.Add(service);
|
||||
s_ServicesBag.Add(service);
|
||||
return service;
|
||||
}
|
||||
|
||||
public static TService CreateService<TService>() where TService : class, IService {
|
||||
TService service = Activator.CreateInstance<TService>();
|
||||
s_servicesBag.Add(service);
|
||||
s_ServicesBag.Add(service);
|
||||
return service;
|
||||
}
|
||||
|
||||
@@ -382,28 +376,8 @@ namespace RebootKit.Engine.Main {
|
||||
|
||||
GameInstance.SendChatMessageRpc(message);
|
||||
}
|
||||
|
||||
static float s_tickTimer;
|
||||
|
||||
|
||||
static void Tick() {
|
||||
float deltaTime = Time.deltaTime;
|
||||
|
||||
float minTickTime = 1.0f / TickRate.IndexValue;
|
||||
s_tickTimer += deltaTime;
|
||||
|
||||
while (s_tickTimer >= minTickTime) {
|
||||
s_tickTimer -= minTickTime;
|
||||
|
||||
if (IsServer()) {
|
||||
ServerTick?.Invoke(TickCount);
|
||||
}
|
||||
|
||||
if (IsClient()) {
|
||||
ClientTick?.Invoke();
|
||||
}
|
||||
|
||||
TickCount++;
|
||||
}
|
||||
}
|
||||
|
||||
static void OnConnectionEvent(NetworkManager network, ConnectionEventData data) {
|
||||
@@ -416,7 +390,7 @@ namespace RebootKit.Engine.Main {
|
||||
GameInstance = Object.Instantiate(EngineConfig.gamePrefab);
|
||||
GameInstance.NetworkObject.Spawn();
|
||||
|
||||
NetworkSystemInstance = Object.Instantiate(s_networkSystemPrefab);
|
||||
NetworkSystemInstance = Object.Instantiate(s_NetworkSystemPrefab);
|
||||
NetworkSystemInstance.NetworkObject.Spawn();
|
||||
}
|
||||
|
||||
|
||||
3
Runtime/Engine/Code/Network.meta
Normal file
3
Runtime/Engine/Code/Network.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b01874b8658349da857b43006deab7d1
|
||||
timeCreated: 1752855354
|
||||
40
Runtime/Engine/Code/Network/DataSerializationUtils.cs
Normal file
40
Runtime/Engine/Code/Network/DataSerializationUtils.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 07f4afdf6bb24860b2524b3250238533
|
||||
timeCreated: 1752855533
|
||||
8
Runtime/Engine/Code/Network/ISerializableEntity.cs
Normal file
8
Runtime/Engine/Code/Network/ISerializableEntity.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace RebootKit.Engine.Network {
|
||||
public interface ISerializableEntity {
|
||||
void Serialize(NetworkBufferWriter writer);
|
||||
void Deserialize(NetworkBufferReader reader);
|
||||
|
||||
int GetMaxBytes();
|
||||
}
|
||||
}
|
||||
3
Runtime/Engine/Code/Network/ISerializableEntity.cs.meta
Normal file
3
Runtime/Engine/Code/Network/ISerializableEntity.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0f726d33d24d45e7b7cadf610566622d
|
||||
timeCreated: 1752855518
|
||||
396
Runtime/Engine/Code/Network/NetworkBufferReader.cs
Normal file
396
Runtime/Engine/Code/Network/NetworkBufferReader.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Runtime/Engine/Code/Network/NetworkBufferReader.cs.meta
Normal file
3
Runtime/Engine/Code/Network/NetworkBufferReader.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 15e108857a064cd28bde3e3a8dfe749e
|
||||
timeCreated: 1752858133
|
||||
316
Runtime/Engine/Code/Network/NetworkBufferWriter.cs
Normal file
316
Runtime/Engine/Code/Network/NetworkBufferWriter.cs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Runtime/Engine/Code/Network/NetworkBufferWriter.cs.meta
Normal file
3
Runtime/Engine/Code/Network/NetworkBufferWriter.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b2f038461ab549e48a5f3c6d59e92f9d
|
||||
timeCreated: 1752855725
|
||||
281
Runtime/Engine/Code/Network/NetworkPacketQueue.cs
Normal file
281
Runtime/Engine/Code/Network/NetworkPacketQueue.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Runtime/Engine/Code/Network/NetworkPacketQueue.cs.meta
Normal file
3
Runtime/Engine/Code/Network/NetworkPacketQueue.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8a122573c79c4b3e9ef3bc2da3b09faa
|
||||
timeCreated: 1752855419
|
||||
@@ -1,13 +1,18 @@
|
||||
using RebootKit.Engine.Foundation;
|
||||
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.Main {
|
||||
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.");
|
||||
@@ -21,19 +26,27 @@ namespace RebootKit.Engine.Main {
|
||||
|
||||
PossessActorRpc(actor.ActorID, RpcTarget.Everyone);
|
||||
}
|
||||
|
||||
|
||||
[Rpc(SendTo.SpecifiedInParams)]
|
||||
void PossessActorRpc(ulong actorID, RpcParams rpcParams) {
|
||||
Actor actor = RR.FindSpawnedActor(actorID);
|
||||
if (actor == null) {
|
||||
s_Logger.Error($"Actor with ID {actorID} not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
515
Runtime/Engine/Code/Network/NetworkSystem.cs
Normal file
515
Runtime/Engine/Code/Network/NetworkSystem.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using Unity.Netcode;
|
||||
|
||||
namespace RebootKit.Engine.Main {
|
||||
namespace RebootKit.Engine.Network {
|
||||
public abstract class NetworkWorldController : NetworkBehaviour {
|
||||
}
|
||||
}
|
||||
@@ -19,5 +19,8 @@
|
||||
internal const string k_CVarsFilename = k_FilenamePrefix + "cvars.txt";
|
||||
|
||||
internal const string k_BuildFlagDebug = "RR_DEBUG";
|
||||
internal const string k_BuildFlagSteam = "RR_STEAM";
|
||||
|
||||
internal const int k_NetworkPacketMagicNumber = 0x52455245; // "RERE" in ASCII
|
||||
}
|
||||
}
|
||||
@@ -3,66 +3,23 @@ using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
using RebootKit.Engine.Foundation;
|
||||
using RebootKit.Engine.Main;
|
||||
using RebootKit.Engine.Network;
|
||||
using TriInspector;
|
||||
using Unity.Collections;
|
||||
using Unity.Collections.LowLevel.Unsafe;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
using Logger = RebootKit.Engine.Foundation.Logger;
|
||||
|
||||
namespace RebootKit.Engine.Simulation {
|
||||
public interface ISerializableEntity {
|
||||
void Serialize(FastBufferWriter writer);
|
||||
void Deserialize(FastBufferReader reader);
|
||||
|
||||
// @NOTE: -1 means use the default size and have hope it will fit.
|
||||
int MinimumSizeInBytes() { return -1; }
|
||||
}
|
||||
|
||||
public interface IActorData : ISerializableEntity { }
|
||||
|
||||
public class NoActorData : IActorData {
|
||||
public void Serialize(FastBufferWriter writer) { }
|
||||
public void Serialize(NetworkBufferWriter writer) { }
|
||||
|
||||
public void Deserialize(FastBufferReader reader) { }
|
||||
}
|
||||
public void Deserialize(NetworkBufferReader reader) { }
|
||||
|
||||
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.MinimumSizeInBytes();
|
||||
if (size < 0) {
|
||||
size = k_DefaultMessageSize;
|
||||
}
|
||||
|
||||
using FastBufferWriter writer = new FastBufferWriter(size, allocator);
|
||||
if (writer.TryBeginWrite(size)) {
|
||||
entity.Serialize(writer);
|
||||
|
||||
int length = writer.Length;
|
||||
NativeArray<byte> data = new NativeArray<byte>(length, allocator);
|
||||
|
||||
unsafe {
|
||||
void* dst = data.GetUnsafePtr();
|
||||
void* src = writer.GetUnsafePtr();
|
||||
Buffer.MemoryCopy(src, dst, length, length);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
public static void Deserialize<TEntity>(NativeArray<byte> data, ref TEntity entity)
|
||||
where TEntity : ISerializableEntity {
|
||||
using FastBufferReader reader = new FastBufferReader(data, Allocator.Temp);
|
||||
if (reader.TryBeginRead(data.Length)) {
|
||||
entity.Deserialize(reader);
|
||||
}
|
||||
public int GetMaxBytes() {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +52,8 @@ namespace RebootKit.Engine.Simulation {
|
||||
}
|
||||
}
|
||||
|
||||
// @NOTE: ActorEvent is used to send events from the server to clients and only clients.
|
||||
// Server should not receive ActorEvents.
|
||||
public struct ActorEvent : INetworkSerializable {
|
||||
public ulong ActorID;
|
||||
public ulong ClientID;
|
||||
@@ -131,7 +90,7 @@ namespace RebootKit.Engine.Simulation {
|
||||
DisableColliders = 1 << 1,
|
||||
}
|
||||
|
||||
struct ActorCoreStateSnapshot : INetworkSerializable {
|
||||
struct ActorCoreStateSnapshot : ISerializableEntity {
|
||||
public DateTime Timestamp;
|
||||
|
||||
// @NOTE: Position, Rotation, and Scale are in local space.
|
||||
@@ -145,15 +104,39 @@ namespace RebootKit.Engine.Simulation {
|
||||
public ulong MasterActorID;
|
||||
public FixedString32Bytes MasterSocketName;
|
||||
|
||||
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter {
|
||||
serializer.SerializeValue(ref Timestamp);
|
||||
serializer.SerializeValue(ref Position);
|
||||
serializer.SerializeValue(ref Rotation);
|
||||
serializer.SerializeValue(ref Scale);
|
||||
serializer.SerializeValue(ref IsHidden);
|
||||
serializer.SerializeValue(ref Flags);
|
||||
serializer.SerializeValue(ref MasterActorID);
|
||||
serializer.SerializeValue(ref MasterSocketName);
|
||||
public void Serialize(NetworkBufferWriter writer) {
|
||||
writer.Write(Timestamp.Ticks);
|
||||
writer.Write(Position);
|
||||
writer.Write(Rotation);
|
||||
writer.Write(Scale);
|
||||
writer.Write(IsHidden);
|
||||
writer.Write((byte) Flags);
|
||||
writer.Write(MasterActorID);
|
||||
writer.Write(MasterSocketName);
|
||||
}
|
||||
|
||||
public void Deserialize(NetworkBufferReader reader) {
|
||||
reader.Read(out long ticks);
|
||||
Timestamp = new DateTime(ticks, DateTimeKind.Utc);
|
||||
reader.Read(out Position);
|
||||
reader.Read(out Rotation);
|
||||
reader.Read(out Scale);
|
||||
reader.Read(out IsHidden);
|
||||
reader.Read(out byte flagsByte);
|
||||
Flags = (ActorPhysicsFlags) flagsByte;
|
||||
reader.Read(out MasterActorID);
|
||||
reader.Read(out MasterSocketName);
|
||||
}
|
||||
|
||||
public int GetMaxBytes() {
|
||||
return sizeof(long) + // Timestamp
|
||||
sizeof(float) * 3 + // Position
|
||||
sizeof(float) * 4 + // Rotation (Quaternion)
|
||||
sizeof(float) * 3 + // Scale
|
||||
sizeof(bool) + // IsHidden
|
||||
sizeof(byte) + // Flags
|
||||
sizeof(ulong) + // MasterActorID
|
||||
sizeof(byte) * 32; // MasterSocketName
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,19 +148,19 @@ namespace RebootKit.Engine.Simulation {
|
||||
/// - Velocity and AngularVelocity are only used if UsingRigidbody is set.
|
||||
/// - When Actor is mounted to another actor, sync won't happen.
|
||||
///
|
||||
|
||||
[Flags]
|
||||
public enum ActorTransformSyncMode : byte {
|
||||
None = 0,
|
||||
Position = 1 << 0,
|
||||
Rotation = 1 << 1,
|
||||
Scale = 1 << 2,
|
||||
UsingRigidbody = 1 << 3, // @NOTE: If this is set, Position and Rotation will be synced using Rigidbody's position and rotation.
|
||||
// @NOTE: If this is set, Position and Rotation will be synced using Rigidbody's position and rotation.
|
||||
UsingRigidbody = 1 << 3,
|
||||
Velocity = 1 << 4, // @NOTE: Velocity is only used if UsingRigidbody is set.
|
||||
AngularVelocity = 1 << 5 // @NOTE: AngularVelocity is only used if UsingRigidbody is set.
|
||||
}
|
||||
|
||||
public struct ActorTransformSyncData : INetworkSerializable {
|
||||
public struct ActorTransformSyncData : ISerializableEntity {
|
||||
public ActorTransformSyncMode SyncMode;
|
||||
|
||||
public Vector3 Position;
|
||||
@@ -209,6 +192,81 @@ namespace RebootKit.Engine.Simulation {
|
||||
serializer.SerializeValue(ref AngularVelocity);
|
||||
}
|
||||
}
|
||||
|
||||
public void Serialize(NetworkBufferWriter writer) {
|
||||
writer.Write((byte) SyncMode);
|
||||
|
||||
if ((SyncMode & ActorTransformSyncMode.Position) != 0) {
|
||||
writer.Write(Position);
|
||||
}
|
||||
|
||||
if ((SyncMode & ActorTransformSyncMode.Rotation) != 0) {
|
||||
writer.Write(Rotation);
|
||||
}
|
||||
|
||||
if ((SyncMode & ActorTransformSyncMode.Scale) != 0) {
|
||||
writer.Write(Scale);
|
||||
}
|
||||
|
||||
if ((SyncMode & ActorTransformSyncMode.Velocity) != 0) {
|
||||
writer.Write(Velocity);
|
||||
}
|
||||
|
||||
if ((SyncMode & ActorTransformSyncMode.AngularVelocity) != 0) {
|
||||
writer.Write(AngularVelocity);
|
||||
}
|
||||
}
|
||||
|
||||
public void Deserialize(NetworkBufferReader reader) {
|
||||
reader.Read(out byte syncModeByte);
|
||||
SyncMode = (ActorTransformSyncMode) syncModeByte;
|
||||
|
||||
if ((SyncMode & ActorTransformSyncMode.Position) != 0) {
|
||||
reader.Read(out Position);
|
||||
}
|
||||
|
||||
if ((SyncMode & ActorTransformSyncMode.Rotation) != 0) {
|
||||
reader.Read(out Rotation);
|
||||
}
|
||||
|
||||
if ((SyncMode & ActorTransformSyncMode.Scale) != 0) {
|
||||
reader.Read(out Scale);
|
||||
}
|
||||
|
||||
if ((SyncMode & ActorTransformSyncMode.Velocity) != 0) {
|
||||
reader.Read(out Velocity);
|
||||
}
|
||||
|
||||
if ((SyncMode & ActorTransformSyncMode.AngularVelocity) != 0) {
|
||||
reader.Read(out AngularVelocity);
|
||||
}
|
||||
}
|
||||
|
||||
public int GetMaxBytes() {
|
||||
int size = sizeof(byte); // SyncMode
|
||||
|
||||
if ((SyncMode & ActorTransformSyncMode.Position) != 0) {
|
||||
size += sizeof(float) * 3; // Vector3
|
||||
}
|
||||
|
||||
if ((SyncMode & ActorTransformSyncMode.Rotation) != 0) {
|
||||
size += sizeof(float) * 4; // Quaternion
|
||||
}
|
||||
|
||||
if ((SyncMode & ActorTransformSyncMode.Scale) != 0) {
|
||||
size += sizeof(float) * 3; // Vector3
|
||||
}
|
||||
|
||||
if ((SyncMode & ActorTransformSyncMode.Velocity) != 0) {
|
||||
size += sizeof(float) * 3; // Vector3
|
||||
}
|
||||
|
||||
if ((SyncMode & ActorTransformSyncMode.AngularVelocity) != 0) {
|
||||
size += sizeof(float) * 3; // Vector3
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class Actor : MonoBehaviour {
|
||||
@@ -256,11 +314,12 @@ namespace RebootKit.Engine.Simulation {
|
||||
internal Actor MasterActor;
|
||||
internal FixedString32Bytes MasterSocketName;
|
||||
|
||||
internal bool IsCoreStateDirty;
|
||||
public bool IsDataDirty { get; protected internal set; }
|
||||
|
||||
internal ActorsManager Manager;
|
||||
internal DateTime LastCoreStateSyncTime = DateTime.MinValue;
|
||||
|
||||
|
||||
//
|
||||
// @MARK: Unity callbacks
|
||||
//
|
||||
@@ -269,11 +328,11 @@ namespace RebootKit.Engine.Simulation {
|
||||
ActorID = UniqueID.NewULongFromGuid();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// @MARK: Callbacks to override in derived classes
|
||||
//
|
||||
|
||||
|
||||
protected abstract IActorData CreateActorData();
|
||||
|
||||
// @MARK: Server side
|
||||
@@ -302,7 +361,7 @@ namespace RebootKit.Engine.Simulation {
|
||||
}
|
||||
|
||||
gameObject.SetActive(shouldBeActive);
|
||||
Manager.SynchronizeActorCoreStateWithOther(this);
|
||||
IsCoreStateDirty = true;
|
||||
}
|
||||
|
||||
public void MountTo(Actor actor, string slotName) {
|
||||
@@ -326,7 +385,7 @@ namespace RebootKit.Engine.Simulation {
|
||||
|
||||
UpdateLocalPhysicsState(PhysicsFlags);
|
||||
UpdateMountedTransform();
|
||||
Manager.SynchronizeActorCoreStateWithOther(this);
|
||||
IsCoreStateDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -348,7 +407,7 @@ namespace RebootKit.Engine.Simulation {
|
||||
PhysicsFlags = PhysicsFlagsBeforeMount;
|
||||
UpdateLocalPhysicsState(PhysicsFlags);
|
||||
|
||||
Manager.SynchronizeActorCoreStateWithOther(this);
|
||||
IsCoreStateDirty = true;
|
||||
}
|
||||
|
||||
public void SetCollidersEnabled(bool enableColliders) {
|
||||
@@ -369,7 +428,7 @@ namespace RebootKit.Engine.Simulation {
|
||||
}
|
||||
|
||||
UpdateLocalCollidersState(enableColliders);
|
||||
Manager.SynchronizeActorCoreStateWithOther(this);
|
||||
IsCoreStateDirty = true;
|
||||
}
|
||||
|
||||
public void SetKinematic(bool isKinematic) {
|
||||
@@ -390,7 +449,7 @@ namespace RebootKit.Engine.Simulation {
|
||||
}
|
||||
|
||||
actorRigidbody.isKinematic = isKinematic;
|
||||
Manager.SynchronizeActorCoreStateWithOther(this);
|
||||
IsCoreStateDirty = true;
|
||||
}
|
||||
|
||||
//
|
||||
@@ -403,6 +462,7 @@ namespace RebootKit.Engine.Simulation {
|
||||
protected void SendActorCommand<TCmdData>(ushort commandID, ref TCmdData commandData)
|
||||
where TCmdData : struct, ISerializableEntity {
|
||||
NativeArray<byte> data = DataSerializationUtils.Serialize(commandData);
|
||||
|
||||
SendActorCommand(commandID, data);
|
||||
}
|
||||
|
||||
@@ -651,11 +711,11 @@ namespace RebootKit.Engine.Simulation {
|
||||
internal IActorData InternalCreateActorData() {
|
||||
return CreateActorData();
|
||||
}
|
||||
|
||||
|
||||
internal void InitialSyncFinished() {
|
||||
OnClientFinishedInitialSync();
|
||||
}
|
||||
|
||||
|
||||
internal void HandleActorCommand(ActorCommand actorCommand) {
|
||||
if (!RR.IsServer()) {
|
||||
s_ActorLogger.Error($"Only the server can handle actor commands. Actor: {name} (ID: {ActorID})");
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using NUnit.Framework;
|
||||
using RebootKit.Engine.Extensions;
|
||||
using RebootKit.Engine.Foundation;
|
||||
using RebootKit.Engine.Main;
|
||||
using RebootKit.Engine.Network;
|
||||
using Unity.Collections;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
@@ -27,12 +31,10 @@ namespace RebootKit.Engine.Simulation {
|
||||
//
|
||||
public override void OnNetworkSpawn() {
|
||||
base.OnNetworkSpawn();
|
||||
RR.ServerTick += OnServerTick;
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn() {
|
||||
base.OnNetworkDespawn();
|
||||
RR.ServerTick -= OnServerTick;
|
||||
}
|
||||
|
||||
//
|
||||
@@ -51,13 +53,11 @@ namespace RebootKit.Engine.Simulation {
|
||||
//
|
||||
// @MARK: Server-side logic
|
||||
//
|
||||
void OnServerTick(ulong tick) {
|
||||
public void ServerTick(float dt) {
|
||||
if (!IsServer) {
|
||||
return;
|
||||
}
|
||||
|
||||
float dt = 1.0f / RR.TickRate.IndexValue;
|
||||
|
||||
TickActorsList(m_InSceneActors, dt);
|
||||
TickActorsList(m_SpawnedActors, dt);
|
||||
}
|
||||
@@ -69,78 +69,28 @@ namespace RebootKit.Engine.Simulation {
|
||||
if (actor.IsDataDirty) {
|
||||
actor.IsDataDirty = false;
|
||||
|
||||
NativeArray<byte> data = SerializeActorState(actor);
|
||||
if (data.IsCreated) {
|
||||
SendActorStateToClients(actor.ActorID, data);
|
||||
} else {
|
||||
s_Logger.Error($"Failed to serialize actor data for {actor.name}");
|
||||
if (actor.Data.GetMaxBytes() > 0) {
|
||||
RR.NetworkSystemInstance.WriteActorState(NetworkPacketTarget.AllClients(), actor.ActorID, actor.Data);
|
||||
}
|
||||
}
|
||||
|
||||
if (actor.IsCoreStateDirty) {
|
||||
actor.IsCoreStateDirty = false;
|
||||
|
||||
RR.NetworkSystemInstance.WriteActorCoreState(NetworkPacketTarget.AllClients(),
|
||||
actor.ActorID,
|
||||
actor.GetCoreStateSnapshot());
|
||||
}
|
||||
|
||||
if (actor.transformSyncMode != ActorTransformSyncMode.None && actor.MasterActor == null) {
|
||||
ActorTransformSyncData syncData = actor.GetTransformSyncData();
|
||||
|
||||
foreach ((ulong _, NetworkClientState state) in RR.NetworkSystemInstance.Clients) {
|
||||
if (state.IsReady) {
|
||||
SynchronizeActorTransformStateRpc(actor.ActorID, syncData, RpcTarget.NotMe);
|
||||
}
|
||||
}
|
||||
RR.NetworkSystemInstance.WriteActorTransformState(NetworkPacketTarget.AllClients(),
|
||||
actor.ActorID,
|
||||
syncData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal void SynchronizeActorCoreStateWithOther(Actor actor) {
|
||||
if (!RR.IsServer()) {
|
||||
s_Logger.Error("Only the server can synchronize actor core states.");
|
||||
return;
|
||||
}
|
||||
|
||||
SynchronizeCoreActorStateRpc(actor.ActorID, actor.GetCoreStateSnapshot(), RpcTarget.NotMe);
|
||||
}
|
||||
|
||||
void SendActorStateToClients(ulong actorID, NativeArray<byte> data) {
|
||||
if (!RR.IsServer()) {
|
||||
s_Logger.Error("Only the server can synchronize actor states with clients.");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ((ulong clientID, NetworkClientState state) in RR.NetworkSystemInstance.Clients) {
|
||||
if (state.IsReady) {
|
||||
SynchronizeActorStateRpc(actorID, data, RpcTarget.Single(clientID, RpcTargetUse.Temp));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Rpc(SendTo.SpecifiedInParams, Delivery = RpcDelivery.Unreliable)]
|
||||
void SynchronizeActorStateRpc(ulong actorID, NativeArray<byte> data, RpcParams rpcParams) {
|
||||
Actor actor = FindActorByID(actorID);
|
||||
if (actor is null) {
|
||||
return;
|
||||
}
|
||||
|
||||
s_Logger.Info($"Synchronizing actor state for {actor.name} with ID {actorID}");
|
||||
DeserializeActorState(actor, data);
|
||||
}
|
||||
|
||||
[Rpc(SendTo.SpecifiedInParams, Delivery = RpcDelivery.Unreliable)]
|
||||
void SynchronizeActorTransformStateRpc(ulong actorID, ActorTransformSyncData syncData, RpcParams rpcParams) {
|
||||
Actor actor = FindActorByID(actorID);
|
||||
if (actor is null) {
|
||||
s_Logger.Error($"Actor with ID {actorID} not found for transform synchronization.");
|
||||
return;
|
||||
}
|
||||
|
||||
actor.RestoreTransformState(syncData);
|
||||
}
|
||||
|
||||
NativeArray<byte> SerializeActorState(Actor actor) {
|
||||
return DataSerializationUtils.Serialize(actor.Data);
|
||||
}
|
||||
|
||||
void DeserializeActorState(Actor actor, NativeArray<byte> data) {
|
||||
DataSerializationUtils.Deserialize(data, ref actor.Data);
|
||||
}
|
||||
|
||||
//
|
||||
// @MARK: Server API
|
||||
//
|
||||
@@ -172,73 +122,14 @@ namespace RebootKit.Engine.Simulation {
|
||||
|
||||
m_SpawnedActors.Add(actor);
|
||||
|
||||
NativeArray<byte> stateData = SerializeActorState(actor);
|
||||
SpawnActorRpc(assetReference.AssetGUID,
|
||||
actor.ActorID,
|
||||
actor.GetCoreStateSnapshot(),
|
||||
stateData,
|
||||
RpcTarget.NotMe);
|
||||
RR.NetworkSystemInstance.WriteSpawnActor(NetworkPacketTarget.AllClients(),
|
||||
assetReference.AssetGUID,
|
||||
actor.ActorID,
|
||||
actor.GetCoreStateSnapshot(),
|
||||
actor.Data);
|
||||
return actor;
|
||||
}
|
||||
|
||||
// @NOTE: This RPC is used to spawn actors on clients.
|
||||
[Rpc(SendTo.SpecifiedInParams)]
|
||||
void SpawnActorRpc(string guid,
|
||||
ulong actorID,
|
||||
ActorCoreStateSnapshot coreStateSnapshot,
|
||||
NativeArray<byte> stateData,
|
||||
RpcParams rpcParams) {
|
||||
AssetReferenceGameObject assetReference = new AssetReferenceGameObject(guid);
|
||||
if (!assetReference.RuntimeKeyIsValid()) {
|
||||
s_Logger.Error($"Invalid asset reference for actor with GUID {guid}");
|
||||
return;
|
||||
}
|
||||
|
||||
GameObject actorObject = assetReference
|
||||
.InstantiateAsync(coreStateSnapshot.Position, coreStateSnapshot.Rotation)
|
||||
.WaitForCompletion();
|
||||
if (actorObject == null) {
|
||||
s_Logger.Error($"Failed to instantiate actor with GUID {guid}");
|
||||
return;
|
||||
}
|
||||
|
||||
Actor actor = actorObject.GetComponent<Actor>();
|
||||
if (actor is null) {
|
||||
s_Logger.Error($"GameObject {actorObject.name} does not have an Actor component.");
|
||||
Destroy(actorObject);
|
||||
return;
|
||||
}
|
||||
|
||||
actor.Manager = this;
|
||||
actor.SourceActorPath = guid;
|
||||
actor.ActorID = actorID;
|
||||
actor.Data = actor.InternalCreateActorData();
|
||||
|
||||
actor.RestoreCoreState(coreStateSnapshot);
|
||||
DeserializeActorState(actor, stateData);
|
||||
m_SpawnedActors.Add(actor);
|
||||
}
|
||||
|
||||
public void KillActor(Actor actor) {
|
||||
if (!IsServer) {
|
||||
s_Logger.Error("Only the server can kill actors.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (actor is null) {
|
||||
s_Logger.Error("Trying to kill a null actor.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_SpawnedActors.Remove(actor)) {
|
||||
s_Logger.Error($"Trying to kill an actor that is not registered: {actor.name}. " +
|
||||
"Remember you can only kill actors spawned that are dynamically created");
|
||||
return;
|
||||
}
|
||||
|
||||
Destroy(actor.gameObject);
|
||||
}
|
||||
|
||||
public void CleanUp() {
|
||||
if (IsServer) {
|
||||
CleanUpRpc();
|
||||
@@ -259,7 +150,7 @@ namespace RebootKit.Engine.Simulation {
|
||||
void CleanUpRpc() {
|
||||
CleanUp();
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// @MARK: Common API
|
||||
//
|
||||
@@ -325,60 +216,27 @@ namespace RebootKit.Engine.Simulation {
|
||||
clientState.ActorsSyncPacketsLeft = m_InSceneActors.Count;
|
||||
RR.NetworkSystemInstance.UpdateClientState(clientState);
|
||||
|
||||
RpcSendParams sendParams = RpcTarget.Single(clientID, RpcTargetUse.Temp);
|
||||
s_Logger.Info($"Starting actor synchronization for client {clientID}.\n" +
|
||||
$"InScene Actors to sync: {m_InSceneActors.Count}\n" +
|
||||
$"Actors to spawn: {m_SpawnedActors.Count}");
|
||||
|
||||
foreach (Actor actor in m_InSceneActors) {
|
||||
NativeArray<byte> data = SerializeActorState(actor);
|
||||
if (!data.IsCreated) {
|
||||
s_Logger.Error($"Failed to serialize actor data for {actor.name}");
|
||||
continue;
|
||||
}
|
||||
|
||||
SynchronizeActorStateForClientRpc(actor.ActorID, actor.GetCoreStateSnapshot(), data, sendParams);
|
||||
RR.NetworkSystemInstance.WriteActorSynchronize(NetworkPacketTarget.Single(clientID),
|
||||
actor.ActorID,
|
||||
actor.GetCoreStateSnapshot(),
|
||||
actor.Data);
|
||||
}
|
||||
|
||||
foreach (Actor actor in m_SpawnedActors) {
|
||||
NativeArray<byte> data = SerializeActorState(actor);
|
||||
if (!data.IsCreated) {
|
||||
s_Logger.Error($"Failed to serialize actor data for {actor.name}");
|
||||
continue;
|
||||
}
|
||||
|
||||
ActorCoreStateSnapshot coreStateSnapshot = actor.GetCoreStateSnapshot();
|
||||
SpawnActorRpc(actor.SourceActorPath,
|
||||
actor.ActorID,
|
||||
coreStateSnapshot,
|
||||
data,
|
||||
sendParams);
|
||||
s_Logger.Info("Spawning actor for client synchronization: " + actor.SourceActorPath);
|
||||
RR.NetworkSystemInstance.WriteSpawnActor(NetworkPacketTarget.Single(clientID),
|
||||
actor.SourceActorPath,
|
||||
actor.ActorID,
|
||||
actor.GetCoreStateSnapshot(),
|
||||
actor.Data);
|
||||
}
|
||||
}
|
||||
|
||||
[Rpc(SendTo.SpecifiedInParams, Delivery = RpcDelivery.Reliable)]
|
||||
void SynchronizeActorStateForClientRpc(ulong actorID,
|
||||
ActorCoreStateSnapshot coreStateSnapshot,
|
||||
NativeArray<byte> data,
|
||||
RpcParams rpcParams) {
|
||||
Actor actor = FindActorByID(actorID);
|
||||
if (actor is null) {
|
||||
return;
|
||||
}
|
||||
|
||||
actor.RestoreCoreState(coreStateSnapshot);
|
||||
DeserializeActorState(actor, data);
|
||||
ClientSynchronizedActorRpc();
|
||||
}
|
||||
|
||||
[Rpc(SendTo.SpecifiedInParams, Delivery = RpcDelivery.Reliable)]
|
||||
void SynchronizeCoreActorStateRpc(ulong actorID, ActorCoreStateSnapshot snapshot, RpcParams rpcParams) {
|
||||
Actor actor = FindActorByID(actorID);
|
||||
if (actor is null) {
|
||||
s_Logger.Error($"Actor with ID {actorID} not found for core state synchronization.");
|
||||
return;
|
||||
}
|
||||
|
||||
actor.RestoreCoreState(snapshot);
|
||||
}
|
||||
|
||||
[Rpc(SendTo.Server, Delivery = RpcDelivery.Reliable)]
|
||||
void ClientSynchronizedActorRpc(RpcParams rpcParams = default) {
|
||||
ulong clientID = rpcParams.Receive.SenderClientId;
|
||||
@@ -389,6 +247,7 @@ namespace RebootKit.Engine.Simulation {
|
||||
}
|
||||
|
||||
clientState.ActorsSyncPacketsLeft--;
|
||||
s_Logger.Info($"Synchronized actor for client {clientID}. Packets left: {clientState.ActorsSyncPacketsLeft}");
|
||||
RR.NetworkSystemInstance.UpdateClientState(clientState);
|
||||
|
||||
if (clientState.ActorsSyncPacketsLeft == 0) {
|
||||
@@ -396,6 +255,129 @@ namespace RebootKit.Engine.Simulation {
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// @MARK: Network Data Handling
|
||||
///
|
||||
internal void OnReceivedEntity(NetworkDataHeader header, NativeArray<byte> data) {
|
||||
if (header.Type == NetworkDataType.ActorCoreState) {
|
||||
Actor actor = FindActorByID(header.ActorID);
|
||||
if (actor == null) {
|
||||
s_Logger.Error($"Failed to find actor with ID {header.ActorID} for core state update.");
|
||||
return;
|
||||
}
|
||||
|
||||
using NetworkBufferReader reader = new NetworkBufferReader(data);
|
||||
ActorCoreStateSnapshot coreState = new ActorCoreStateSnapshot();
|
||||
coreState.Deserialize(reader);
|
||||
actor.RestoreCoreState(coreState);
|
||||
} else if (header.Type == NetworkDataType.ActorTransformSync) {
|
||||
Actor actor = FindActorByID(header.ActorID);
|
||||
if (actor == null) {
|
||||
s_Logger.Error($"Failed to find actor with ID {header.ActorID} for transform state update.");
|
||||
return;
|
||||
}
|
||||
|
||||
using NetworkBufferReader reader = new NetworkBufferReader(data);
|
||||
ActorTransformSyncData transformSyncData = new ActorTransformSyncData();
|
||||
transformSyncData.Deserialize(reader);
|
||||
actor.RestoreTransformState(transformSyncData);
|
||||
} else if (header.Type == NetworkDataType.ActorState) {
|
||||
Actor actor = FindActorByID(header.ActorID);
|
||||
if (actor == null) {
|
||||
s_Logger.Error($"Failed to find actor with ID {header.ActorID} for state update.");
|
||||
return;
|
||||
}
|
||||
|
||||
DataSerializationUtils.Deserialize(data, ref actor.Data);
|
||||
} else if (header.Type == NetworkDataType.ActorEvent) {
|
||||
Actor actor = FindActorByID(header.ActorID);
|
||||
if (actor == null) {
|
||||
s_Logger.Error($"Failed to find actor with ID {header.ActorID} for event handling.");
|
||||
return;
|
||||
}
|
||||
|
||||
throw new NotImplementedException();
|
||||
} else if (header.Type == NetworkDataType.ActorCommand) {
|
||||
Actor actor = FindActorByID(header.ActorID);
|
||||
if (actor == null) {
|
||||
s_Logger.Error($"Failed to find actor with ID {header.ActorID} for command handling.");
|
||||
return;
|
||||
}
|
||||
|
||||
throw new NotImplementedException();
|
||||
} else if (header.Type == NetworkDataType.SynchronizeActor) {
|
||||
Actor actor = FindActorByID(header.ActorID);
|
||||
if (actor == null) {
|
||||
s_Logger.Error($"Failed to find actor with ID {header.ActorID} for synchronization.");
|
||||
return;
|
||||
}
|
||||
|
||||
using NetworkBufferReader reader = new NetworkBufferReader(data);
|
||||
|
||||
ActorCoreStateSnapshot coreState = new ActorCoreStateSnapshot();
|
||||
coreState.Deserialize(reader);
|
||||
|
||||
reader.Read(out ushort actorDataSize);
|
||||
reader.Read(out NativeArray<byte> stateData, actorDataSize);
|
||||
|
||||
actor.RestoreCoreState(coreState);
|
||||
DataSerializationUtils.Deserialize(stateData, ref actor.Data);
|
||||
|
||||
ClientSynchronizedActorRpc();
|
||||
} else if (header.Type == NetworkDataType.SpawnActor) {
|
||||
using NetworkBufferReader reader = new NetworkBufferReader(data);
|
||||
|
||||
reader.Read(out FixedString64Bytes value);
|
||||
string guid = value.ToString();
|
||||
|
||||
ActorCoreStateSnapshot coreState = new ActorCoreStateSnapshot();
|
||||
coreState.Deserialize(reader);
|
||||
|
||||
reader.Read(out ushort actorDataSize);
|
||||
reader.Read(out NativeArray<byte> stateData, actorDataSize);
|
||||
|
||||
SpawnLocalActor(guid,
|
||||
header.ActorID,
|
||||
coreState,
|
||||
stateData);
|
||||
}
|
||||
}
|
||||
|
||||
void SpawnLocalActor(string guid,
|
||||
ulong actorID,
|
||||
ActorCoreStateSnapshot coreStateSnapshot,
|
||||
NativeArray<byte> stateData) {
|
||||
AssetReferenceGameObject assetReference = new AssetReferenceGameObject(guid);
|
||||
if (!assetReference.RuntimeKeyIsValid()) {
|
||||
s_Logger.Error($"Invalid asset reference for actor with GUID {guid}");
|
||||
return;
|
||||
}
|
||||
|
||||
GameObject actorObject = assetReference
|
||||
.InstantiateAsync(coreStateSnapshot.Position, coreStateSnapshot.Rotation)
|
||||
.WaitForCompletion();
|
||||
if (actorObject == null) {
|
||||
s_Logger.Error($"Failed to instantiate actor with GUID {guid}");
|
||||
return;
|
||||
}
|
||||
|
||||
Actor actor = actorObject.GetComponent<Actor>();
|
||||
if (actor is null) {
|
||||
s_Logger.Error($"GameObject {actorObject.name} does not have an Actor component.");
|
||||
Destroy(actorObject);
|
||||
return;
|
||||
}
|
||||
|
||||
actor.Manager = this;
|
||||
actor.SourceActorPath = guid;
|
||||
actor.ActorID = actorID;
|
||||
actor.Data = actor.InternalCreateActorData();
|
||||
|
||||
actor.RestoreCoreState(coreStateSnapshot);
|
||||
DataSerializationUtils.Deserialize(stateData, ref actor.Data);
|
||||
m_SpawnedActors.Add(actor);
|
||||
}
|
||||
|
||||
//
|
||||
// @MARK: Actor Commands and Events
|
||||
//
|
||||
@@ -422,6 +404,10 @@ namespace RebootKit.Engine.Simulation {
|
||||
}
|
||||
|
||||
foreach ((ulong clientID, NetworkClientState state) in RR.NetworkSystemInstance.Clients) {
|
||||
if (NetworkManager.Singleton.LocalClientId == clientID) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (state.IsReady) {
|
||||
SendActorEventRpc(actorEvent, RpcTarget.Single(clientID, RpcTargetUse.Temp));
|
||||
}
|
||||
|
||||
@@ -24,8 +24,10 @@ namespace RebootKit.Engine.Steam {
|
||||
}
|
||||
|
||||
IsInitialized = true;
|
||||
|
||||
|
||||
await UniTask.Yield(cancellationToken);
|
||||
|
||||
SteamFriends.OnGameRichPresenceJoinRequested += OnJoinRequested;
|
||||
}
|
||||
|
||||
internal static void Shutdown() {
|
||||
@@ -36,9 +38,19 @@ namespace RebootKit.Engine.Steam {
|
||||
|
||||
s_Logger.Info("Shutting down Steam Manager...");
|
||||
|
||||
SteamFriends.OnGameRichPresenceJoinRequested -= OnJoinRequested;
|
||||
SteamClient.Shutdown();
|
||||
|
||||
IsInitialized = false;
|
||||
}
|
||||
|
||||
static void OnJoinRequested(Friend friend, string key) {
|
||||
s_Logger.Info($"Join request received from {friend.Name} with key: {key}");
|
||||
|
||||
if (string.IsNullOrEmpty(key)) {
|
||||
s_Logger.Warning("Join request key is empty. Cannot process join request.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
204
Tests/Runtime/Engine/NetworkBufferWriterReaderTests.cs
Normal file
204
Tests/Runtime/Engine/NetworkBufferWriterReaderTests.cs
Normal file
@@ -0,0 +1,204 @@
|
||||
using NUnit.Framework;
|
||||
using RebootKit.Engine.Extensions;
|
||||
using RebootKit.Engine.Foundation;
|
||||
using RebootKit.Engine.Network;
|
||||
using Unity.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Tests.Runtime.Engine {
|
||||
public class NetworkBufferWriterReaderTests {
|
||||
[Test]
|
||||
public void NetworkBuffer_Int() {
|
||||
const int k_Value = 12345;
|
||||
|
||||
using NativeArray<byte> data = new NativeArray<byte>(4, Allocator.Temp);
|
||||
|
||||
using NetworkBufferWriter writer = new NetworkBufferWriter(data, 0);
|
||||
writer.Write(k_Value);
|
||||
|
||||
using NetworkBufferReader reader = new NetworkBufferReader(data);
|
||||
Assert.IsTrue(reader.Read(out int value));
|
||||
Assert.AreEqual(k_Value, value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NetworkBuffer_Two_Ints() {
|
||||
const int k_Value1 = 12345;
|
||||
const int k_Value2 = 67890;
|
||||
|
||||
using NativeArray<byte> data = new NativeArray<byte>(8, Allocator.Temp);
|
||||
|
||||
using NetworkBufferWriter writer = new NetworkBufferWriter(data, 0);
|
||||
writer.Write(k_Value1);
|
||||
writer.Write(k_Value2);
|
||||
|
||||
using NetworkBufferReader reader = new NetworkBufferReader(data);
|
||||
Assert.IsTrue(reader.Read(out int value1));
|
||||
Assert.AreEqual(k_Value1, value1);
|
||||
Assert.IsTrue(reader.Read(out int value2));
|
||||
Assert.AreEqual(k_Value2, value2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NetworkBuffer_Short() {
|
||||
const short k_Value = 123;
|
||||
|
||||
using NativeArray<byte> data = new NativeArray<byte>(2, Allocator.Temp);
|
||||
|
||||
using NetworkBufferWriter writer = new NetworkBufferWriter(data, 0);
|
||||
writer.Write(k_Value);
|
||||
|
||||
using NetworkBufferReader reader = new NetworkBufferReader(data);
|
||||
Assert.IsTrue(reader.Read(out short value));
|
||||
Assert.AreEqual(k_Value, value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NetworkBuffer_UShort() {
|
||||
const ushort k_Value = 123;
|
||||
using NativeArray<byte> data = new NativeArray<byte>(2, Allocator.Temp);
|
||||
|
||||
using NetworkBufferWriter writer = new NetworkBufferWriter(data, 0);
|
||||
writer.Write(k_Value);
|
||||
|
||||
using NetworkBufferReader reader = new NetworkBufferReader(data);
|
||||
Assert.IsTrue(reader.Read(out ushort value));
|
||||
Assert.AreEqual(k_Value, value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NetworkBuffer_Long() {
|
||||
const long k_Value = 123456789L;
|
||||
|
||||
using NativeArray<byte> data = new NativeArray<byte>(8, Allocator.Temp);
|
||||
|
||||
using NetworkBufferWriter writer = new NetworkBufferWriter(data, 0);
|
||||
writer.Write(k_Value);
|
||||
|
||||
using NetworkBufferReader reader = new NetworkBufferReader(data);
|
||||
Assert.IsTrue(reader.Read(out long value));
|
||||
Assert.AreEqual(k_Value, value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NetworkBuffer_ULong() {
|
||||
const ulong k_Value = 123456789UL;
|
||||
|
||||
using NativeArray<byte> data = new NativeArray<byte>(8, Allocator.Temp);
|
||||
|
||||
using NetworkBufferWriter writer = new NetworkBufferWriter(data, 0);
|
||||
writer.Write(k_Value);
|
||||
|
||||
using NetworkBufferReader reader = new NetworkBufferReader(data);
|
||||
Assert.IsTrue(reader.Read(out ulong value));
|
||||
Assert.AreEqual(k_Value, value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NetworkBuffer_Bool() {
|
||||
const bool k_Value = true;
|
||||
|
||||
using NativeArray<byte> data = new NativeArray<byte>(1, Allocator.Temp);
|
||||
|
||||
using NetworkBufferWriter writer = new NetworkBufferWriter(data, 0);
|
||||
writer.Write(k_Value);
|
||||
|
||||
using NetworkBufferReader reader = new NetworkBufferReader(data);
|
||||
Assert.IsTrue(reader.Read(out bool value));
|
||||
Assert.AreEqual(k_Value, value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NetworkBuffer_Byte() {
|
||||
const byte k_Value = 0xAB;
|
||||
|
||||
using NativeArray<byte> data = new NativeArray<byte>(1, Allocator.Temp);
|
||||
|
||||
using NetworkBufferWriter writer = new NetworkBufferWriter(data, 0);
|
||||
writer.Write(k_Value);
|
||||
|
||||
using NetworkBufferReader reader = new NetworkBufferReader(data);
|
||||
Assert.IsTrue(reader.Read(out byte value));
|
||||
Assert.AreEqual(k_Value, value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NetworkBuffer_Float() {
|
||||
const float k_Value = 123.45f;
|
||||
using NativeArray<byte> data = new NativeArray<byte>(4, Allocator.Temp);
|
||||
using NetworkBufferWriter writer = new NetworkBufferWriter(data, 0);
|
||||
writer.Write(k_Value);
|
||||
|
||||
using NetworkBufferReader reader = new NetworkBufferReader(data);
|
||||
Assert.IsTrue(reader.Read(out float value));
|
||||
Assert.AreEqual(k_Value, value, 0.0001f);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NetworkBuffer_ReadBeyond() {
|
||||
using NativeArray<byte> data = new NativeArray<byte>(4, Allocator.Temp);
|
||||
|
||||
using NetworkBufferWriter writer = new NetworkBufferWriter(data, 0);
|
||||
writer.Write(12345);
|
||||
|
||||
using NetworkBufferReader reader = new NetworkBufferReader(data);
|
||||
Assert.IsTrue(reader.Read(out int value));
|
||||
Assert.AreEqual(12345, value);
|
||||
|
||||
Assert.IsFalse(reader.Read(out int _));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NetworkBuffer_FixedString32Bytes() {
|
||||
FixedString32Bytes value = new FixedString32Bytes("henlo_world");
|
||||
|
||||
using NativeArray<byte> data = new NativeArray<byte>(32, Allocator.Temp);
|
||||
using NetworkBufferWriter writer = new NetworkBufferWriter(data, 0);
|
||||
writer.Write(value);
|
||||
|
||||
using NetworkBufferReader reader = new NetworkBufferReader(data);
|
||||
Assert.IsTrue(reader.Read(out FixedString32Bytes readValue), "Failed to read FixedString32Bytes");
|
||||
Assert.IsTrue(value == readValue, "Value should be equal to read value");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NetworkBuffer_Empty() {
|
||||
using NativeArray<byte> data = new NativeArray<byte>(0, Allocator.Temp);
|
||||
|
||||
using NetworkBufferReader reader = new NetworkBufferReader(data);
|
||||
Assert.IsFalse(reader.Read(out int value));
|
||||
Assert.AreEqual(0, value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NetworkBuffer_Size_NativeArray() {
|
||||
NativeArray<byte> value = new NativeArray<byte>(new byte[] {
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5
|
||||
}, Allocator.Temp);
|
||||
|
||||
using NativeArray<byte> data = new NativeArray<byte>(value.Length + sizeof(ushort), Allocator.Temp);
|
||||
using NetworkBufferWriter writer = new NetworkBufferWriter(data, 0);
|
||||
writer.Write((ushort) value.Length);
|
||||
writer.Write(value);
|
||||
|
||||
using NetworkBufferReader reader = new NetworkBufferReader(data);
|
||||
reader.Read(out ushort readLength);
|
||||
Assert.IsTrue(readLength == value.Length,
|
||||
$"Read Length({readLength }) should be equal to an actual value.length({value.Length})");
|
||||
Assert.IsTrue(reader.Read(out NativeArray<byte> readValue, readLength), "Failed to read NativeArray<byte>");
|
||||
Assert.AreEqual(value.Length, readValue.Length,
|
||||
"Length of read NativeArray<byte> should match written length");
|
||||
|
||||
for (int i = 0; i < value.Length; i++) {
|
||||
Assert.AreEqual(value[i], readValue[i], $"Value at index {i} should match");
|
||||
}
|
||||
|
||||
value.Dispose();
|
||||
readValue.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8f01bbcc978f4b9993c29d389987bc9c
|
||||
timeCreated: 1752874442
|
||||
@@ -1,20 +1,21 @@
|
||||
{
|
||||
"name": "RebootKit.Engine.Tests",
|
||||
"rootNamespace": "",
|
||||
"references": [
|
||||
"GUID:284059c7949783646b281a1b815580e6",
|
||||
"GUID:0acc523941302664db1f4e527237feb3",
|
||||
"GUID:27619889b8ba8c24980f49ee34dbb44a"
|
||||
],
|
||||
"includePlatforms": [
|
||||
"Editor"
|
||||
],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
"name": "RebootKit.Engine.Tests",
|
||||
"rootNamespace": "",
|
||||
"references": [
|
||||
"GUID:284059c7949783646b281a1b815580e6",
|
||||
"GUID:0acc523941302664db1f4e527237feb3",
|
||||
"GUID:e0cd26848372d4e5c891c569017e11f1",
|
||||
"GUID:27619889b8ba8c24980f49ee34dbb44a"
|
||||
],
|
||||
"includePlatforms": [
|
||||
"Editor"
|
||||
],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
Reference in New Issue
Block a user