This commit is contained in:
2025-07-17 06:36:37 +02:00
parent 4ec3dedd42
commit 1054061d91
52 changed files with 804 additions and 704 deletions

View File

@@ -259,25 +259,35 @@ namespace RebootKit.Engine.Simulation {
public bool IsDataDirty { get; protected internal set; }
internal ActorsManager Manager;
internal DateTime LastCoreStateSyncTime = DateTime.MinValue;
//
// @MARK: Unity callbacks
//
void OnValidate() {
if (ActorID == 0) {
ActorID = UniqueID.NewULongFromGuid();
}
}
//
// @MARK: Callbacks to override in derived classes
//
protected abstract IActorData CreateActorData();
// Override this method to implement server-side logic
public virtual void ServerTick(float deltaTime) { }
// Override this method to implement client-side logic
public virtual void ClientTick(float deltaTime) { }
// @NOTE: Server-side method to handle actor commands
// @MARK: Server side
public virtual void OnServerTick(float deltaTime) { }
protected virtual void OnActorCommandServer(ActorCommand actorCommand) { }
// @NOTE: Client-side method to handle actor events
// Override this method to implement client-side logic
public virtual void OnClientTick(float deltaTime) { }
protected virtual void OnActorEventClient(ActorEvent actorEvent) { }
protected virtual void OnClientFinishedInitialSync() { }
//
// @MARK: Server API
//
public void SetHidden(bool hidden) {
if (!RR.IsServer()) {
s_ActorLogger.Error($"Only the server can set actor visibility. Actor: {name} (ID: {ActorID})");
@@ -383,7 +393,9 @@ namespace RebootKit.Engine.Simulation {
Manager.SynchronizeActorCoreStateWithOther(this);
}
//
// @MARK: Common API
//
public bool IsHidden() {
return !gameObject.activeSelf;
}
@@ -457,7 +469,9 @@ namespace RebootKit.Engine.Simulation {
return false;
}
// @MARK: Internal API
//
// @MARK: Internal
//
internal ActorCoreStateSnapshot GetCoreStateSnapshot() {
ActorCoreStateSnapshot snapshot = new ActorCoreStateSnapshot();
snapshot.Timestamp = DateTime.UtcNow;
@@ -637,7 +651,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})");
@@ -672,13 +690,5 @@ namespace RebootKit.Engine.Simulation {
OnActorEventClient(actorEvent);
}
// @MARK: Unity lifecycle methods
void OnValidate() {
if (ActorID == 0) {
ActorID = UniqueID.NewULongFromGuid();
}
}
}
}

View File

