working on actors
This commit is contained in:
		| @@ -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; | ||||
|         } | ||||
|  | ||||
|   | ||||
							
								
								
									
										12
									
								
								Runtime/Engine/Code/Foundation/UniqueID.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								Runtime/Engine/Code/Foundation/UniqueID.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -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; | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										3
									
								
								Runtime/Engine/Code/Foundation/UniqueID.cs.meta
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								Runtime/Engine/Code/Foundation/UniqueID.cs.meta
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| fileFormatVersion: 2 | ||||
| guid: 7e7c51400f234e5986c5d2779ca00fb3 | ||||
| timeCreated: 1751491055 | ||||
| @@ -10,41 +10,26 @@ namespace RebootKit.Engine.Main { | ||||
|     public abstract class Game : NetworkBehaviour { | ||||
|         static readonly Logger s_GameLogger = new Logger(nameof(Game)); | ||||
|          | ||||
|         protected NetworkVariable<FixedString128Bytes> m_CurrentWorldID = | ||||
|                 new NetworkVariable<FixedString128Bytes>(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)] | ||||
|         public void SendChatMessageRpc(string message) { | ||||
| @@ -56,23 +41,13 @@ namespace RebootKit.Engine.Main { | ||||
|             OnChatMessage(message); | ||||
|         } | ||||
|  | ||||
|         void OnCurrentWorldIDChanged(FixedString128Bytes previousValue, FixedString128Bytes newValue) { | ||||
|             string worldID = newValue.Value; | ||||
|             LoadWorld(worldID); | ||||
|         internal void PlayerBecameReady(ulong clientID) { | ||||
|             if (!IsServer) { | ||||
|                 s_GameLogger.Error("PlayerBecameReady called on client, but this should only be called on the server."); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|         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."); | ||||
|                 } | ||||
|             } | ||||
|             OnPlayerBecameReady(clientID); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										200
									
								
								Runtime/Engine/Code/Main/NetworkSystem.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										200
									
								
								Runtime/Engine/Code/Main/NetworkSystem.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -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<ulong, NetworkClientState> m_Clients = new Dictionary<ulong, NetworkClientState>(); | ||||
|  | ||||
|         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<ulong, NetworkClientState> 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); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										3
									
								
								Runtime/Engine/Code/Main/NetworkSystem.cs.meta
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								Runtime/Engine/Code/Main/NetworkSystem.cs.meta
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| fileFormatVersion: 2 | ||||
| guid: 1f967d37c17e4704b80849c305a53be9 | ||||
| timeCreated: 1751411566 | ||||
| @@ -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<SceneInstance> 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; } | ||||
| @@ -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<NetworkSystem>(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; | ||||
|             } | ||||
|              | ||||
|             GameInstance = null; | ||||
|             if (NetworkSystemInstance is not null) { | ||||
|                 NetworkSystemInstance.NetworkObject.Despawn(); | ||||
|                 NetworkSystemInstance = null; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Console Commands | ||||
|   | ||||
| @@ -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/"; | ||||
|   | ||||
| @@ -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 abstract class Actor : MonoBehaviour { | ||||
|         bool m_IsPlaying = false; | ||||
|         public bool IsPlaying { | ||||
|             get { | ||||
|                 return m_IsPlaying; | ||||
|     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; } | ||||
|     } | ||||
|  | ||||
|             set { | ||||
|                 if (m_IsPlaying == value) { | ||||
|     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<byte> Serialize<TEntity>(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<byte> data = new NativeArray<byte>(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<TEntity>(NativeArray<byte> 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<byte> Data; | ||||
|  | ||||
|         public void NetworkSerialize<T>(BufferSerializer<T> 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<byte> Data; | ||||
|  | ||||
|         public void NetworkSerialize<T>(BufferSerializer<T> 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 { | ||||
|         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; | ||||
|             } | ||||
|  | ||||
|                 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; | ||||
|             } | ||||
|  | ||||
|         public virtual void OnSpawned() { | ||||
|             if (actorCommand.ActorID != ActorID) { | ||||
|                 s_ActorLogger | ||||
|                     .Error($"Actor command ActorID {actorCommand.ActorID} does not match this actor's ID {ActorID}"); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|         public virtual void OnDespawned() { | ||||
|             OnActorCommandServer(actorCommand); | ||||
|         } | ||||
|  | ||||
|         public virtual void OnBeginPlay() { | ||||
|         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; | ||||
|             } | ||||
|  | ||||
|         public virtual void OnEndPlay() { | ||||
|             if (actorEvent.ActorID != ActorID) { | ||||
|                 s_ActorLogger | ||||
|                     .Error($"Actor event ActorID {actorEvent.ActorID} does not match this actor's ID {ActorID}"); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|         public virtual void Tick(float deltaTime) { | ||||
|             OnActorEventClient(actorEvent); | ||||
|         } | ||||
|  | ||||
|         public virtual void SerializeNetworkState(ref DataStreamWriter writer) { | ||||
|         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<TCmdData>(ushort commandID, ref TCmdData commandData) | ||||
|             where TCmdData : struct, ISerializableEntity { | ||||
|             NativeArray<byte> data = DataSerializationUtils.Serialize(commandData); | ||||
|             SendActorCommand(commandID, data); | ||||
|         } | ||||
|  | ||||
|         public virtual void DeserializeNetworkState(ref DataStreamReader reader) { | ||||
|         protected void SendActorCommand(ushort commandID, NativeArray<byte> 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); | ||||
|         } | ||||
|  | ||||
|         protected void SendActorEvent(ushort eventID, NativeArray<byte> 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); | ||||
|         } | ||||
|  | ||||
|         protected T DataAs<T>() where T : IActorData { | ||||
|             if (Data is T data) { | ||||
|                 return data; | ||||
|             } | ||||
|  | ||||
|             throw new System.InvalidCastException($"Actor data is not of type {typeof(T).Name}"); | ||||
|         } | ||||
|  | ||||
|         void OnValidate() { | ||||
|             if (ActorID == 0) { | ||||
|                 ActorID = UniqueID.NewULongFromGuid(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										338
									
								
								Runtime/Engine/Code/Simulation/ActorsManager.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										338
									
								
								Runtime/Engine/Code/Simulation/ActorsManager.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -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<Actor> m_InSceneActors = new List<Actor>(); | ||||
|         readonly List<Actor> m_SpawnedActors = new List<Actor>(); | ||||
|  | ||||
|         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<byte> 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<byte> 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<byte> data) { | ||||
|             Actor actor = FindActorByID(actorID); | ||||
|             if (actor is null) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             DeserializeActorState(actor, data); | ||||
|         } | ||||
|  | ||||
|         NativeArray<byte> SerializeActorState(Actor actor) { | ||||
|             return DataSerializationUtils.Serialize(actor.Data); | ||||
|         } | ||||
|  | ||||
|         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, 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; | ||||
|                 } | ||||
|  | ||||
|                 SpawnActorRpc(actor.SourceActorPath, | ||||
|                               actor.ActorID, | ||||
|                               actor.transform.position, | ||||
|                               actor.transform.localRotation, | ||||
|                               data, | ||||
|                               sendParams); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         [Rpc(SendTo.SpecifiedInParams, Delivery = RpcDelivery.Reliable)] | ||||
|         void SynchronizeActorStateForClientRpc(ulong actorID, NativeArray<byte> 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<Actor>(); | ||||
|             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<byte> 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<byte> 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<Actor>(); | ||||
|             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); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										3
									
								
								Runtime/Engine/Code/Simulation/ActorsManager.cs.meta
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								Runtime/Engine/Code/Simulation/ActorsManager.cs.meta
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| fileFormatVersion: 2 | ||||
| guid: af0c5ff8ac4945eca2a9cab0a88268f4 | ||||
| timeCreated: 1751411111 | ||||
| @@ -4,4 +4,8 @@ namespace RebootKit.Engine.Simulation.Sensors { | ||||
|     public interface ISensor { | ||||
|         GameObject Sense(); | ||||
|     } | ||||
|  | ||||
|     public interface ISensor<out T> where T : class { | ||||
|         T Sense(); | ||||
|     } | ||||
| } | ||||
| @@ -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; | ||||
| @@ -28,13 +29,18 @@ namespace RebootKit.Engine.Simulation { | ||||
|         WorldConfig m_Config; | ||||
|         AsyncOperationHandle<SceneInstance> m_SceneInstance; | ||||
|          | ||||
|         struct ActorData { | ||||
|             public Actor Actor; | ||||
|             public readonly bool ManagedByAddressables; | ||||
|         enum ActorOrigin { | ||||
|             InScene, | ||||
|             Prefab | ||||
|         } | ||||
|  | ||||
|             public ActorData(Actor actor, bool managedByAddressables) { | ||||
|         struct ActorData { | ||||
|             public ActorOrigin Origin; | ||||
|             public Actor Actor; | ||||
|              | ||||
|             public ActorData(ActorOrigin origin, Actor actor) { | ||||
|                 Origin = origin; | ||||
|                 Actor = actor; | ||||
|                 ManagedByAddressables = managedByAddressables; | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @@ -69,6 +75,8 @@ 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<Actor>()) { | ||||
|                     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<TActor> SpawnActor<TActor>(AssetReferenceT<GameObject> 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); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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:  | ||||
|   <Actors>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 | ||||
| @@ -0,0 +1,7 @@ | ||||
| fileFormatVersion: 2 | ||||
| guid: 2cc631d24ab41194ebdeffff7faf62a5 | ||||
| PrefabImporter: | ||||
|   externalObjects: {} | ||||
|   userData:  | ||||
|   assetBundleName:  | ||||
|   assetBundleVariant:  | ||||
| @@ -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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user