using System.Collections.Generic; using RebootKit.Engine.Foundation; using RebootKit.Engine.Main; using Unity.Collections; using Unity.Netcode; using UnityEngine; using UnityEngine.AddressableAssets; 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. public class ActorsManager : NetworkBehaviour { static readonly Logger s_Logger = new Logger(nameof(ActorsManager)); readonly List m_InSceneActors = new List(); readonly List m_SpawnedActors = new List(); public override void OnNetworkSpawn() { base.OnNetworkSpawn(); RR.ServerTick += OnServerTick; } public override void OnNetworkDespawn() { base.OnNetworkDespawn(); RR.ServerTick -= OnServerTick; } void Update() { foreach (Actor actor in m_InSceneActors) { actor.ClientTick(Time.deltaTime); } } void OnServerTick(ulong tick) { if (!IsServer) { return; } float dt = 1.0f / RR.TickRate.IndexValue; foreach (Actor actor in m_InSceneActors) { actor.ServerTick(dt); if (actor.IsDataDirty) { actor.IsDataDirty = false; NativeArray data = SerializeActorState(actor); if (data.IsCreated) { SynchronizeActorStateClientRpc(actor.ActorID, data); } else { s_Logger.Error($"Failed to serialize actor data for {actor.name}"); } } } foreach (Actor actor in m_SpawnedActors) { actor.ServerTick(dt); if (actor.IsDataDirty) { actor.IsDataDirty = false; NativeArray data = SerializeActorState(actor); if (data.IsCreated) { SynchronizeActorStateClientRpc(actor.ActorID, data); } else { s_Logger.Error($"Failed to serialize actor data for {actor.name}"); } } } } [ClientRpc(Delivery = RpcDelivery.Unreliable)] void SynchronizeActorStateClientRpc(ulong actorID, NativeArray data) { Actor actor = FindActorByID(actorID); if (actor is null) { return; } DeserializeActorState(actor, data); } NativeArray SerializeActorState(Actor actor) { return DataSerializationUtils.Serialize(actor.Data); } void DeserializeActorState(Actor actor, NativeArray 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 data = SerializeActorState(actor); if (!data.IsCreated) { s_Logger.Error($"Failed to serialize actor data for {actor.name}"); continue; } SynchronizeActorStateForClientRpc(actor.ActorID, data, sendParams); } foreach (Actor actor in m_SpawnedActors) { NativeArray data = SerializeActorState(actor); if (!data.IsCreated) { s_Logger.Error($"Failed to serialize actor data for {actor.name}"); continue; } SpawnActorRpc(actor.SourceActorPath, actor.ActorID, actor.transform.position, actor.transform.localRotation, data, sendParams); } } [Rpc(SendTo.SpecifiedInParams, Delivery = RpcDelivery.Reliable)] void SynchronizeActorStateForClientRpc(ulong actorID, NativeArray data, RpcParams rpcParams) { Actor actor = FindActorByID(actorID); if (actor is null) { return; } DeserializeActorState(actor, data); ClientSynchronizedActorRpc(); } [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); } [Rpc(SendTo.Everyone)] internal void SendActorEventToClientsRpc(ActorEvent actorEvent) { 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 is not 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; } public void SpawnActor(AssetReferenceGameObject assetReference, Vector3 position, Quaternion rotation) { if (!IsServer) { s_Logger.Error("Only the server can spawn actors."); return; } if (!assetReference.RuntimeKeyIsValid()) { s_Logger.Error("Trying to spawn an actor with an invalid asset reference."); return; } GameObject actorObject = assetReference.InstantiateAsync(position, rotation).WaitForCompletion(); Actor actor = actorObject.GetComponent(); if (actor is null) { s_Logger.Error($"GameObject {actorObject.name} does not have an Actor component."); Destroy(actorObject); return; } actor.SourceActorPath = assetReference.AssetGUID; actor.ActorID = UniqueID.NewULongFromGuid(); actor.Data = actor.InternalCreateActorData(); m_SpawnedActors.Add(actor); NativeArray stateData = SerializeActorState(actor); SpawnActorRpc(assetReference.AssetGUID, actor.ActorID, position, rotation, stateData, RpcTarget.NotMe); } // @NOTE: This RPC is used to spawn actors on clients. [Rpc(SendTo.SpecifiedInParams)] void SpawnActorRpc(string guid, ulong actorID, Vector3 position, Quaternion rotation, NativeArray 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(position, rotation).WaitForCompletion(); if (actorObject == null) { s_Logger.Error($"Failed to instantiate actor with GUID {guid}"); return; } Actor actor = actorObject.GetComponent(); if (actor is null) { s_Logger.Error($"GameObject {actorObject.name} does not have an Actor component."); Destroy(actorObject); return; } actor.SourceActorPath = guid; actor.ActorID = actorID; actor.Data = actor.InternalCreateActorData(); 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); } } }