using System; using System.Collections.Generic; using System.Linq; using RebootKit.Engine.Extensions; using RebootKit.Engine.Foundation; using RebootKit.Engine.Main; using RebootKit.Engine.Network; using Unity.Collections; 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 : IDisposable { static readonly Logger s_Logger = new Logger(nameof(ActorsManager)); readonly NetworkSystem m_Network; readonly List m_InSceneActors = new List(); readonly List m_SpawnedActors = new List(); // @NOTE: 0 is reserved for no actor so we should start assigning IDs from 1. ushort m_ActorIDCounter; 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 void Dispose() { } // // @MARK: Update // public void Tick(float deltaTime) { foreach (Actor actor in m_InSceneActors) { actor.OnClientTick(deltaTime); } foreach (Actor actor in m_SpawnedActors) { actor.OnClientTick(deltaTime); } } // // @MARK: Server-side logic // public void ServerTick(float dt) { if (!RR.IsServer()) { return; } TickActorsList(m_InSceneActors, dt); TickActorsList(m_SpawnedActors, dt); } void TickActorsList(List actors, float deltaTime) { foreach (Actor actor in actors) { actor.OnServerTick(deltaTime); if (actor.IsDataDirty) { actor.IsDataDirty = false; if (actor.Data.GetMaxBytes() > 0) { 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; 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; } 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; } } } } // // @MARK: Server API // public Actor SpawnActor(AssetReferenceGameObject assetReference, Vector3 position, Quaternion rotation) { if (!RR.IsServer()) { s_Logger.Error("Only the server can spawn actors."); return null; } if (!assetReference.RuntimeKeyIsValid()) { s_Logger.Error("Trying to spawn an actor with an invalid asset reference."); return null; } 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(); if (actor is null) { s_Logger.Error($"GameObject {actorObject.name} does not have an Actor component."); Object.Destroy(actorObject); return null; } actor.Manager = this; actor.SourceActorPath = assetReference.AssetGUID; actor.ActorID = actorID; actor.Data = actor.InternalCreateActorData(); m_SpawnedActors.Add(actor); 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; } bool TryGenerateNextActorID(out ushort actorID) { m_ActorIDCounter += 1; actorID = m_ActorIDCounter; return true; } 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; } } // // @MARK: Common API // public void RegisterInSceneActor(Actor actor) { if (actor.Data == null) { actor.Data = actor.InternalCreateActorData(); } actor.Manager = this; m_InSceneActors.Add(actor); } 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; } } foreach (Actor actor in m_SpawnedActors) { if (actor.ActorID == actorID) { return actor; } } return null; } public void CleanUp() { m_InSceneActors.Clear(); foreach (Actor actor in m_SpawnedActors) { if (actor.OrNull() != null) { Object.Destroy(actor.gameObject); } } m_SpawnedActors.Clear(); } /// /// @MARK: Network Data Handling /// 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 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 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, ActorCoreStateSnapshot coreStateSnapshot, NativeSlice 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(); if (actor is null) { s_Logger.Error($"GameObject {actorObject.name} does not have an Actor component."); Object.Destroy(actorObject); return; } actor.Manager = this; actor.SourceActorPath = guid; actor.ActorID = coreStateSnapshot.ActorID; actor.Data = actor.InternalCreateActorData(); if (!RR.IsServer()) { actor.InitializeOnClient(); } actor.RestoreCoreState(coreStateSnapshot); if (stateData.Length > 0) { DataSerializationUtils.Deserialize(stateData, ref actor.Data); } m_SpawnedActors.Add(actor); } // // @MARK: Actor Commands and Events // internal void SendActorEvent(ActorEvent actorEvent) { if (!RR.IsServer()) { s_Logger.Error("Only the server can send actor events."); return; } foreach (ulong clientID in m_Network.Clients.Keys) { m_Network.WriteActorEvent(clientID, actorEvent); } } internal void SendActorCommand(ActorCommand 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 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); } } } }