diff --git a/Runtime/Engine/Code/Development/DebugOverlayView.cs b/Runtime/Engine/Code/Development/DebugOverlayView.cs index 1eac12c..96b5eda 100644 --- a/Runtime/Engine/Code/Development/DebugOverlayView.cs +++ b/Runtime/Engine/Code/Development/DebugOverlayView.cs @@ -1,5 +1,8 @@ -using RebootKit.Engine.Main; +using System.Text; +using RebootKit.Engine.Main; using RebootKit.Engine.UI; +using Unity.Netcode; +using Unity.Netcode.Transports.UTP; using UnityEngine; using UnityEngine.UIElements; @@ -10,6 +13,7 @@ namespace RebootKit.Engine.Services.Development { VisualElement m_RootElement; Label m_FPSLabel; + Label m_NetworkStatsLabel; void Update() { if (m_RootElement == null) { @@ -18,6 +22,17 @@ namespace RebootKit.Engine.Services.Development { Resolution resolution = Screen.currentResolution; m_FPSLabel.text = $"fps: {Mathf.RoundToInt(1f / Time.deltaTime)} | dt: {Time.deltaTime:F4}ms | runtime: {Time.time:F4}s | resolution: {resolution.width}x{resolution.height}@{resolution.refreshRateRatio}Hz"; + + NetworkManager nm = NetworkManager.Singleton; + + StringBuilder sb = new StringBuilder(); + + sb.Append("Network: "); + sb.Append($"IsServer: {nm.IsServer}"); + sb.Append($" | IsClient: {nm.IsClient}"); + sb.Append($" | IsHost: {nm.IsHost}"); + + m_NetworkStatsLabel.text = sb.ToString(); } public override VisualElement Build() { @@ -26,6 +41,8 @@ namespace RebootKit.Engine.Services.Development { CreateLabel($"Toggle Overlay [F3] | RebootKit | game: {Application.productName}, version: {Application.version}"); m_FPSLabel = CreateLabel($"FPS: {Application.targetFrameRate}"); + m_NetworkStatsLabel = CreateLabel("Network Stats"); + return m_RootElement; } diff --git a/Runtime/Engine/Code/Foundation/UniqueID.cs b/Runtime/Engine/Code/Foundation/UniqueID.cs new file mode 100644 index 0000000..714d039 --- /dev/null +++ b/Runtime/Engine/Code/Foundation/UniqueID.cs @@ -0,0 +1,12 @@ +using System; + +namespace RebootKit.Engine.Foundation { + public static class UniqueID { + public static ulong NewULongFromGuid() { + Guid guid = Guid.NewGuid(); + byte[] bytes = guid.ToByteArray(); + ulong id = BitConverter.ToUInt64(bytes, 0); + return id; + } + } +} \ No newline at end of file diff --git a/Runtime/Engine/Code/Foundation/UniqueID.cs.meta b/Runtime/Engine/Code/Foundation/UniqueID.cs.meta new file mode 100644 index 0000000..4b92ab7 --- /dev/null +++ b/Runtime/Engine/Code/Foundation/UniqueID.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 7e7c51400f234e5986c5d2779ca00fb3 +timeCreated: 1751491055 \ No newline at end of file diff --git a/Runtime/Engine/Code/Main/Game.cs b/Runtime/Engine/Code/Main/Game.cs index 7a37d6f..ebaa357 100644 --- a/Runtime/Engine/Code/Main/Game.cs +++ b/Runtime/Engine/Code/Main/Game.cs @@ -10,40 +10,25 @@ namespace RebootKit.Engine.Main { public abstract class Game : NetworkBehaviour { static readonly Logger s_GameLogger = new Logger(nameof(Game)); - protected NetworkVariable m_CurrentWorldID = - new NetworkVariable(new FixedString128Bytes("")); - + // Server only callbacks + protected virtual void OnPlayerBecameReady(ulong clientID) { + } + // Event callbacks - public virtual void OnWorldLoaded() { - } - - public virtual void OnWorldUnload() { - } - - public virtual void OnChatMessage(string message) { + protected virtual void OnChatMessage(string message) { s_GameLogger.Info($"Chat: {message}"); } - - // Network + + // NGO callbacks public override void OnNetworkSpawn() { base.OnNetworkSpawn(); RR.GameInstance = this; - - m_CurrentWorldID.OnValueChanged += OnCurrentWorldIDChanged; - LoadWorld(m_CurrentWorldID.Value.Value); } public override void OnNetworkDespawn() { base.OnNetworkDespawn(); - - m_CurrentWorldID.OnValueChanged -= OnCurrentWorldIDChanged; RR.GameInstance = null; } - - [ServerRpc] - public void SetCurrentWorldServerRpc(string worldID) { - m_CurrentWorldID.Value = new FixedString128Bytes(worldID); - } // Chat [Rpc(SendTo.Server)] @@ -55,24 +40,14 @@ namespace RebootKit.Engine.Main { void PrintChatMessageClientRpc(string message) { OnChatMessage(message); } - - void OnCurrentWorldIDChanged(FixedString128Bytes previousValue, FixedString128Bytes newValue) { - string worldID = newValue.Value; - LoadWorld(worldID); - } - void LoadWorld(string worldID) { - if (string.IsNullOrEmpty(worldID)) { - RR.World.Unload(); - } else { - WorldConfigAsset worldConfigAsset = RR.GetWorldConfigAsset(worldID); - if (worldConfigAsset is not null) { - RR.CloseMainMenu(); - RR.World.LoadAsync(worldConfigAsset.Config, Application.exitCancellationToken).Forget(); - } else { - s_GameLogger.Error($"World config asset for '{worldID}' not found."); - } + internal void PlayerBecameReady(ulong clientID) { + if (!IsServer) { + s_GameLogger.Error("PlayerBecameReady called on client, but this should only be called on the server."); + return; } + + OnPlayerBecameReady(clientID); } } } \ No newline at end of file diff --git a/Runtime/Engine/Code/Main/NetworkSystem.cs b/Runtime/Engine/Code/Main/NetworkSystem.cs new file mode 100644 index 0000000..0b9fba6 --- /dev/null +++ b/Runtime/Engine/Code/Main/NetworkSystem.cs @@ -0,0 +1,200 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using Cysharp.Threading.Tasks; +using RebootKit.Engine.Services.Simulation; +using RebootKit.Engine.Simulation; +using Unity.Collections; +using Unity.Netcode; +using UnityEngine; +using Logger = RebootKit.Engine.Foundation.Logger; + +namespace RebootKit.Engine.Main { + class NetworkClientState { + public ulong ClientID; + public bool IsWorldLoaded; + public bool AreActorsSynced; + public bool IsReadyForActorsSync; + public int ActorsSyncPacketsLeft; + + public bool IsReady; + } + + public class NetworkSystem : NetworkBehaviour { + static readonly Logger s_Logger = new Logger(nameof(NetworkSystem)); + + [field: SerializeField] public ActorsManager Actors { get; private set; } + + readonly Dictionary m_Clients = new Dictionary(); + + FixedString512Bytes m_WorldID = new FixedString512Bytes(""); + bool m_IsChangingWorld = false; + + void Awake() { + RR.NetworkSystemInstance = this; + } + + 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, + IsWorldLoaded = false, + AreActorsSynced = false, + IsReadyForActorsSync = false, + IsReady = false + }; + m_Clients.Add(clientID, newClientState); + + if (!m_WorldID.IsEmpty) { + s_Logger.Info($"Synchronizing world load for client {clientID} with world ID '{m_WorldID}'"); + ClientLoadWorldRpc(m_WorldID.ToString(), RpcTarget.Single(clientID, RpcTargetUse.Temp)); + } + } + + void OnClientDisconnect(ulong clientID) { + if (!IsServer) { + return; + } + + s_Logger.Info($"OnClientDisconnect: {clientID}"); + m_Clients.Remove(clientID); + } + + internal NetworkClientState GetClientState(ulong clientID) { + if (m_Clients.TryGetValue(clientID, out NetworkClientState clientState)) { + return clientState; + } + + s_Logger.Error($"Client state for {clientID} not found."); + return null; + } + + 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 '{m_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; + } + + m_WorldID = worldID; + + foreach (KeyValuePair kv in m_Clients) { + kv.Value.IsWorldLoaded = false; + kv.Value.AreActorsSynced = false; + kv.Value.IsReadyForActorsSync = false; + kv.Value.IsReady = false; + } + + 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; + + NetworkClientState localClientState = GetClientState(NetworkManager.Singleton.LocalClientId); + localClientState.IsReady = true; + + 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); + + m_WorldID = worldID; + ClientLoadedWorldRpc(worldID); + } + + [Rpc(SendTo.Server, Delivery = RpcDelivery.Reliable)] + void ClientLoadedWorldRpc(string worldID, RpcParams rpcParams = default) { + ulong clientID = rpcParams.Receive.SenderClientId; + + if (!m_WorldID.Equals(worldID)) { + s_Logger.Error($"Client {clientID} tried to load world '{worldID}', but server is in world '{m_WorldID}'."); + NetworkManager.Singleton.DisconnectClient(clientID, "World mismatch!"); + return; + } + + if (m_Clients.TryGetValue(clientID, out NetworkClientState clientState)) { + clientState.IsWorldLoaded = true; + clientState.IsReadyForActorsSync = false; + Actors.SynchronizeActorsForClient(clientID); + } else { + NetworkManager.Singleton.DisconnectClient(clientID, "Client is not registered!"); + } + } + + internal void ClientSynchronizedActors(ulong clientID) { + NetworkClientState clientState = GetClientState(clientID); + if (clientState is null) { + s_Logger.Error($"Client state for {clientID} not found."); + return; + } + + clientState.IsReady = true; + RR.GameInstance.PlayerBecameReady(clientID); + } + } +} \ No newline at end of file diff --git a/Runtime/Engine/Code/Main/NetworkSystem.cs.meta b/Runtime/Engine/Code/Main/NetworkSystem.cs.meta new file mode 100644 index 0000000..ecd69cc --- /dev/null +++ b/Runtime/Engine/Code/Main/NetworkSystem.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 1f967d37c17e4704b80849c305a53be9 +timeCreated: 1751411566 \ No newline at end of file diff --git a/Runtime/Engine/Code/Main/RR.cs b/Runtime/Engine/Code/Main/RR.cs index 1d764b7..c32b143 100755 --- a/Runtime/Engine/Code/Main/RR.cs +++ b/Runtime/Engine/Code/Main/RR.cs @@ -27,7 +27,7 @@ namespace RebootKit.Engine.Main { [ConfigVar("con.write_log", 1, "Enables writing game log to console output")] static ConfigVar s_writeLogToConsole; - [ConfigVar("sv.tick_rate", 60, "Server tick rate in Hz")] + [ConfigVar("sv.tick_rate", 24, "Server tick rate in Hz")] public static ConfigVar TickRate; internal static EngineConfigAsset EngineConfig; @@ -37,6 +37,9 @@ namespace RebootKit.Engine.Main { static AsyncOperationHandle s_mainMenuSceneHandle; + static NetworkSystem s_networkSystemPrefab; + internal static NetworkSystem NetworkSystemInstance; + internal static ConsoleService Console { get; private set; } public static InputService Input { get; private set; } public static WorldService World { get; private set; } @@ -60,7 +63,7 @@ namespace RebootKit.Engine.Main { s_Logger.Info("Initializing"); s_servicesBag = new DisposableBag(); s_disposableBag = new DisposableBag(); - + s_Logger.Info("Registering core services"); Console = CreateService(EngineConfig.coreServices.consoleService); Input = CreateService(EngineConfig.coreServices.inputService); @@ -68,11 +71,14 @@ namespace RebootKit.Engine.Main { await InitializeAssetsAsync(cancellationToken); - await SteamManager.InitializeAsync(cancellationToken); + // await SteamManager.InitializeAsync(cancellationToken); } // @NOTE: This method is called after the main scene is loaded. internal static async UniTask RunAsync(CancellationToken cancellationToken) { + s_networkSystemPrefab = + Resources.Load(RConsts.k_CoreNetworkGameSystemsResourcesPath); + NetworkManager.Singleton.OnConnectionEvent += OnConnectionEvent; NetworkManager.Singleton.OnServerStarted += OnServerStarted; NetworkManager.Singleton.OnServerStopped += OnServerStopped; @@ -111,7 +117,7 @@ namespace RebootKit.Engine.Main { NetworkManager.Singleton.OnServerStopped -= OnServerStopped; } - SteamManager.Shutdown(); + // SteamManager.Shutdown(); s_servicesBag.Dispose(); s_disposableBag.Dispose(); @@ -138,7 +144,7 @@ namespace RebootKit.Engine.Main { WorldConfigAsset worldConfig = s_WorldConfigsAssets.Find(asset => asset.Config.name.Equals(name, StringComparison.Ordinal)); - if (!worldConfig) { + if (worldConfig is null) { throw new KeyNotFoundException($"World config '{name}' not found"); } @@ -187,7 +193,29 @@ namespace RebootKit.Engine.Main { return; } - GameInstance.SetCurrentWorldServerRpc(worldID); + NetworkSystemInstance.SetCurrentWorld(worldID); + } + + public static void SpawnActor(AssetReferenceGameObject assetReference, + Vector3 position, + Quaternion rotation) { + if (!IsServer()) { + s_Logger.Error("Cannot spawn actor. Not a server instance."); + return; + } + + if (NetworkSystemInstance is null) { + s_Logger.Error("NetworkSystemInstance is not initialized. Cannot spawn actor."); + return; + } + + if (!assetReference.RuntimeKeyIsValid()) { + s_Logger.Error("Asset reference is not valid. Cannot spawn actor."); + return; + } + + s_Logger.Info($"Spawning actor from asset reference: {assetReference.RuntimeKey}"); + NetworkSystemInstance.Actors.SpawnActor(assetReference, position, rotation); } // Service API @@ -343,8 +371,6 @@ namespace RebootKit.Engine.Main { TickCount++; } - - World.Tick(deltaTime); } static void OnConnectionEvent(NetworkManager network, ConnectionEventData data) { @@ -356,6 +382,9 @@ namespace RebootKit.Engine.Main { GameInstance = Object.Instantiate(EngineConfig.gamePrefab); GameInstance.NetworkObject.Spawn(); + + NetworkSystemInstance = Object.Instantiate(s_networkSystemPrefab); + NetworkSystemInstance.NetworkObject.Spawn(); } static void OnServerStopped(bool obj) { @@ -363,10 +392,13 @@ namespace RebootKit.Engine.Main { if (GameInstance is not null) { GameInstance.NetworkObject.Despawn(); - Object.Destroy(GameInstance.gameObject); + GameInstance = null; + } + + if (NetworkSystemInstance is not null) { + NetworkSystemInstance.NetworkObject.Despawn(); + NetworkSystemInstance = null; } - - GameInstance = null; } // Console Commands diff --git a/Runtime/Engine/Code/RConsts.cs b/Runtime/Engine/Code/RConsts.cs index 66ee08d..7ccdd52 100755 --- a/Runtime/Engine/Code/RConsts.cs +++ b/Runtime/Engine/Code/RConsts.cs @@ -8,6 +8,8 @@ public const string k_EngineConfigResourcesPath = "TheGame/" + k_EngineConfigAssetName; public const string k_EngineConfigAssetPath = "Assets/TheGame/" + k_EngineConfigAssetName + ".asset"; + public const string k_CoreNetworkGameSystemsResourcesPath = "RebootKit/core_network_game_systems"; + internal const string k_AddComponentMenu = "Reboot Reality/"; internal const string k_CreateAssetMenu = "Reboot Reality/"; diff --git a/Runtime/Engine/Code/Simulation/Actor.cs b/Runtime/Engine/Code/Simulation/Actor.cs index 7356517..f9fd246 100644 --- a/Runtime/Engine/Code/Simulation/Actor.cs +++ b/Runtime/Engine/Code/Simulation/Actor.cs @@ -1,48 +1,247 @@ -using Unity.Collections; +using System; +using NUnit.Framework; +using RebootKit.Engine.Foundation; +using RebootKit.Engine.Main; +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 Deserialize(FastBufferReader reader) { } + } + + public static class DataSerializationUtils { + public const int k_DefaultMessageSize = 256; + + public static NativeArray Serialize(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 data = new NativeArray(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(NativeArray data, ref TEntity entity) + where TEntity : ISerializableEntity { + using FastBufferReader reader = new FastBufferReader(data, Allocator.Temp); + if (reader.TryBeginRead(data.Length)) { + entity.Deserialize(reader); + } + } + } + + public struct ActorCommand : INetworkSerializable { + public ulong ActorID; + public ulong ClientID; + public ushort CommandID; + public NativeArray Data; + + public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter { + serializer.SerializeValue(ref ActorID); + serializer.SerializeValue(ref ClientID); + serializer.SerializeValue(ref CommandID); + + if (serializer.IsWriter) { + bool hasData = Data.IsCreated; + serializer.SerializeValue(ref hasData); + + if (hasData) { + serializer.SerializeValue(ref Data, Allocator.Temp); + } + } else if (serializer.IsReader) { + bool hasData = false; + serializer.SerializeValue(ref hasData); + + if (hasData) { + serializer.SerializeValue(ref Data, Allocator.Temp); + } + } + } + } + + public struct ActorEvent : INetworkSerializable { + public ulong ActorID; + public ulong ClientID; + public ushort EventID; + public NativeArray Data; + + public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter { + serializer.SerializeValue(ref ActorID); + serializer.SerializeValue(ref ClientID); + serializer.SerializeValue(ref EventID); + + if (serializer.IsWriter) { + bool hasData = Data.IsCreated; + serializer.SerializeValue(ref hasData); + + if (hasData) { + serializer.SerializeValue(ref Data, Allocator.Temp); + } + } else if (serializer.IsReader) { + bool hasData = false; + serializer.SerializeValue(ref hasData); + + if (hasData) { + serializer.SerializeValue(ref Data, Allocator.Temp); + } + } + } + } + public abstract class Actor : MonoBehaviour { - bool m_IsPlaying = false; - public bool IsPlaying { - get { - return m_IsPlaying; + static readonly Logger s_ActorLogger = new Logger(nameof(Actor)); + + [field: SerializeField, TriInspector.ReadOnly] public string SourceActorPath { get; internal set; } = ""; + [field: SerializeField, ReadOnly] public ulong ActorID { get; internal set; } + + [NonSerialized] internal IActorData Data; + + public bool IsDataDirty { get; protected internal set; } + + internal ActorsManager Manager; + + internal IActorData InternalCreateActorData() { + return CreateActorData(); + } + + internal void HandleActorCommand(ActorCommand actorCommand) { + if (!RR.IsServer()) { + s_ActorLogger.Error($"Only the server can handle actor commands. Actor: {name} (ID: {ActorID})"); + return; } - set { - if (m_IsPlaying == value) { - return; - } - - m_IsPlaying = value; - - if (m_IsPlaying) { - OnBeginPlay(); - } else { - OnEndPlay(); - } + if (Manager is null) { + s_ActorLogger.Error($"Cannot handle command because Manager is null for actor {name} (ID: {ActorID})"); + return; } + + if (actorCommand.ActorID != ActorID) { + s_ActorLogger + .Error($"Actor command ActorID {actorCommand.ActorID} does not match this actor's ID {ActorID}"); + return; + } + + OnActorCommandServer(actorCommand); } - public virtual void OnSpawned() { + internal void HandleActorEvent(ActorEvent actorEvent) { + if (Manager is null) { + s_ActorLogger.Error($"Cannot handle event because Manager is null for actor {name} (ID: {ActorID})"); + return; + } + + if (actorEvent.ActorID != ActorID) { + s_ActorLogger + .Error($"Actor event ActorID {actorEvent.ActorID} does not match this actor's ID {ActorID}"); + return; + } + + OnActorEventClient(actorEvent); } - public virtual void OnDespawned() { + 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 + protected virtual void OnActorCommandServer(ActorCommand actorCommand) { } + + // @NOTE: Client-side method to handle actor events + protected virtual void OnActorEventClient(ActorEvent actorEvent) { } + + protected void SendActorCommand(ushort commandID, ref TCmdData commandData) + where TCmdData : struct, ISerializableEntity { + NativeArray data = DataSerializationUtils.Serialize(commandData); + SendActorCommand(commandID, data); } - public virtual void OnBeginPlay() { + protected void SendActorCommand(ushort commandID, NativeArray data = default) { + if (Manager is null) { + s_ActorLogger.Error($"Cannot send command because Manager is null for actor {name} (ID: {ActorID})"); + return; + } + + ActorCommand command = new ActorCommand { + ActorID = ActorID, + ClientID = NetworkManager.Singleton.LocalClientId, + CommandID = commandID, + Data = data + }; + + Manager.SendActorCommandToServerRpc(command); } - public virtual void OnEndPlay() { + protected void SendActorEvent(ushort eventID, NativeArray data = default) { + if (!RR.IsServer()) { + s_ActorLogger.Error($"Only the server can send actor events. Actor: {name} (ID: {ActorID})"); + return; + } + + if (Manager is null) { + s_ActorLogger.Error($"Cannot send event because Manager is null for actor {name} (ID: {ActorID})"); + return; + } + + ActorEvent actorEvent = new ActorEvent { + ActorID = ActorID, + ClientID = NetworkManager.Singleton.LocalClientId, + EventID = eventID, + Data = data + }; + + Manager.SendActorEventToClientsRpc(actorEvent); } - public virtual void Tick(float deltaTime) { - } - - public virtual void SerializeNetworkState(ref DataStreamWriter writer) { + protected T DataAs() where T : IActorData { + if (Data is T data) { + return data; + } + + throw new System.InvalidCastException($"Actor data is not of type {typeof(T).Name}"); } - public virtual void DeserializeNetworkState(ref DataStreamReader reader) { + void OnValidate() { + if (ActorID == 0) { + ActorID = UniqueID.NewULongFromGuid(); + } } } } \ No newline at end of file diff --git a/Runtime/Engine/Code/Simulation/ActorsManager.cs b/Runtime/Engine/Code/Simulation/ActorsManager.cs new file mode 100644 index 0000000..4b6c11e --- /dev/null +++ b/Runtime/Engine/Code/Simulation/ActorsManager.cs @@ -0,0 +1,338 @@ +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); + } + } +} \ No newline at end of file diff --git a/Runtime/Engine/Code/Simulation/ActorsManager.cs.meta b/Runtime/Engine/Code/Simulation/ActorsManager.cs.meta new file mode 100644 index 0000000..36e1b97 --- /dev/null +++ b/Runtime/Engine/Code/Simulation/ActorsManager.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: af0c5ff8ac4945eca2a9cab0a88268f4 +timeCreated: 1751411111 \ No newline at end of file diff --git a/Runtime/Engine/Code/Simulation/Sensors/ISensor.cs b/Runtime/Engine/Code/Simulation/Sensors/ISensor.cs index 9e4bfc3..de56812 100644 --- a/Runtime/Engine/Code/Simulation/Sensors/ISensor.cs +++ b/Runtime/Engine/Code/Simulation/Sensors/ISensor.cs @@ -4,4 +4,8 @@ namespace RebootKit.Engine.Simulation.Sensors { public interface ISensor { GameObject Sense(); } + + public interface ISensor where T : class { + T Sense(); + } } \ No newline at end of file diff --git a/Runtime/Engine/Code/Simulation/WorldService.cs b/Runtime/Engine/Code/Simulation/WorldService.cs index 879be62..ccfd47f 100644 --- a/Runtime/Engine/Code/Simulation/WorldService.cs +++ b/Runtime/Engine/Code/Simulation/WorldService.cs @@ -4,6 +4,7 @@ using Cysharp.Threading.Tasks; using RebootKit.Engine.Foundation; using RebootKit.Engine.Main; using RebootKit.Engine.Services.Simulation; +using Unity.Netcode; using UnityEngine; using UnityEngine.AddressableAssets; using UnityEngine.Assertions; @@ -27,14 +28,19 @@ namespace RebootKit.Engine.Simulation { WorldState m_WorldState = WorldState.Unloaded; WorldConfig m_Config; AsyncOperationHandle m_SceneInstance; + + enum ActorOrigin { + InScene, + Prefab + } struct ActorData { + public ActorOrigin Origin; public Actor Actor; - public readonly bool ManagedByAddressables; - public ActorData(Actor actor, bool managedByAddressables) { + public ActorData(ActorOrigin origin, Actor actor) { + Origin = origin; Actor = actor; - ManagedByAddressables = managedByAddressables; } } @@ -68,7 +74,9 @@ namespace RebootKit.Engine.Simulation { await m_SceneInstance.Result.ActivateAsync(); SceneManager.SetActiveScene(m_SceneInstance.Result.Scene); - + + // await UniTask.WaitWhile(() => RR.CoreNetworkGameSystemsInstance is null, cancellationToken: cancellationToken); + foreach (GameObject root in m_SceneInstance.Result.Scene.GetRootGameObjects()) { if (root.TryGetComponent(out IWorldContext worldContext)) { Assert.IsNull(Context, @@ -77,15 +85,11 @@ namespace RebootKit.Engine.Simulation { } foreach (Actor actor in root.GetComponentsInChildren()) { - m_Actors.Add(new ActorData(actor, false)); + RR.NetworkSystemInstance.Actors.RegisterInSceneActor(actor); } } m_WorldState = WorldState.Loaded; - - if (RR.GameInstance is not null) { - RR.GameInstance.OnWorldLoaded(); - } } public void Unload() { @@ -93,12 +97,10 @@ namespace RebootKit.Engine.Simulation { return; } - if (RR.GameInstance is not null) { - RR.GameInstance.OnWorldUnload(); + if (RR.NetworkSystemInstance is not null) { + RR.NetworkSystemInstance.Actors.CleanUp(); } - KillAllActors(); - if (m_SceneInstance.IsValid()) { m_SceneInstance.Release(); m_SceneInstance = default; @@ -107,79 +109,5 @@ namespace RebootKit.Engine.Simulation { m_WorldState = WorldState.Unloaded; Context = null; } - - public async UniTask SpawnActor(AssetReferenceT asset, - CancellationToken cancellationToken) where TActor : Actor { - if (m_WorldState != WorldState.Loaded) { - s_Logger.Error("World is not loaded. Cannot spawn actor."); - return null; - } - - GameObject gameObject = await Addressables.InstantiateAsync(asset); - if (cancellationToken.IsCancellationRequested) { - asset.ReleaseInstance(gameObject); - return null; - } - - if (gameObject.TryGetComponent(out TActor actor)) { - actor.OnSpawned(); - actor.IsPlaying = true; - m_Actors.Add(new ActorData(actor, true)); - return actor; - } - - asset.ReleaseInstance(gameObject); - return null; - } - - public void KillActor(Actor actor) { - ActorData actorData = default; - bool found = false; - for (int i = m_Actors.Count - 1; i >= 0; i--) { - if (m_Actors[i].Actor == actor) { - found = true; - actorData = m_Actors[i]; - m_Actors.RemoveAt(i); - break; - } - } - Assert.IsTrue(found, $"Actor {actor.name} not found in the world actors list."); - - actor.IsPlaying = false; - actor.OnDespawned(); - - if (actorData.ManagedByAddressables) { - Addressables.ReleaseInstance(actor.gameObject); - } - } - - public void KillAllActors() { - foreach (ActorData actorData in m_Actors) { - actorData.Actor.IsPlaying = false; - actorData.Actor.OnDespawned(); - - if (actorData.ManagedByAddressables) { - Addressables.ReleaseInstance(actorData.Actor.gameObject); - } else { - UnityEngine.Object.Destroy(actorData.Actor.gameObject); - } - } - - m_Actors.Clear(); - } - - public void Tick(float deltaTime) { - if (m_WorldState != WorldState.Loaded) { - return; - } - - foreach (ActorData actorData in m_Actors) { - Actor actor = actorData.Actor; - - if (actor.IsPlaying) { - actor.Tick(deltaTime); - } - } - } } } \ No newline at end of file diff --git a/Runtime/Engine/Resources/RebootKit/core_network_game_systems.prefab b/Runtime/Engine/Resources/RebootKit/core_network_game_systems.prefab new file mode 100644 index 0000000..efa4b1c --- /dev/null +++ b/Runtime/Engine/Resources/RebootKit/core_network_game_systems.prefab @@ -0,0 +1,87 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &1321683558189709310 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1878935056881269035} + - component: {fileID: 258784011466397156} + - component: {fileID: 8437866359923088342} + - component: {fileID: 2149791309811179493} + m_Layer: 0 + m_Name: core_network_game_systems + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1878935056881269035 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1321683558189709310} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &258784011466397156 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1321683558189709310} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3} + m_Name: + m_EditorClassIdentifier: + GlobalObjectIdHash: 2588764075 + InScenePlacedSourceGlobalObjectIdHash: 0 + DeferredDespawnTick: 0 + Ownership: 1 + AlwaysReplicateAsRoot: 0 + SynchronizeTransform: 0 + ActiveSceneSynchronization: 0 + SceneMigrationSynchronization: 0 + SpawnWithObservers: 1 + DontDestroyWithOwner: 0 + AutoObjectParentSync: 0 + SyncOwnerTransformWhenParented: 0 + AllowOwnerToParent: 0 +--- !u!114 &8437866359923088342 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1321683558189709310} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 1f967d37c17e4704b80849c305a53be9, type: 3} + m_Name: + m_EditorClassIdentifier: + k__BackingField: {fileID: 2149791309811179493} +--- !u!114 &2149791309811179493 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1321683558189709310} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: af0c5ff8ac4945eca2a9cab0a88268f4, type: 3} + m_Name: + m_EditorClassIdentifier: + ShowTopMostFoldoutHeaderGroup: 1 diff --git a/Runtime/Engine/Resources/RebootKit/core_network_game_systems.prefab.meta b/Runtime/Engine/Resources/RebootKit/core_network_game_systems.prefab.meta new file mode 100644 index 0000000..e77881a --- /dev/null +++ b/Runtime/Engine/Resources/RebootKit/core_network_game_systems.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 2cc631d24ab41194ebdeffff7faf62a5 +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Engine/core_assets/boot/scn_boot.unity b/Runtime/Engine/core_assets/boot/scn_boot.unity index f4ac9e0..cfe4070 100755 --- a/Runtime/Engine/core_assets/boot/scn_boot.unity +++ b/Runtime/Engine/core_assets/boot/scn_boot.unity @@ -421,7 +421,7 @@ MonoBehaviour: NetworkManagerExpanded: 0 NetworkConfig: ProtocolVersion: 0 - NetworkTransport: {fileID: 1456272201} + NetworkTransport: {fileID: 1456272198} PlayerPrefab: {fileID: 0} Prefabs: NetworkPrefabsLists: @@ -434,7 +434,7 @@ MonoBehaviour: TimeResyncInterval: 30 EnsureNetworkVariableLengthSafety: 0 EnableSceneManagement: 0 - ForceSamePrefabs: 1 + ForceSamePrefabs: 0 RecycleNetworkIds: 1 NetworkIdRecycleDelay: 120 RpcHashSize: 0