@@ -11,12 +11,20 @@ using Logger = RebootKit.Engine.Foundation.Logger;
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 {
static readonly Logger s_Logger = new Logger(nameof(ActorsManager));
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; } }
//
// @MARK: NetworkBehaviour callbacks
//
public override void OnNetworkSpawn() {
base.OnNetworkSpawn();
RR.ServerTick += OnServerTick;
@@ -27,16 +35,22 @@ namespace RebootKit.Engine.Simulation {
RR.ServerTick -= OnServerTick;
}
//
// @MARK: Unity callbacks
//
void Update() {
foreach (Actor actor in m_InSceneActors) {
actor.ClientTick(Time.deltaTime);
actor.OnClientTick(Time.deltaTime);
}
foreach (Actor actor in m_SpawnedActors) {
actor.ClientTick(Time.deltaTime);
actor.OnClientTick(Time.deltaTime);
}
}
//
// @MARK: Server-side logic
//
void OnServerTick(ulong tick) {
if (!IsServer) {
return;
@@ -50,14 +64,14 @@ namespace RebootKit.Engine.Simulation {
void TickActorsList(List<Actor> actors, float deltaTime) {
foreach (Actor actor in actors) {
actor.ServerTick(deltaTime);
actor.OnServerTick(deltaTime);
if (actor.IsDataDirty) {
actor.IsDataDirty = false;
NativeArray<byte> data = SerializeActorState(actor);
if (data.IsCreated) {
SynchronizeActorStateWithClients(actor.ActorID, data);
SendActorStateToClients(actor.ActorID, data);
} else {
s_Logger.Error($"Failed to serialize actor data for {actor.name}");
}
@@ -84,7 +98,7 @@ namespace RebootKit.Engine.Simulation {
SynchronizeCoreActorStateRpc(actor.ActorID, actor.GetCoreStateSnapshot(), RpcTarget.NotMe);
}
void SynchronizeActorStateWithClients(ulong actorID, NativeArray<byte> data) {
void SendActorStateToClients(ulong actorID, NativeArray<byte> data) {
if (!RR.IsServer()) {
s_Logger.Error("Only the server can synchronize actor states with clients.");
return;
@@ -103,9 +117,8 @@ namespace RebootKit.Engine.Simulation {
if (actor is null) {
return;
}
s_Logger.Info($"Synchronizing actor state for {actor.name} with ID {actorID}");
s_Logger.Info($"Synchronizing actor state for {actor.name} with ID {actorID}");
DeserializeActorState(actor, data);
}
@@ -127,197 +140,10 @@ namespace RebootKit.Engine.Simulation {
void DeserializeActorState(Actor actor, NativeArray<byte> data) {
DataSerializationUtils.Deserialize(data, ref actor.Data);
}
internal void SynchronizeActorsForClient(ulong clientID) {
NetworkClientState clientState = RR.NetworkSystemInstance.GetClientState(clientID);
if (clientState == null) {
s_Logger.Error($"Client state for {clientID} not found. Cannot synchronize actors.");
return;
}
PrepareClientForActorsSyncRpc(RpcTarget.Single(clientState.ClientID, RpcTargetUse.Temp));
}
[Rpc(SendTo.SpecifiedInParams, Delivery = RpcDelivery.Reliable)]
void PrepareClientForActorsSyncRpc(RpcParams rpcParams) {
foreach (Actor spawnedActor in m_SpawnedActors) {
Destroy(spawnedActor.gameObject);
}
m_SpawnedActors.Clear();
ClientIsReadyForActorsSyncRpc();
}
[Rpc(SendTo.Server)]
void ClientIsReadyForActorsSyncRpc(RpcParams rpcParams = default) {
ulong clientID = rpcParams.Receive.SenderClientId;
NetworkClientState clientState = RR.NetworkSystemInstance.GetClientState(clientID);
if (clientState == null) {
s_Logger.Error($"Client state for {clientID} not found. Cannot mark client as ready for actors sync.");
return;
}
clientState.IsReadyForActorsSync = true;
clientState.ActorsSyncPacketsLeft = m_InSceneActors.Count;
RpcSendParams sendParams = RpcTarget.Single(clientID, RpcTargetUse.Temp);
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);
}
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);
}
}
[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;
NetworkClientState clientState = RR.NetworkSystemInstance.GetClientState(clientID);
if (clientState == null) {
s_Logger.Error($"Client state for {clientID} not found. Cannot mark client as synchronized.");
return;
}
clientState.ActorsSyncPacketsLeft--;
if (clientState.ActorsSyncPacketsLeft == 0) {
RR.NetworkSystemInstance.ClientSynchronizedActors(clientID);
}
}
[Rpc(SendTo.Server, Delivery = RpcDelivery.Reliable)]
internal void SendActorCommandToServerRpc(ActorCommand cmd) {
if (!IsServer) {
s_Logger.Error("Only the server can handle actor events.");
return;
}
Actor actor = FindActorByID(cmd.ActorID);
if (actor is null) {
s_Logger.Error($"Actor with ID {cmd.ActorID} not found for command {cmd.CommandID}");
return;
}
actor.HandleActorCommand(cmd);
}
internal void SendActorEvent(ActorEvent actorEvent) {
if (!RR.IsServer()) {
s_Logger.Error("Only the server can send actor events.");
return;
}
foreach ((ulong clientID, NetworkClientState state) in RR.NetworkSystemInstance.Clients) {
if (state.IsReady) {
SendActorEventRpc(actorEvent, RpcTarget.Single(clientID, RpcTargetUse.Temp));
}
}
}
[Rpc(SendTo.SpecifiedInParams)]
void SendActorEventRpc(ActorEvent actorEvent, RpcParams rpcParams) {
Actor actor = FindActorByID(actorEvent.ActorID);
if (actor is null) {
s_Logger.Error($"Actor with ID {actorEvent.ActorID} not found for event {actorEvent.EventID}");
return;
}
actor.HandleActorEvent(actorEvent);
}
public void RegisterInSceneActor(Actor actor) {
if (actor.Data == null) {
actor.Data = actor.InternalCreateActorData();
}
actor.Manager = this;
m_InSceneActors.Add(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();
}
[Rpc(SendTo.NotMe)]
void CleanUpRpc() {
CleanUp();
}
public Actor FindActorByID(ulong actorID) {
foreach (Actor actor in m_InSceneActors) {
if (actor.ActorID == actorID) {
return actor;
}
}
foreach (Actor actor in m_SpawnedActors) {
if (actor.ActorID == actorID) {
return actor;
}
}
return null;
}
//
// @MARK: Server API
//
public Actor SpawnActor(AssetReferenceGameObject assetReference, Vector3 position, Quaternion rotation) {
if (!IsServer) {
s_Logger.Error("Only the server can spawn actors.");
@@ -412,5 +238,205 @@ namespace RebootKit.Engine.Simulation {
Destroy(actor.gameObject);
}
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();
}
[Rpc(SendTo.NotMe)]
void CleanUpRpc() {
CleanUp();
}
//
// @MARK: Common API
//
public void RegisterInSceneActor(Actor actor) {
if (actor.Data == null) {
actor.Data = actor.InternalCreateActorData();
}
actor.Manager = this;
m_InSceneActors.Add(actor);
}
public Actor FindActorByID(ulong actorID) {
foreach (Actor actor in m_InSceneActors) {
if (actor.ActorID == actorID) {
return actor;
}
}
foreach (Actor actor in m_SpawnedActors) {
if (actor.ActorID == actorID) {
return actor;
}
}
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.");
}
}
[Rpc(SendTo.SpecifiedInParams, Delivery = RpcDelivery.Reliable)]
void PrepareClientForActorsSyncRpc(RpcParams rpcParams) {
foreach (Actor spawnedActor in m_SpawnedActors) {
Destroy(spawnedActor.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);
RpcSendParams sendParams = RpcTarget.Single(clientID, RpcTargetUse.Temp);
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);
}
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);
}
}
[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;
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--;
RR.NetworkSystemInstance.UpdateClientState(clientState);
if (clientState.ActorsSyncPacketsLeft == 0) {
RR.NetworkSystemInstance.ClientSynchronizedActors(clientID);
}
}
//
// @MARK: Actor Commands and Events
//
[Rpc(SendTo.Server, Delivery = RpcDelivery.Reliable)]
internal void SendActorCommandToServerRpc(ActorCommand cmd) {
if (!IsServer) {
s_Logger.Error("Only the server can handle actor events.");
return;
}
Actor actor = FindActorByID(cmd.ActorID);
if (actor is null) {
s_Logger.Error($"Actor with ID {cmd.ActorID} not found for command {cmd.CommandID}");
return;
}
actor.HandleActorCommand(cmd);
}
internal void SendActorEvent(ActorEvent actorEvent) {
if (!RR.IsServer()) {
s_Logger.Error("Only the server can send actor events.");
return;
}
foreach ((ulong clientID, NetworkClientState state) in RR.NetworkSystemInstance.Clients) {
if (state.IsReady) {
SendActorEventRpc(actorEvent, RpcTarget.Single(clientID, RpcTargetUse.Temp));
}
}
}
[Rpc(SendTo.SpecifiedInParams)]
void SendActorEventRpc(ActorEvent actorEvent, RpcParams rpcParams) {
Actor actor = FindActorByID(actorEvent.ActorID);
if (actor is null) {
s_Logger.Error($"Actor with ID {actorEvent.ActorID} not found for event {actorEvent.EventID}");
return;
}
actor.HandleActorEvent(actorEvent);
}
}
}