using Unity.Collections; using Unity.Entities; using Unity.NetCode; public enum LevelSyncState { Idle, LevelLoadRequest, LevelLoadInProgress, LevelLoaded } public struct ClientLoadLevel : IRpcCommand { public int LevelIndex; } public struct ClientReady : IRpcCommand { public int LevelIndex; } // Add to network connections when level load sync starts, clients without this when loading is done are new connections public struct LevelLoadingInProgress : IComponentData { } // Add when connection/client is done loading so in progress count should equal done count when server can start public struct LevelLoadingDone : IComponentData { } public struct LevelSyncStateComponent : IComponentData { public LevelSyncState State; public int CurrentLevel; // When client state is LevelLoadInProgress this level should be loaded public int NextLevel; } [WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation | WorldSystemFilterFlags.ThinClientSimulation)] [CreateBefore(typeof(LevelLoader))] public partial class NetcodeClientLevelSync : SystemBase { protected override void OnCreate() { RequireForUpdate(); EntityManager.CreateEntity(typeof(LevelSyncStateComponent)); } protected override void OnUpdate() { var connectionEntity = SystemAPI.GetSingletonEntity(); var levelState = SystemAPI.GetSingleton(); if (!SystemAPI.QueryBuilder().WithAll().Build().IsEmptyIgnoreFilter) { FixedString64Bytes worldName = World.Name; var ecb = new EntityCommandBuffer(Allocator.Temp); // When load level command arrives, disable ghost sync, unload current level and load specified level foreach (var (level, entity) in SystemAPI.Query>().WithEntityAccess() .WithAll()) { UnityEngine.Debug.Log($"[{worldName}] received command to load {level.ValueRO.LevelIndex}"); ecb.RemoveComponent(connectionEntity); levelState.State = LevelSyncState.LevelLoadRequest; levelState.NextLevel = level.ValueRO.LevelIndex; SystemAPI.SetSingleton(levelState); ecb.DestroyEntity(entity); } ecb.Playback(EntityManager); } if (levelState.State == LevelSyncState.LevelLoaded) { if (!EntityManager.HasComponent(connectionEntity)) { var netId = SystemAPI.GetSingleton(); UnityEngine.Debug.Log($"{World.Name} enable sync on connection {netId.Value}"); EntityManager.AddComponent(connectionEntity); } UnityEngine.Debug.Log($"[{World.Name}] notifying server it's finished loading {levelState.CurrentLevel}"); var rpcCmd = EntityManager.CreateEntity(typeof(ClientReady), typeof(SendRpcCommandRequest)); EntityManager.AddComponentData(rpcCmd, new ClientReady(){LevelIndex = levelState.CurrentLevel}); levelState.State = LevelSyncState.Idle; SystemAPI.SetSingleton(levelState); } } } [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)] [CreateBefore(typeof(LevelLoader))] public partial class NetcodeServerLevelSync : SystemBase { private EntityQuery m_ClientsReadyQuery; private EntityQuery m_ClientsLoadingQuery; protected override void OnCreate() { m_ClientsReadyQuery = EntityManager.CreateEntityQuery(ComponentType.ReadOnly()); m_ClientsLoadingQuery = EntityManager.CreateEntityQuery(ComponentType.ReadOnly()); RequireForUpdate(); EntityManager.CreateEntity(typeof(LevelSyncStateComponent)); } protected override void OnUpdate() { var ecb = new EntityCommandBuffer(Allocator.Temp); var connections = GetComponentLookup(); var loadingInProgress = GetComponentLookup(); // TODO: Level number not being used for anything atm Entities.ForEach((Entity entity, in ClientReady level, in ReceiveRpcCommandRequest req) => { UnityEngine.Debug.Log($"Client {connections[req.SourceConnection].Value} finished loading {level.LevelIndex}."); ecb.AddComponent(req.SourceConnection); if (!loadingInProgress.HasComponent(req.SourceConnection)) UnityEngine.Debug.LogError("Ready client was never marked as starting level loading"); ecb.DestroyEntity(entity); }).Run(); ecb.Playback(EntityManager); var readyCount = m_ClientsReadyQuery.CalculateEntityCount(); var loadingCount = m_ClientsLoadingQuery.CalculateEntityCount(); // All scenes finished loading and clients are ready var levelState = SystemAPI.GetSingleton(); if (levelState.State == LevelSyncState.LevelLoaded && loadingCount == readyCount) { UnityEngine.Debug.Log("Server subscenes finished loading and all clients are ready"); var conQuery = EntityManager.CreateEntityQuery(ComponentType.ReadOnly()); var cons = conQuery.ToEntityArray(Allocator.Temp); var conIds = conQuery.ToComponentDataArray(Allocator.Temp); for (int i = 0; i < cons.Length; ++i) { if (!EntityManager.HasComponent(cons[i])) { UnityEngine.Debug.Log($"[{World.Name}] Enable sync on {conIds[i].Value}"); EntityManager.AddComponent(cons[i]); } } levelState.State = LevelSyncState.Idle; SystemAPI.SetSingleton(levelState); ecb = new EntityCommandBuffer(Allocator.Temp); FixedString64Bytes world = World.Name; Entities.WithAll().ForEach((Entity entity) => { ecb.RemoveComponent(entity); ecb.RemoveComponent(entity); }).Run(); ecb.Playback(EntityManager); } } } public static class NetcodeLevelSync { public static void TriggerClientLoadLevel(int level, World serverWorld) { // Trigger level load on all clients var rpcCmd = serverWorld.EntityManager.CreateEntity(); serverWorld.EntityManager.AddComponentData(rpcCmd, new ClientLoadLevel(){LevelIndex = level}); serverWorld.EntityManager.AddComponent(rpcCmd); // Mark each connection as being in progress of loading var connectionsQuery = serverWorld.EntityManager.CreateEntityQuery(ComponentType.ReadOnly()); var connectionEntities = connectionsQuery.ToEntityArray(Allocator.Temp); foreach (var connection in connectionEntities) { serverWorld.EntityManager.AddComponentData(connection, new LevelLoadingInProgress()); } } public static void SetLevelState(LevelSyncState state, World world) { var levelStateQuery = world.EntityManager.CreateEntityQuery(ComponentType.ReadWrite()); var levelStateEntity = levelStateQuery.ToEntityArray(Allocator.Temp); var levelStateData = levelStateQuery.ToComponentDataArray(Allocator.Temp); var levelState = levelStateData[0]; levelState.State = state; world.EntityManager.SetComponentData(levelStateEntity[0], levelState); } }