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 RebootKit.Engine.UI; | ||||||
|  | using Unity.Netcode; | ||||||
|  | using Unity.Netcode.Transports.UTP; | ||||||
| using UnityEngine; | using UnityEngine; | ||||||
| using UnityEngine.UIElements; | using UnityEngine.UIElements; | ||||||
|  |  | ||||||
| @@ -10,6 +13,7 @@ namespace RebootKit.Engine.Services.Development { | |||||||
|         VisualElement m_RootElement; |         VisualElement m_RootElement; | ||||||
|          |          | ||||||
|         Label m_FPSLabel; |         Label m_FPSLabel; | ||||||
|  |         Label m_NetworkStatsLabel; | ||||||
|  |  | ||||||
|         void Update() { |         void Update() { | ||||||
|             if (m_RootElement == null) { |             if (m_RootElement == null) { | ||||||
| @@ -18,6 +22,17 @@ namespace RebootKit.Engine.Services.Development { | |||||||
|  |  | ||||||
|             Resolution resolution = Screen.currentResolution; |             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"; |             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() { |         public override VisualElement Build() { | ||||||
| @@ -26,6 +41,8 @@ namespace RebootKit.Engine.Services.Development { | |||||||
|             CreateLabel($"Toggle Overlay [F3] | RebootKit | game: {Application.productName}, version: {Application.version}"); |             CreateLabel($"Toggle Overlay [F3] | RebootKit | game: {Application.productName}, version: {Application.version}"); | ||||||
|             m_FPSLabel = CreateLabel($"FPS: {Application.targetFrameRate}"); |             m_FPSLabel = CreateLabel($"FPS: {Application.targetFrameRate}"); | ||||||
|              |              | ||||||
|  |             m_NetworkStatsLabel = CreateLabel("Network Stats"); | ||||||
|  |              | ||||||
|             return m_RootElement; |             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,40 +10,25 @@ namespace RebootKit.Engine.Main { | |||||||
|     public abstract class Game : NetworkBehaviour { |     public abstract class Game : NetworkBehaviour { | ||||||
|         static readonly Logger s_GameLogger = new Logger(nameof(Game)); |         static readonly Logger s_GameLogger = new Logger(nameof(Game)); | ||||||
|          |          | ||||||
|         protected NetworkVariable<FixedString128Bytes> m_CurrentWorldID = |         // Server only callbacks | ||||||
|                 new NetworkVariable<FixedString128Bytes>(new FixedString128Bytes("")); |         protected virtual void OnPlayerBecameReady(ulong clientID) { | ||||||
|          |         } | ||||||
|  |  | ||||||
|         // Event callbacks |         // Event callbacks | ||||||
|         public virtual void OnWorldLoaded() { |         protected virtual void OnChatMessage(string message) { | ||||||
|         } |  | ||||||
|  |  | ||||||
|         public virtual void OnWorldUnload() { |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         public virtual void OnChatMessage(string message) { |  | ||||||
|             s_GameLogger.Info($"Chat: {message}"); |             s_GameLogger.Info($"Chat: {message}"); | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         // Network |         // NGO callbacks | ||||||
|         public override void OnNetworkSpawn() { |         public override void OnNetworkSpawn() { | ||||||
|             base.OnNetworkSpawn(); |             base.OnNetworkSpawn(); | ||||||
|             RR.GameInstance = this; |             RR.GameInstance = this; | ||||||
|  |  | ||||||
|             m_CurrentWorldID.OnValueChanged += OnCurrentWorldIDChanged; |  | ||||||
|             LoadWorld(m_CurrentWorldID.Value.Value); |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         public override void OnNetworkDespawn() { |         public override void OnNetworkDespawn() { | ||||||
|             base.OnNetworkDespawn(); |             base.OnNetworkDespawn(); | ||||||
|  |  | ||||||
|             m_CurrentWorldID.OnValueChanged -= OnCurrentWorldIDChanged; |  | ||||||
|             RR.GameInstance = null; |             RR.GameInstance = null; | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         [ServerRpc] |  | ||||||
|         public void SetCurrentWorldServerRpc(string worldID) { |  | ||||||
|             m_CurrentWorldID.Value = new FixedString128Bytes(worldID); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // Chat |         // Chat | ||||||
|         [Rpc(SendTo.Server)] |         [Rpc(SendTo.Server)] | ||||||
| @@ -55,24 +40,14 @@ namespace RebootKit.Engine.Main { | |||||||
|         void PrintChatMessageClientRpc(string message) { |         void PrintChatMessageClientRpc(string message) { | ||||||
|             OnChatMessage(message); |             OnChatMessage(message); | ||||||
|         } |         } | ||||||
|          |  | ||||||
|         void OnCurrentWorldIDChanged(FixedString128Bytes previousValue, FixedString128Bytes newValue) { |  | ||||||
|             string worldID = newValue.Value; |  | ||||||
|             LoadWorld(worldID); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         void LoadWorld(string worldID) { |         internal void PlayerBecameReady(ulong clientID) { | ||||||
|             if (string.IsNullOrEmpty(worldID)) { |             if (!IsServer) { | ||||||
|                 RR.World.Unload(); |                 s_GameLogger.Error("PlayerBecameReady called on client, but this should only be called on the server."); | ||||||
|             } else { |                 return; | ||||||
|                 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")] |         [ConfigVar("con.write_log", 1, "Enables writing game log to console output")] | ||||||
|         static ConfigVar s_writeLogToConsole; |         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; |         public static ConfigVar TickRate; | ||||||
|  |  | ||||||
|         internal static EngineConfigAsset EngineConfig; |         internal static EngineConfigAsset EngineConfig; | ||||||
| @@ -37,6 +37,9 @@ namespace RebootKit.Engine.Main { | |||||||
|  |  | ||||||
|         static AsyncOperationHandle<SceneInstance> s_mainMenuSceneHandle; |         static AsyncOperationHandle<SceneInstance> s_mainMenuSceneHandle; | ||||||
|  |  | ||||||
|  |         static NetworkSystem s_networkSystemPrefab; | ||||||
|  |         internal static NetworkSystem NetworkSystemInstance; | ||||||
|  |  | ||||||
|         internal static ConsoleService Console { get; private set; } |         internal static ConsoleService Console { get; private set; } | ||||||
|         public static InputService Input { get; private set; } |         public static InputService Input { get; private set; } | ||||||
|         public static WorldService World { get; private set; } |         public static WorldService World { get; private set; } | ||||||
| @@ -60,7 +63,7 @@ namespace RebootKit.Engine.Main { | |||||||
|             s_Logger.Info("Initializing"); |             s_Logger.Info("Initializing"); | ||||||
|             s_servicesBag = new DisposableBag(); |             s_servicesBag = new DisposableBag(); | ||||||
|             s_disposableBag = new DisposableBag(); |             s_disposableBag = new DisposableBag(); | ||||||
|  |              | ||||||
|             s_Logger.Info("Registering core services"); |             s_Logger.Info("Registering core services"); | ||||||
|             Console = CreateService(EngineConfig.coreServices.consoleService); |             Console = CreateService(EngineConfig.coreServices.consoleService); | ||||||
|             Input = CreateService(EngineConfig.coreServices.inputService); |             Input = CreateService(EngineConfig.coreServices.inputService); | ||||||
| @@ -68,11 +71,14 @@ namespace RebootKit.Engine.Main { | |||||||
|  |  | ||||||
|             await InitializeAssetsAsync(cancellationToken); |             await InitializeAssetsAsync(cancellationToken); | ||||||
|  |  | ||||||
|             await SteamManager.InitializeAsync(cancellationToken); |             // await SteamManager.InitializeAsync(cancellationToken); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // @NOTE: This method is called after the main scene is loaded. |         // @NOTE: This method is called after the main scene is loaded. | ||||||
|         internal static async UniTask RunAsync(CancellationToken cancellationToken) { |         internal static async UniTask RunAsync(CancellationToken cancellationToken) { | ||||||
|  |             s_networkSystemPrefab = | ||||||
|  |                 Resources.Load<NetworkSystem>(RConsts.k_CoreNetworkGameSystemsResourcesPath); | ||||||
|  |  | ||||||
|             NetworkManager.Singleton.OnConnectionEvent += OnConnectionEvent; |             NetworkManager.Singleton.OnConnectionEvent += OnConnectionEvent; | ||||||
|             NetworkManager.Singleton.OnServerStarted += OnServerStarted; |             NetworkManager.Singleton.OnServerStarted += OnServerStarted; | ||||||
|             NetworkManager.Singleton.OnServerStopped += OnServerStopped; |             NetworkManager.Singleton.OnServerStopped += OnServerStopped; | ||||||
| @@ -111,7 +117,7 @@ namespace RebootKit.Engine.Main { | |||||||
|                 NetworkManager.Singleton.OnServerStopped -= OnServerStopped; |                 NetworkManager.Singleton.OnServerStopped -= OnServerStopped; | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             SteamManager.Shutdown(); |             // SteamManager.Shutdown(); | ||||||
|  |  | ||||||
|             s_servicesBag.Dispose(); |             s_servicesBag.Dispose(); | ||||||
|             s_disposableBag.Dispose(); |             s_disposableBag.Dispose(); | ||||||
| @@ -138,7 +144,7 @@ namespace RebootKit.Engine.Main { | |||||||
|  |  | ||||||
|             WorldConfigAsset worldConfig = |             WorldConfigAsset worldConfig = | ||||||
|                 s_WorldConfigsAssets.Find(asset => asset.Config.name.Equals(name, StringComparison.Ordinal)); |                 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"); |                 throw new KeyNotFoundException($"World config '{name}' not found"); | ||||||
|             } |             } | ||||||
|  |  | ||||||
| @@ -187,7 +193,29 @@ namespace RebootKit.Engine.Main { | |||||||
|                 return; |                 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 |         // Service API | ||||||
| @@ -343,8 +371,6 @@ namespace RebootKit.Engine.Main { | |||||||
|  |  | ||||||
|                 TickCount++; |                 TickCount++; | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             World.Tick(deltaTime); |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         static void OnConnectionEvent(NetworkManager network, ConnectionEventData data) { |         static void OnConnectionEvent(NetworkManager network, ConnectionEventData data) { | ||||||
| @@ -356,6 +382,9 @@ namespace RebootKit.Engine.Main { | |||||||
|  |  | ||||||
|             GameInstance = Object.Instantiate(EngineConfig.gamePrefab); |             GameInstance = Object.Instantiate(EngineConfig.gamePrefab); | ||||||
|             GameInstance.NetworkObject.Spawn(); |             GameInstance.NetworkObject.Spawn(); | ||||||
|  |              | ||||||
|  |             NetworkSystemInstance = Object.Instantiate(s_networkSystemPrefab); | ||||||
|  |             NetworkSystemInstance.NetworkObject.Spawn(); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         static void OnServerStopped(bool obj) { |         static void OnServerStopped(bool obj) { | ||||||
| @@ -363,10 +392,13 @@ namespace RebootKit.Engine.Main { | |||||||
|  |  | ||||||
|             if (GameInstance is not null) { |             if (GameInstance is not null) { | ||||||
|                 GameInstance.NetworkObject.Despawn(); |                 GameInstance.NetworkObject.Despawn(); | ||||||
|                 Object.Destroy(GameInstance.gameObject); |                 GameInstance = null; | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             if (NetworkSystemInstance is not null) { | ||||||
|  |                 NetworkSystemInstance.NetworkObject.Despawn(); | ||||||
|  |                 NetworkSystemInstance = null; | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             GameInstance = null; |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // Console Commands |         // Console Commands | ||||||
|   | |||||||
| @@ -8,6 +8,8 @@ | |||||||
|         public const string k_EngineConfigResourcesPath = "TheGame/" + k_EngineConfigAssetName; |         public const string k_EngineConfigResourcesPath = "TheGame/" + k_EngineConfigAssetName; | ||||||
|         public const string k_EngineConfigAssetPath = "Assets/TheGame/" + k_EngineConfigAssetName + ".asset"; |         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_AddComponentMenu = "Reboot Reality/"; | ||||||
|          |          | ||||||
|         internal const string k_CreateAssetMenu = "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 UnityEngine; | ||||||
|  | using Logger = RebootKit.Engine.Foundation.Logger; | ||||||
|  |  | ||||||
| namespace RebootKit.Engine.Simulation { | 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<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 { |     public abstract class Actor : MonoBehaviour { | ||||||
|         bool m_IsPlaying = false; |         static readonly Logger s_ActorLogger = new Logger(nameof(Actor)); | ||||||
|         public bool IsPlaying { |  | ||||||
|             get { |         [field: SerializeField, TriInspector.ReadOnly] public string SourceActorPath { get; internal set; } = ""; | ||||||
|                 return m_IsPlaying; |         [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 (Manager is null) { | ||||||
|                 if (m_IsPlaying == value) { |                 s_ActorLogger.Error($"Cannot handle command because Manager is null for actor {name} (ID: {ActorID})"); | ||||||
|                     return; |                 return; | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 m_IsPlaying = value; |  | ||||||
|  |  | ||||||
|                 if (m_IsPlaying) { |  | ||||||
|                     OnBeginPlay(); |  | ||||||
|                 } else { |  | ||||||
|                     OnEndPlay(); |  | ||||||
|                 } |  | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  |             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<TCmdData>(ushort commandID, ref TCmdData commandData) | ||||||
|  |             where TCmdData : struct, ISerializableEntity { | ||||||
|  |             NativeArray<byte> data = DataSerializationUtils.Serialize(commandData); | ||||||
|  |             SendActorCommand(commandID, data); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         public virtual void OnBeginPlay() { |         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); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         public virtual void OnEndPlay() { |         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); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         public virtual void Tick(float deltaTime) { |         protected T DataAs<T>() where T : IActorData { | ||||||
|         } |             if (Data is T data) { | ||||||
|          |                 return data; | ||||||
|         public virtual void SerializeNetworkState(ref DataStreamWriter writer) { |             } | ||||||
|  |  | ||||||
|  |             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(); | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
							
								
								
									
										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 { |     public interface ISensor { | ||||||
|         GameObject Sense(); |         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.Foundation; | ||||||
| using RebootKit.Engine.Main; | using RebootKit.Engine.Main; | ||||||
| using RebootKit.Engine.Services.Simulation; | using RebootKit.Engine.Services.Simulation; | ||||||
|  | using Unity.Netcode; | ||||||
| using UnityEngine; | using UnityEngine; | ||||||
| using UnityEngine.AddressableAssets; | using UnityEngine.AddressableAssets; | ||||||
| using UnityEngine.Assertions; | using UnityEngine.Assertions; | ||||||
| @@ -27,14 +28,19 @@ namespace RebootKit.Engine.Simulation { | |||||||
|         WorldState m_WorldState = WorldState.Unloaded; |         WorldState m_WorldState = WorldState.Unloaded; | ||||||
|         WorldConfig m_Config; |         WorldConfig m_Config; | ||||||
|         AsyncOperationHandle<SceneInstance> m_SceneInstance; |         AsyncOperationHandle<SceneInstance> m_SceneInstance; | ||||||
|  |          | ||||||
|  |         enum ActorOrigin { | ||||||
|  |             InScene, | ||||||
|  |             Prefab | ||||||
|  |         } | ||||||
|  |  | ||||||
|         struct ActorData { |         struct ActorData { | ||||||
|  |             public ActorOrigin Origin; | ||||||
|             public Actor Actor; |             public Actor Actor; | ||||||
|             public readonly bool ManagedByAddressables; |  | ||||||
|              |              | ||||||
|             public ActorData(Actor actor, bool managedByAddressables) { |             public ActorData(ActorOrigin origin, Actor actor) { | ||||||
|  |                 Origin = origin; | ||||||
|                 Actor = actor; |                 Actor = actor; | ||||||
|                 ManagedByAddressables = managedByAddressables; |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -68,7 +74,9 @@ namespace RebootKit.Engine.Simulation { | |||||||
|  |  | ||||||
|             await m_SceneInstance.Result.ActivateAsync(); |             await m_SceneInstance.Result.ActivateAsync(); | ||||||
|             SceneManager.SetActiveScene(m_SceneInstance.Result.Scene); |             SceneManager.SetActiveScene(m_SceneInstance.Result.Scene); | ||||||
|  |              | ||||||
|  |             // await UniTask.WaitWhile(() => RR.CoreNetworkGameSystemsInstance is null, cancellationToken: cancellationToken); | ||||||
|  |              | ||||||
|             foreach (GameObject root in m_SceneInstance.Result.Scene.GetRootGameObjects()) { |             foreach (GameObject root in m_SceneInstance.Result.Scene.GetRootGameObjects()) { | ||||||
|                 if (root.TryGetComponent(out IWorldContext worldContext)) { |                 if (root.TryGetComponent(out IWorldContext worldContext)) { | ||||||
|                     Assert.IsNull(Context, |                     Assert.IsNull(Context, | ||||||
| @@ -77,15 +85,11 @@ namespace RebootKit.Engine.Simulation { | |||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 foreach (Actor actor in root.GetComponentsInChildren<Actor>()) { |                 foreach (Actor actor in root.GetComponentsInChildren<Actor>()) { | ||||||
|                     m_Actors.Add(new ActorData(actor, false)); |                     RR.NetworkSystemInstance.Actors.RegisterInSceneActor(actor); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             m_WorldState = WorldState.Loaded; |             m_WorldState = WorldState.Loaded; | ||||||
|  |  | ||||||
|             if (RR.GameInstance is not null) { |  | ||||||
|                 RR.GameInstance.OnWorldLoaded(); |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         public void Unload() { |         public void Unload() { | ||||||
| @@ -93,12 +97,10 @@ namespace RebootKit.Engine.Simulation { | |||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             if (RR.GameInstance is not null) { |             if (RR.NetworkSystemInstance is not null) { | ||||||
|                 RR.GameInstance.OnWorldUnload(); |                 RR.NetworkSystemInstance.Actors.CleanUp(); | ||||||
|             } |             } | ||||||
|              |              | ||||||
|             KillAllActors(); |  | ||||||
|  |  | ||||||
|             if (m_SceneInstance.IsValid()) { |             if (m_SceneInstance.IsValid()) { | ||||||
|                 m_SceneInstance.Release(); |                 m_SceneInstance.Release(); | ||||||
|                 m_SceneInstance = default; |                 m_SceneInstance = default; | ||||||
| @@ -107,79 +109,5 @@ namespace RebootKit.Engine.Simulation { | |||||||
|             m_WorldState = WorldState.Unloaded; |             m_WorldState = WorldState.Unloaded; | ||||||
|             Context = null; |             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 |   NetworkManagerExpanded: 0 | ||||||
|   NetworkConfig: |   NetworkConfig: | ||||||
|     ProtocolVersion: 0 |     ProtocolVersion: 0 | ||||||
|     NetworkTransport: {fileID: 1456272201} |     NetworkTransport: {fileID: 1456272198} | ||||||
|     PlayerPrefab: {fileID: 0} |     PlayerPrefab: {fileID: 0} | ||||||
|     Prefabs: |     Prefabs: | ||||||
|       NetworkPrefabsLists: |       NetworkPrefabsLists: | ||||||
| @@ -434,7 +434,7 @@ MonoBehaviour: | |||||||
|     TimeResyncInterval: 30 |     TimeResyncInterval: 30 | ||||||
|     EnsureNetworkVariableLengthSafety: 0 |     EnsureNetworkVariableLengthSafety: 0 | ||||||
|     EnableSceneManagement: 0 |     EnableSceneManagement: 0 | ||||||
|     ForceSamePrefabs: 1 |     ForceSamePrefabs: 0 | ||||||
|     RecycleNetworkIds: 1 |     RecycleNetworkIds: 1 | ||||||
|     NetworkIdRecycleDelay: 120 |     NetworkIdRecycleDelay: 120 | ||||||
|     RpcHashSize: 0 |     RpcHashSize: 0 | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user