optimizing network stuff

This commit is contained in:
2025-07-30 05:51:39 +02:00
parent ea99249fe2
commit 159e9adcd7
56 changed files with 2272 additions and 1781 deletions

View File

@@ -1,52 +1,51 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using NUnit.Framework;
using System.Linq;
using RebootKit.Engine.Extensions;
using RebootKit.Engine.Foundation;
using RebootKit.Engine.Main;
using RebootKit.Engine.Network;
using Unity.Collections;
using Unity.Netcode;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.Assertions;
using Logger = RebootKit.Engine.Foundation.Logger;
using Object = UnityEngine.Object;
namespace RebootKit.Engine.Simulation {
// @TODO:
// - Actors States might be packed into chunks to reduce the number of RPCs sent.
// - Release addressables when they are no longer needed.
public class ActorsManager : NetworkBehaviour {
public class ActorsManager : IDisposable {
static readonly Logger s_Logger = new Logger(nameof(ActorsManager));
readonly NetworkSystem m_Network;
readonly List<Actor> m_InSceneActors = new List<Actor>();
readonly List<Actor> m_SpawnedActors = new List<Actor>();
public int InSceneActorsCount { get { return m_InSceneActors.Count; } }
public int SpawnedActorsCount { get { return m_SpawnedActors.Count; } }
public int TotalActorsCount { get { return m_InSceneActors.Count + m_SpawnedActors.Count; } }
ushort m_ActorIDCounter = 0;
//
// @MARK: NetworkBehaviour callbacks
//
public override void OnNetworkSpawn() {
base.OnNetworkSpawn();
public ushort InSceneActorsCount { get { return (ushort) m_InSceneActors.Count; } }
public ushort SpawnedActorsCount { get { return (ushort) m_SpawnedActors.Count; } }
public int TotalActorsCount { get { return InSceneActorsCount + SpawnedActorsCount; } }
public ActorsManager(NetworkSystem networkSystem) {
m_Network = networkSystem;
}
public override void OnNetworkDespawn() {
base.OnNetworkDespawn();
}
public void Dispose() { }
//
// @MARK: Unity callbacks
// @MARK: Update
//
void Update() {
public void Tick(float deltaTime) {
foreach (Actor actor in m_InSceneActors) {
actor.OnClientTick(Time.deltaTime);
actor.OnClientTick(deltaTime);
}
foreach (Actor actor in m_SpawnedActors) {
actor.OnClientTick(Time.deltaTime);
actor.OnClientTick(deltaTime);
}
}
@@ -54,7 +53,7 @@ namespace RebootKit.Engine.Simulation {
// @MARK: Server-side logic
//
public void ServerTick(float dt) {
if (!IsServer) {
if (!RR.IsServer()) {
return;
}
@@ -70,23 +69,50 @@ namespace RebootKit.Engine.Simulation {
actor.IsDataDirty = false;
if (actor.Data.GetMaxBytes() > 0) {
RR.NetworkSystemInstance.WriteActorState(NetworkPacketTarget.AllClients(), actor.ActorID, actor.Data);
foreach (NetworkClientState client in m_Network.Clients.Values) {
if (client.IsServer) {
continue;
}
m_Network.WriteActorState(client.ClientID, actor.ActorID, actor.Data);
}
}
}
if (actor.IsCoreStateDirty) {
actor.IsCoreStateDirty = false;
RR.NetworkSystemInstance.WriteActorCoreState(NetworkPacketTarget.AllClients(),
actor.ActorID,
actor.GetCoreStateSnapshot());
foreach (NetworkClientState client in m_Network.Clients.Values) {
if (client.IsServer) {
continue;
}
m_Network.WriteActorCoreState(client.ClientID, actor.GetCoreStateSnapshot());
}
}
}
foreach (NetworkClientState client in m_Network.Clients.Values) {
if (client.IsServer) {
continue;
}
if (actor.transformSyncMode != ActorTransformSyncMode.None && actor.MasterActor == null) {
ActorTransformSyncData syncData = actor.GetTransformSyncData();
RR.NetworkSystemInstance.WriteActorTransformState(NetworkPacketTarget.AllClients(),
actor.ActorID,
syncData);
foreach (Actor actor in actors.OrderBy(t => t.GetLastSyncTick(client.ClientID))) {
if (!actor.syncTransform || actor.MasterActor != null) {
continue;
}
ActorTransformSyncData syncData = actor.GetTransformSyncDataForClient(client.ClientID);
if (syncData.SyncMode == ActorTransformSyncMode.None) {
continue;
}
if (m_Network.WriteActorTransformState(client.ClientID, syncData)) {
actor.UpdateClientState(client.ClientID, RR.Network.TickCount);
} else {
// @NOTE: We ran out of space in the packet
break;
}
}
}
}
@@ -95,7 +121,7 @@ namespace RebootKit.Engine.Simulation {
// @MARK: Server API
//
public Actor SpawnActor(AssetReferenceGameObject assetReference, Vector3 position, Quaternion rotation) {
if (!IsServer) {
if (!RR.IsServer()) {
s_Logger.Error("Only the server can spawn actors.");
return null;
}
@@ -105,13 +131,16 @@ namespace RebootKit.Engine.Simulation {
return null;
}
ulong actorID = UniqueID.NewULongFromGuid();
if (!TryGenerateNextActorID(out ushort actorID)) {
s_Logger.Error("Cannot spawn actor: Failed to generate next actor ID.");
return null;
}
GameObject actorObject = assetReference.InstantiateAsync(position, rotation).WaitForCompletion();
Actor actor = actorObject.GetComponent<Actor>();
if (actor is null) {
s_Logger.Error($"GameObject {actorObject.name} does not have an Actor component.");
Destroy(actorObject);
Object.Destroy(actorObject);
return null;
}
@@ -119,36 +148,44 @@ namespace RebootKit.Engine.Simulation {
actor.SourceActorPath = assetReference.AssetGUID;
actor.ActorID = actorID;
actor.Data = actor.InternalCreateActorData();
m_SpawnedActors.Add(actor);
RR.NetworkSystemInstance.WriteSpawnActor(NetworkPacketTarget.AllClients(),
assetReference.AssetGUID,
actor.ActorID,
actor.GetCoreStateSnapshot(),
actor.Data);
foreach (NetworkClientState client in m_Network.Clients.Values) {
if (client.IsServer) {
continue;
}
m_Network.SendSpawnActor(client.ClientID,
assetReference.AssetGUID,
actor.GetCoreStateSnapshot(),
actor.Data);
}
return actor;
}
public void CleanUp() {
if (IsServer) {
CleanUpRpc();
}
m_InSceneActors.Clear();
foreach (Actor actor in m_SpawnedActors) {
if (actor.OrNull() != null) {
Destroy(actor.gameObject);
}
}
m_SpawnedActors.Clear();
bool TryGenerateNextActorID(out ushort actorID) {
m_ActorIDCounter += 1;
actorID = m_ActorIDCounter;
return true;
}
[Rpc(SendTo.NotMe)]
void CleanUpRpc() {
CleanUp();
internal void AssignActorsIDs() {
if (!RR.IsServer()) {
s_Logger.Info("Only the server can assign actors IDs.");
return;
}
m_ActorIDCounter = 0;
foreach (Actor actor in m_InSceneActors) {
if (!TryGenerateNextActorID(out ushort actorID)) {
s_Logger.Error("Failed to generate actor ID. Probably reached the limit of 65535 actors.");
return;
}
actor.ActorID = actorID;
}
}
//
@@ -164,7 +201,17 @@ namespace RebootKit.Engine.Simulation {
m_InSceneActors.Add(actor);
}
public Actor FindActorByID(ulong actorID) {
Actor FindInSceneActorWithStaticID(ulong staticID) {
foreach (Actor actor in m_InSceneActors) {
if (actor.ActorStaticID == staticID) {
return actor;
}
}
return null;
}
public Actor FindActorByID(ushort actorID) {
foreach (Actor actor in m_InSceneActors) {
if (actor.ActorID == actorID) {
return actor;
@@ -180,186 +227,112 @@ namespace RebootKit.Engine.Simulation {
return null;
}
//
// @MARK: Initial synchronization
//
internal void InitializeActorsForClient(ulong clientID) {
if (RR.NetworkSystemInstance.TryGetClientState(clientID, out NetworkClientState clientState)) {
clientState.SyncState = NetworkClientSyncState.PreparingForActorsSync;
RR.NetworkSystemInstance.UpdateClientState(clientState);
PrepareClientForActorsSyncRpc(RpcTarget.Single(clientState.ClientID, RpcTargetUse.Temp));
} else {
s_Logger.Error($"Client state for {clientID} not found. Cannot synchronize actors.");
}
}
public void CleanUp() {
m_InSceneActors.Clear();
[Rpc(SendTo.SpecifiedInParams, Delivery = RpcDelivery.Reliable)]
void PrepareClientForActorsSyncRpc(RpcParams rpcParams) {
foreach (Actor spawnedActor in m_SpawnedActors) {
Destroy(spawnedActor.gameObject);
foreach (Actor actor in m_SpawnedActors) {
if (actor.OrNull() != null) {
Object.Destroy(actor.gameObject);
}
}
m_SpawnedActors.Clear();
ClientIsReadyForActorsSyncRpc();
}
[Rpc(SendTo.Server)]
void ClientIsReadyForActorsSyncRpc(RpcParams rpcParams = default) {
ulong clientID = rpcParams.Receive.SenderClientId;
if (!RR.NetworkSystemInstance.TryGetClientState(clientID, out NetworkClientState clientState)) {
s_Logger.Error($"Client state for {clientID} not found. Cannot mark client as ready for actors sync.");
return;
}
clientState.SyncState = NetworkClientSyncState.SyncingActors;
clientState.ActorsSyncPacketsLeft = m_InSceneActors.Count;
RR.NetworkSystemInstance.UpdateClientState(clientState);
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) {
RR.NetworkSystemInstance.WriteActorSynchronize(NetworkPacketTarget.Single(clientID),
actor.ActorID,
actor.GetCoreStateSnapshot(),
actor.Data);
}
foreach (Actor actor in m_SpawnedActors) {
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.Server, Delivery = RpcDelivery.Reliable)]
void ClientSynchronizedActorRpc(RpcParams rpcParams = default) {
ulong clientID = rpcParams.Receive.SenderClientId;
if (!RR.NetworkSystemInstance.TryGetClientState(clientID, out NetworkClientState clientState)) {
s_Logger.Error($"Client state for {clientID} not found. Cannot mark client as synchronized.");
return;
}
clientState.ActorsSyncPacketsLeft--;
s_Logger.Info($"Synchronized actor for client {clientID}. Packets left: {clientState.ActorsSyncPacketsLeft}");
RR.NetworkSystemInstance.UpdateClientState(clientState);
if (clientState.ActorsSyncPacketsLeft == 0) {
RR.NetworkSystemInstance.ClientSynchronizedActors(clientID);
}
}
///
/// @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;
}
using NetworkBufferReader reader = new NetworkBufferReader(data);
ActorEvent actorEvent = new ActorEvent();
actorEvent.Deserialize(reader);
actor.HandleActorEvent(actorEvent);
} else if (header.Type == NetworkDataType.ActorCommand) {
if (!RR.IsServer()) {
s_Logger.Error($"Received ActorCommand on client, but this should only be handled on the server.");
return;
}
Actor actor = FindActorByID(header.ActorID);
if (actor == null) {
s_Logger.Error($"Failed to find actor with ID {header.ActorID} for command handling.");
return;
}
using NetworkBufferReader reader = new NetworkBufferReader(data);
ActorCommand actorCommand = new ActorCommand();
actorCommand.Deserialize(reader);
actor.HandleActorCommand(actorCommand);
} 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);
///
internal void ProcessActorEvent(in ActorEvent actorEvent) {
Actor actor = FindActorByID(actorEvent.ActorID);
if (actor == null) {
s_Logger.Error($"Failed to find actor with ID {actorEvent.ActorID} for event handling.");
return;
}
actor.HandleActorEvent(actorEvent);
}
internal void ProcessActorCommand(ulong senderID, in ActorCommand actorCommand) {
if (!RR.IsServer()) {
s_Logger.Error("Received ActorCommand on client, but this should only be handled on the server.");
return;
}
Actor actor = FindActorByID(actorCommand.ActorID);
if (actor == null) {
s_Logger.Error($"Failed to find actor with ID {actorCommand.ActorID} for command handling.");
return;
}
actor.HandleActorCommand(senderID, actorCommand);
}
internal void ProcessActorCoreState(in ActorCoreStateSnapshot coreState) {
if (RR.IsServer()) {
s_Logger.Error("Received ActorCoreState on server, but this should only be handled on the client.");
return;
}
Actor actor = FindActorByID(coreState.ActorID);
if (actor == null) {
s_Logger.Error($"Failed to find actor with ID {coreState.ActorID} for core state update.");
return;
}
actor.RestoreCoreState(coreState);
}
internal void ProcessActorState(ushort actorID, NativeSlice<byte> stateData) {
if (RR.IsServer()) {
s_Logger.Error("Received ActorState on server, but this should only be handled on the client.");
return;
}
Actor actor = FindActorByID(actorID);
if (actor == null) {
s_Logger.Error($"Failed to find actor with ID {actorID} for state update.");
return;
}
DataSerializationUtils.Deserialize(stateData, ref actor.Data);
}
internal void ProcessActorTransformState(in ActorTransformSyncData transformData) {
if (RR.IsServer()) {
s_Logger.Error("Received ActorTransformSync on server, but this should only be handled on the client.");
return;
}
Actor actor = FindActorByID(transformData.ActorID);
if (actor == null) {
s_Logger.Error($"Failed to find actor with ID {transformData.ActorID} for transform state update.");
return;
}
actor.RestoreTransformState(transformData);
}
internal void ProcessSpawnActor(in FixedString64Bytes assetGUID,
in ActorCoreStateSnapshot coreState,
in NativeSlice<byte> stateData) {
if (RR.IsServer()) {
s_Logger.Error("Received SpawnActor on server, but this should only be handled on the client.");
return;
}
if (assetGUID.IsEmpty) {
s_Logger.Error("Received SpawnActor with empty asset GUID, this should not happen.");
return;
}
SpawnLocalActor(assetGUID.ToString(),
coreState,
stateData);
}
void SpawnLocalActor(string guid,
ulong actorID,
ActorCoreStateSnapshot coreStateSnapshot,
NativeArray<byte> stateData) {
NativeSlice<byte> stateData) {
AssetReferenceGameObject assetReference = new AssetReferenceGameObject(guid);
if (!assetReference.RuntimeKeyIsValid()) {
s_Logger.Error($"Invalid asset reference for actor with GUID {guid}");
@@ -377,17 +350,24 @@ namespace RebootKit.Engine.Simulation {
Actor actor = actorObject.GetComponent<Actor>();
if (actor is null) {
s_Logger.Error($"GameObject {actorObject.name} does not have an Actor component.");
Destroy(actorObject);
Object.Destroy(actorObject);
return;
}
actor.Manager = this;
actor.SourceActorPath = guid;
actor.ActorID = actorID;
actor.ActorID = coreStateSnapshot.ActorID;
actor.Data = actor.InternalCreateActorData();
if (!RR.IsServer()) {
actor.InitializeOnClient();
}
actor.RestoreCoreState(coreStateSnapshot);
DataSerializationUtils.Deserialize(stateData, ref actor.Data);
if (stateData.Length > 0) {
DataSerializationUtils.Deserialize(stateData, ref actor.Data);
}
m_SpawnedActors.Add(actor);
}
@@ -399,12 +379,91 @@ namespace RebootKit.Engine.Simulation {
s_Logger.Error("Only the server can send actor events.");
return;
}
RR.NetworkSystemInstance.WriteActorEvent(NetworkPacketTarget.AllClients(), actorEvent);
foreach (ulong clientID in m_Network.Clients.Keys) {
m_Network.WriteActorEvent(clientID, actorEvent);
}
}
internal void SendActorCommand(ActorCommand actorCommand) {
RR.NetworkSystemInstance.WriteActorCommand(actorCommand);
m_Network.WriteActorCommand(actorCommand);
}
///
/// @MARK: Synchronization Helpers
///
// @TODO: We might want to sending this in chunks because it probably wont fit into a single packet in real game.
internal void WriteInSceneActorsStates(NetworkBufferWriter writer) {
writer.Write((ushort) InSceneActorsCount);
foreach (Actor actor in m_InSceneActors) {
writer.Write(actor.ActorStaticID);
writer.Write(actor.ActorID);
actor.GetCoreStateSnapshot().Serialize(writer);
writer.Write((byte) actor.Data.GetMaxBytes());
actor.Data.Serialize(writer);
}
}
internal bool ReadInSceneActorsStates(NetworkBufferReader reader) {
reader.Read(out ushort count);
for (int i = 0; i < count; ++i) {
reader.Read(out ulong actorStaticID);
reader.Read(out ushort actorID);
s_Logger.Info($"Reading actor with StaticID {actorStaticID} and ID {actorID} during synchronization.");
Actor actor = FindInSceneActorWithStaticID(actorStaticID);
if (actor == null) {
s_Logger.Error($"Failed to find actor with ID {actorID} during synchronization.");
return false;
}
ActorCoreStateSnapshot coreState = new ActorCoreStateSnapshot();
coreState.Deserialize(reader);
reader.Read(out byte actorDataSize);
reader.Read(out NativeSlice<byte> stateData, actorDataSize);
actor.RestoreCoreState(coreState);
if (stateData.Length > 0) {
DataSerializationUtils.Deserialize(stateData, ref actor.Data);
}
s_Logger.Info($"Assigning StaticID {actorStaticID} and ID {actorID}");
actor.ActorID = actorID;
s_Logger.Info("Actor id set to " + actor.ActorID);
}
foreach (Actor inSceneActor in m_InSceneActors) {
s_Logger.Info($"InSceneActor: StaticID={inSceneActor.ActorStaticID}, ID={inSceneActor.ActorID}, Path={inSceneActor.SourceActorPath}");
}
return true;
}
internal void SpawnDynamicActorsForClient(ulong clientID) {
if (!RR.IsServer()) {
s_Logger.Error("Only the server can spawn dynamic actors for clients.");
return;
}
foreach (Actor actor in m_SpawnedActors) {
if (actor.OrNull() == null) {
continue;
}
ActorCoreStateSnapshot coreState = actor.GetCoreStateSnapshot();
m_Network.SendSpawnActor(clientID,
actor.SourceActorPath,
coreState,
actor.Data);
}
}
}
}