using System; using Gameplay; using JetBrains.Annotations; using Unity.Entities; using Unity.NetCode; using Unity.Collections; using Unity.MegaCity.Gameplay; using Unity.Transforms; using Unity.Mathematics; using Unity.Burst; using Unity.MegaCity.UI; using Unity.Networking.Transport; using UnityEngine; using static Unity.Entities.SystemAPI; using Unity.Jobs; using Unity.Cn.Multiverse; namespace Unity.MegaCity.Traffic { /// /// The bootstrap needs to extend `ClientServerBootstrap`, there can only be one class extending it in the project /// [UnityEngine.Scripting.Preserve] public class NetCodeBootstrap : ClientServerBootstrap { public static NetworkEndpoint MegaCityServerIp => NetworkEndpoint.Parse("128.14.159.58", 7979); /// /// Limitation imposed to ensure UTP send/receiveQueueSize's are set appropriately. /// . /// public const int MaxPlayerCount = 200; // The initialize method is what entities calls to create the default worlds public override bool Initialize(string defaultWorldName) { // Handle max player count globally. NetworkStreamReceiveSystem.DriverConstructor = new MegaCityDriverConstructor(); #if UNITY_SERVER && !UNITY_EDITOR UnityEngine.Application.targetFrameRate = 60; CreateDefaultClientServerWorlds(); // Start Multiverse through the wrapper. MultiverseSDKWrapper.Instance.StartMultiverse(); // Niki.Walker: Disabled as UNITY_SERVER does not support creating thin client worlds. // // On the server, also create thin clients (if requested). // TryCreateThinClientsIfRequested(); return true; #else // Try and auto-connect. #if UNITY_EDITOR if (RequestedPlayType == PlayType.Client) { if (MultiplayerPlayModePreferences.IsEditorInputtedAddressValidForConnect(out var editorSpecifiedEndpoint)) { AutoConnectPort = editorSpecifiedEndpoint.Port; DefaultConnectAddress = editorSpecifiedEndpoint; UnityEngine.Debug.Log($"Detected auto-connection preference in 'Multiplayer PlayMode Tool' targeting '{editorSpecifiedEndpoint}' (Port: '{AutoConnectPort}')!"); } } #else // We always set the DefaultConnectAddress in a player, because it's unlikely you'll want to test locally here. DefaultConnectAddress = ModeBootstrap.Options.UserSpecifiedEndpoint; if (TryCreateThinClientsIfRequested()) return true; #endif // Netcode worlds are always created, regardless. CreateDefaultClientServerWorlds(); return true; #endif } [UsedImplicitly] private static bool TryCreateThinClientsIfRequested() { if (ModeBootstrap.Options.IsThinClient) { var requestedNumThinClients = ModeBootstrap.Options.TargetThinClientWorldCount; if (requestedNumThinClients > 0) { // Hardcoded DefaultConnectAddress for the MegaCity demo. AutoConnectPort = ModeBootstrap.Options.UserSpecifiedEndpoint.Port; for (var i = 0; i < requestedNumThinClients; i++) { Debug.Log($"Creating a Thin Client World! {(i + 1)} of {requestedNumThinClients}..."); var world = CreateThinClientWorld(); if (i == 0 || World.DefaultGameObjectInjectionWorld == null || !World.DefaultGameObjectInjectionWorld.IsCreated) { World.DefaultGameObjectInjectionWorld = world; Debug.Log($"Setting DefaultGameObjectInjectionWorld to world '{world.Name}'."); } } Debug.Log($"Detected headless client! Automatically creating {requestedNumThinClients} ThinClients, and connecting them to the hardcoded endpoint '{DefaultConnectAddress}' (Port: '{AutoConnectPort}')!"); return true; } Debug.LogError($"Detected headless client, but TargetThinClientWorldCount is {requestedNumThinClients}! Cannot initialize!"); } return false; } } [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)] [UpdateInGroup(typeof(InitializationSystemGroup))] [CreateAfter(typeof(NetworkStreamReceiveSystem))] public partial struct ServerInGame : ISystem { #region Jobs [BurstCompile] private partial struct GetPositionJob : IJob { public NativeArray SpawnPoints; public NativeList UsedPositions; public Mathematics.Random Random; [BurstCompile] public void Execute() { var availablePositions = CreateAvailablePositions(); // If all player positions have been used, reset the list if (availablePositions.Length == 0) { UsedPositions.Clear(); availablePositions = CreateAvailablePositions(); } // Choose a random position from the list of available player names var randomIndex = Random.NextInt(0, availablePositions.Length); var position = availablePositions[randomIndex]; UsedPositions.Add(position); } private NativeList CreateAvailablePositions() { var availablePositions = new NativeList(Allocator.TempJob); // Get a list of spawnPoints that have not been used foreach (var position in SpawnPoints) { if (!UsedPositions.Contains(position.Value)) { availablePositions.Add(position.Value); } } return availablePositions; } } [BurstCompile] partial struct UpdateConnectionPositionSystemJob : IJobEntity { [ReadOnly] public ComponentLookup transformLookup; public void Execute(ref GhostConnectionPosition conPos, in CommandTarget target) { if (!transformLookup.HasComponent(target.targetEntity)) return; conPos = new GhostConnectionPosition { Position = transformLookup[target.targetEntity].Position }; } } #endregion private NativeList m_UsedPositions; private Mathematics.Random m_Random; private bool m_IsInitialized; public void OnCreate(ref SystemState state) { m_IsInitialized = false; state.RequireForUpdate(); state.RequireForUpdate(); m_UsedPositions = new NativeList(Allocator.Persistent); var currentTime = DateTime.Now; var seed = currentTime.Minute + currentTime.Second + currentTime.Millisecond + 1; m_Random = new Mathematics.Random((uint)seed); GetSingletonRW().ValueRW.Listen(NetworkEndpoint.AnyIpv4.WithPort(ModeBootstrap.Options.UserSpecifiedEndpoint.Port)); const int tileSize = 256; var grid = state.EntityManager.CreateEntity(); state.EntityManager.SetName(grid, "GhostImportanceSingleton"); state.EntityManager.AddComponentData(grid, new GhostDistanceData { TileSize = new int3(tileSize, 1024 * 8, tileSize), TileCenter = new int3(0, 0, 0), TileBorderWidth = new float3(5f), }); state.EntityManager.AddComponentData(grid, new GhostImportance { ScaleImportanceFunction = GhostDistanceImportance.ScaleFunctionPointer, GhostConnectionComponentType = ComponentType.ReadOnly(), GhostImportanceDataType = ComponentType.ReadOnly(), GhostImportancePerChunkDataType = ComponentType.ReadOnly(), }); } public void OnUpdate(ref SystemState state) { #if UNITY_SERVER && !UNITY_EDITOR if(!m_IsInitialized) { m_IsInitialized = true; MultiverseSDKWrapper.Instance.ReadyMultiverse(); } #endif var spawnBuffer = GetSingletonBuffer(); var prefab = GetSingleton().Player; var cmdBuffer = new EntityCommandBuffer(Allocator.Temp); var originalTrans = state.EntityManager.GetComponentData(prefab); var health = state.EntityManager.GetComponentData(prefab); state.EntityManager.GetName(prefab, out var prefabName); foreach (var (netId, entity) in Query>().WithNone() .WithEntityAccess()) { var findNewPosition = new GetPositionJob { SpawnPoints = spawnBuffer.ToNativeArray(Allocator.TempJob), UsedPositions = m_UsedPositions, Random = m_Random }; state.Dependency = findNewPosition.Schedule(state.Dependency); state.Dependency.Complete(); cmdBuffer.AddComponent(entity); var player = cmdBuffer.Instantiate(prefab); var networkIdValue = netId.ValueRO.Value; cmdBuffer.SetComponent(player, new GhostOwner { NetworkId = networkIdValue }); var newTrans = originalTrans; newTrans.Position = m_UsedPositions[m_UsedPositions.Length - 1]; cmdBuffer.SetComponent(player, newTrans); cmdBuffer.AppendToBuffer(entity, new LinkedEntityGroup { Value = player }); cmdBuffer.SetComponent(player, health); cmdBuffer.AddComponent(entity); cmdBuffer.SetComponent(entity, new CommandTarget { targetEntity = player }); } cmdBuffer.Playback(state.EntityManager); var updateJob = new UpdateConnectionPositionSystemJob { transformLookup = GetComponentLookup(true) }; state.Dependency = updateJob.ScheduleParallel(state.Dependency); } } [WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation | WorldSystemFilterFlags.ThinClientSimulation)] [UpdateInGroup(typeof(InitializationSystemGroup))] public partial struct ClientInGame : ISystem { private bool m_HasRegisteredSmoothingAction; public void OnCreate(ref SystemState state) { state.RequireForUpdate(); var tickRate = NetworkTimeSystem.DefaultClientTickRate; tickRate.MaxExtrapolationTimeSimTicks = 120; tickRate.InterpolationTimeMS = 150; state.EntityManager.CreateSingleton(tickRate); // Niki.Walker: Client-side optimizations: var ghostSendSystemData = new GhostSendSystemData { MinSendImportance = 2 }; // Don't frequently resend the same bot vehicles. //ghostSendSystemData.FirstSendImportanceMultiplier = 100; // Significantly bias towards sending new ghosts. // Disabled as it ruins the start of the game. state.EntityManager.CreateSingleton(ghostSendSystemData); } public void OnUpdate(ref SystemState state) { if (!m_HasRegisteredSmoothingAction && TryGetSingletonRW(out var ghostPredictionSmoothing)) { m_HasRegisteredSmoothingAction = true; ghostPredictionSmoothing.ValueRW.RegisterSmoothingAction(state.EntityManager, MegaCitySmoothingAction.Action); } var cmdBuffer = new EntityCommandBuffer(Allocator.Temp); foreach (var (netId, entity) in Query>().WithNone() .WithEntityAccess()) { cmdBuffer.AddComponent(entity); } cmdBuffer.Playback(state.EntityManager); } } }