private async Task ServerRunSpawnTaskAsync(SpawnConfig config, IProtoTrigger trigger, IServerZone zone) { // The spawning algorithm is inspired by random scattering approach described in the article // www.voidinspace.com/2013/06/procedural-generation-a-vegetation-scattering-tool-for-unity3d-part-i/ // but we use a quadtree instead of polygon for the spawn zone definition. if (zone.IsEmpty) { if (trigger != null) { Logger.Important($"Cannot spawn at {zone} - the zone is empty"); } else { // the spawn script is triggered from the editor system Logger.Dev($"Cannot spawn at {zone} - the zone is empty"); } return; } var isInitialSpawn = trigger == null || trigger is TriggerWorldInit; var yieldIfOutOfTime = isInitialSpawn ? () => Task.CompletedTask : (Func <Task>)Core.YieldIfOutOfTime; if (!isInitialSpawn) { // ensure that spawn execution is invoked in the end of frame await Core.AwaitEndOfFrame; } await yieldIfOutOfTime(); var stopwatchTotal = Stopwatch.StartNew(); var zonePositionsCount = zone.PositionsCount; // please note: this operation is super heavy as it collects spawn areas with objects // that's why it's made async var spawnZoneAreas = await ServerSpawnZoneAreasHelper.ServerGetCachedZoneAreaAsync(zone, yieldIfOutOfTime); await this.ServerFillSpawnAreasInZoneAsync(zone, spawnZoneAreas, yieldIfOutOfTime); var maxSpawnFailedAttemptsInRow = isInitialSpawn ? InitialSpawnMaxSpawnFailedAttemptsInRow : DefaultSpawnMaxSpawnFailedAttemptsInRow; maxSpawnFailedAttemptsInRow = (int)Math.Min(int.MaxValue, maxSpawnFailedAttemptsInRow * this.MaxSpawnAttemptsMultiplier); // calculate how many objects are already available var spawnedObjectsCount = spawnZoneAreas .Values .SelectMany(_ => _.WorldObjectsByPreset) .GroupBy(_ => _.Key) .ToDictionary(g => g.Key, g => g.Sum(list => list.Value.Count)); await yieldIfOutOfTime(); // calculate how many objects we need to spawn using var allSpawnRequestsTempList = Api.Shared.WrapInTempList( this.SpawnList .Where(preset => preset.Density > 0) .Select(preset => this.ServerCreateSpawnRequestForPreset( preset, config, zone, zonePositionsCount, spawnedObjectsCount))); var allSpawnRequests = allSpawnRequestsTempList.AsList(); var mobsTrackingManager = SpawnedMobsTrackingManagersStore.Get(this, zone); var physicsSpace = Server.World.GetPhysicsSpace(); var checkPlayersNearby = !this.CanSpawnIfPlayersNearby; // stage 1: global spawn using var activeSpawnRequestsList = Api.Shared.WrapInTempList( allSpawnRequests.Where(request => !request.UseSectorDensity && request.CountToSpawn > 0)); while (activeSpawnRequestsList.Count > 0) { await yieldIfOutOfTime(); var spawnPosition = zone.GetRandomPosition(Random); TrySpawn(spawnPosition, out _, out _); } stopwatchTotal.Stop(); var timeSpentSpawnGlobal = stopwatchTotal.Elapsed; await yieldIfOutOfTime(); // stage 2: sector spawn stopwatchTotal.Restart(); if (allSpawnRequests.Any(r => r.UseSectorDensity)) { maxSpawnFailedAttemptsInRow = DefaultAreaSpawnAttemptsCountPerPreset; if (isInitialSpawn) { maxSpawnFailedAttemptsInRow *= 16; } using var areasList = Api.Shared.WrapInTempList(spawnZoneAreas.Values); areasList.AsList().Shuffle(); foreach (var area in areasList.AsList()) { await yieldIfOutOfTime(); if (activeSpawnRequestsList.Count > 0) { activeSpawnRequestsList.Clear(); } foreach (var spawnRequest in allSpawnRequests) { if (!spawnRequest.UseSectorDensity) { continue; } if (IsAreaLocalDensityExceeded(spawnRequest, spawnRequest.Preset, area, out _)) { // already spawned too many objects of the required type in the area continue; } activeSpawnRequestsList.Add(spawnRequest); spawnRequest.FailedAttempts = 0; } if (activeSpawnRequestsList.Count == 0) { continue; } // make a few attempts to spawn in this area var attempts = activeSpawnRequestsList.Count * DefaultAreaSpawnAttemptsCountPerPreset; for (var attempt = 0; attempt < attempts; attempt++) { await yieldIfOutOfTime(); var spawnPosition = area.GetRandomPositionInside(zone, Random); TrySpawn(spawnPosition, out var spawnRequest, out var isSectorDensityExceeded); if (isSectorDensityExceeded) { // sector density exceeded for this spawn request activeSpawnRequestsList.Remove(spawnRequest); } if (activeSpawnRequestsList.Count == 0) { // everything for this sector has been spawned break; } } } } stopwatchTotal.Stop(); var timeSpentSpawnAreas = stopwatchTotal.Elapsed; var sb = new StringBuilder("Spawn script \"", capacity: 1024) .Append(this.ShortId) .Append("\" for zone \"") .Append(zone.ProtoGameObject.ShortId) .AppendLine("\": spawn completed. Stats:") .Append("Time spent (including wait time distributed across frames): ") .Append((timeSpentSpawnGlobal + timeSpentSpawnAreas).TotalMilliseconds.ToString("0.#")) .Append("ms (global: ") .Append(timeSpentSpawnGlobal.TotalMilliseconds.ToString("0.#")) .Append("ms, areas: ") .Append(timeSpentSpawnAreas.TotalMilliseconds.ToString("0.#")) .AppendLine("ms)") .AppendLine("Spawned objects:") .AppendLine( "[format: * preset: +spawnedNowCount, currentCount/desiredCount=ratio%, currentDensity%/requiredDensity%]"); foreach (var request in allSpawnRequests) { var currentDensity = request.CurrentCount / (double)zonePositionsCount; sb.Append("* ") .Append(request.UseSectorDensity ? "(sector) " : "(global) ") .Append(request.Preset.PrintEntries()) .AppendLine(":") .Append(" +") .Append(request.SpawnedCount) .Append(", ") .Append(request.CurrentCount) .Append('/') .Append(request.DesiredCount) .Append('=') // it's normal if we will have NaN if desired count is zero .Append((request.CurrentCount / (double)request.DesiredCount * 100d).ToString("0.##")) .Append("%, ") .Append((currentDensity * 100).ToString("0.###")) .Append("%/") .Append((request.Density * 100).ToString("0.###")) .AppendLine("%"); } Logger.Important(sb); void TrySpawn( Vector2Ushort spawnPosition, out SpawnRequest spawnRequest, out bool isSectorDensityExceeded) { spawnRequest = activeSpawnRequestsList.AsList().TakeByRandom(Random); var spawnProtoObject = spawnRequest.Preset.GetRandomObjectProto(); IGameObjectWithProto spawnedObject = null; if (ServerIsCanSpawn( spawnRequest, spawnRequest.Preset, spawnZoneAreas, spawnPosition, physicsSpace, out var spawnZoneArea, out isSectorDensityExceeded)) { if (!checkPlayersNearby || !ServerIsAnyPlayerNearby(spawnPosition)) { spawnedObject = this.ServerSpawn(trigger, zone, spawnProtoObject, spawnPosition); } } if (spawnedObject == null) { // cannot spawn if (++spawnRequest.FailedAttempts >= maxSpawnFailedAttemptsInRow) { // too many attempts failed - stop spawning this preset activeSpawnRequestsList.Remove(spawnRequest); } return; } if (this.hasServerOnObjectSpawnedMethodOverride) { try { this.ServerOnObjectSpawned(spawnedObject); } catch (Exception ex) { Logger.Exception(ex); } } // spawned successfully // register object in zone area if (spawnZoneArea == null) { throw new Exception("Should be impossible"); } spawnZoneArea.Add(spawnRequest.Preset, spawnPosition); if (spawnProtoObject is IProtoCharacterMob) { mobsTrackingManager.Add((ICharacter)spawnedObject); } spawnRequest.OnSpawn(); spawnRequest.FailedAttempts = 0; if (spawnRequest.CountToSpawn == 0) { activeSpawnRequestsList.Remove(spawnRequest); } } }
private async Task ServerRunSpawnTaskAsync(SpawnConfig config, IProtoTrigger trigger, IServerZone zone) { // The spawning algorithm is inspired by random scattering approach described in the article // www.voidinspace.com/2013/06/procedural-generation-a-vegetation-scattering-tool-for-unity3d-part-i/ // but we use a quadtree instead of polygon for the spawn zone definition. if (zone.IsEmpty) { if (trigger != null) { Logger.Important($"Cannot spawn at {zone} - the zone is empty"); } else { // the spawn script is triggered from the editor system Logger.Dev($"Cannot spawn at {zone} - the zone is empty"); } return; } var isInitialSpawn = trigger == null || trigger is TriggerWorldInit; var yieldIfOutOfTime = isInitialSpawn ? () => Task.CompletedTask : (Func <Task>)Core.YieldIfOutOfTime; await yieldIfOutOfTime(); var stopwatchTotal = Stopwatch.StartNew(); var zonePositionsCount = zone.PositionsCount; // please note: this operation is super heavy as it collects spawn areas with objects // that's why it's made async var spawnZoneAreas = await ServerSpawnZoneAreasHelper.ServerGetCachedZoneAreaAsync(zone, yieldIfOutOfTime); await this.ServerFillSpawnAreasInZoneAsync(zone, spawnZoneAreas, yieldIfOutOfTime); var maxSpawnFailedAttemptsInRow = isInitialSpawn ? InitialSpawnMaxSpawnFailedAttemptsInRow : DefaultSpawnMaxSpawnFailedAttemptsInRow; maxSpawnFailedAttemptsInRow = (int)(maxSpawnFailedAttemptsInRow * this.MaxSpawnAttempsMultiplier); // calculate how many objects are already available var spawnedObjectsCount = spawnZoneAreas .Values .SelectMany(_ => _.WorldObjectsByPreset) .GroupBy(_ => _.Key) .ToDictionary(g => g.Key, g => g.Sum(list => list.Value.Count)); await yieldIfOutOfTime(); var densityMultiplier = config.DensityMultiplier; // calculate how many objects we need to spawn var allSpawnRequests = this.SpawnList .Where(preset => preset.Density > 0) .Select( preset => { var density = preset.Density * densityMultiplier; var desiredCount = (int)(density * zonePositionsCount); var currentCount = spawnedObjectsCount.Find(preset); //if (isInitialSpawn) //{ var countToSpawn = Math.Max(0, desiredCount - currentCount); //} // TODO: refactor this to be actually useful with local density //else // if this is an iteration spawn request //{ // // limit count to spawn to match the iteration limit // var fractionSpawned = Math.Min(currentCount / (double)desiredCount, 1.0); // var fractionRange = preset.IterationLimitFractionRange; // // countToSpawn = (int)Math.Ceiling // (desiredCount * fractionRange.GetByFraction(1 - fractionSpawned)); //} // we're not using this feature NoiseSelector tileRandomSelector = null; //this.CreateTileRandomSelector(density, preset, desiredCount); var useSectorDensity = preset.PresetUseSectorDensity && ((density * SpawnZoneAreaSize * SpawnZoneAreaSize) >= SectorDensityThreshold); return(new SpawnRequest(preset, desiredCount, currentCount, countToSpawn, density, tileRandomSelector, useSectorDensity)); }) .ToList(); var mobsTrackingManager = SpawnedMobsTrackingManagersStore.Get(this, zone); var playersPositions = Server .Characters.EnumerateAllPlayerCharacters(onlyOnline: true, exceptSpectators: true) .Select(p => p.TilePosition) .ToList(); var physicsSpace = Server.World.GetPhysicsSpace(); // stage 1: global spawn var activeSpawnRequestsList = allSpawnRequests.Where(request => !request.UseSectorDensity && request.CountToSpawn > 0) .ToList(); while (activeSpawnRequestsList.Count > 0) { await yieldIfOutOfTime(); var spawnPosition = zone.GetRandomPosition(Random); TrySpawn(spawnPosition, checkPlayersNearby: true, out _, out _); } stopwatchTotal.Stop(); var timeSpentSpawnGlobal = stopwatchTotal.Elapsed; await yieldIfOutOfTime(); // stage 2: sector spawn stopwatchTotal.Restart(); if (allSpawnRequests.Any(r => r.UseSectorDensity)) { maxSpawnFailedAttemptsInRow = AreaSpawnAttempsCountPerPreset; if (isInitialSpawn) { maxSpawnFailedAttemptsInRow *= 16; } var areasList = spawnZoneAreas.Values.ToList(); areasList.Shuffle(); foreach (var area in areasList) { await yieldIfOutOfTime(); if (activeSpawnRequestsList.Count > 0) { activeSpawnRequestsList.Clear(); } foreach (var spawnRequest in allSpawnRequests) { if (!spawnRequest.UseSectorDensity) { continue; } if (IsAreaLocalDensityExceeded(spawnRequest, spawnRequest.Preset, area, out _)) { // already spawned too many objects of the required type in the area continue; } activeSpawnRequestsList.Add(spawnRequest); spawnRequest.FailedAttempts = 0; } if (activeSpawnRequestsList.Count == 0) { continue; } if (ServerIsAnyPlayerNearby(area, playersPositions)) { continue; } // make a few attempts to spawn in this area var attempts = activeSpawnRequestsList.Count * AreaSpawnAttempsCountPerPreset; for (var attempt = 0; attempt < attempts; attempt++) { await yieldIfOutOfTime(); var spawnPosition = area.GetRandomPositionInside(zone, Random); TrySpawn(spawnPosition, checkPlayersNearby: false, out var spawnRequest, out var isSectorDensityExceeded); if (isSectorDensityExceeded) { // sector density exceeded for this spawn request activeSpawnRequestsList.Remove(spawnRequest); } if (activeSpawnRequestsList.Count == 0) { // everything for this sector has been spawned break; } } } } stopwatchTotal.Stop(); var timeSpentSpawnAreas = stopwatchTotal.Elapsed; var sb = new StringBuilder("Spawn script \"", capacity: 1024) .Append(this.ShortId) .Append("\" for zone \"") .Append(zone.ProtoGameObject.ShortId) .AppendLine("\": spawn completed. Stats:") .Append("Time spent (including wait time distributed across frames): ") .Append((timeSpentSpawnGlobal + timeSpentSpawnAreas).TotalMilliseconds.ToString("0.#")) .Append("ms (global: ") .Append(timeSpentSpawnGlobal.TotalMilliseconds.ToString("0.#")) .Append("ms, areas: ") .Append(timeSpentSpawnAreas.TotalMilliseconds.ToString("0.#")) .AppendLine("ms)") .AppendLine("Spawned objects:") .AppendLine( "[format: * preset: +spawnedNowCount, currentCount/desiredCount=ratio%, currentDensity%/requiredDensity%]"); foreach (var request in allSpawnRequests) { var currentDensity = request.CurrentCount / (double)zonePositionsCount; sb.Append("* ") .Append(request.UseSectorDensity ? "(sector) " : ("(global) ")) .Append(request.Preset.PrintEntries()) .AppendLine(":") .Append(" +") .Append(request.SpawnedCount) .Append(", ") .Append(request.CurrentCount) .Append('/') .Append(request.DesiredCount) .Append('=') // it's normal if we will have NaN if desired count is zero .Append((request.CurrentCount / (double)request.DesiredCount * 100d).ToString("0.##")) .Append("%, ") .Append((currentDensity * 100).ToString("0.###")) .Append("%/") .Append((request.Density * 100).ToString("0.###")) .AppendLine("%"); } Logger.Important(sb); void TrySpawn( Vector2Ushort spawnPosition, bool checkPlayersNearby, out SpawnRequest spawnRequest, out bool isSectorDensityExceeded) { spawnRequest = activeSpawnRequestsList.TakeByRandom(Random); if (spawnRequest.TileRandomSelector != null && !spawnRequest.TileRandomSelector .IsMatch(spawnPosition.X, spawnPosition.Y, rangeMultiplier: 1)) { // the spawn position doesn't satisfy the the random selector isSectorDensityExceeded = false; return; } var spawnProtoObject = spawnRequest.Preset.GetRandomObjectProto(); IGameObjectWithProto spawnedObject = null; if (ServerIsCanSpawn( spawnRequest, spawnRequest.Preset, spawnZoneAreas, spawnPosition, physicsSpace, out var spawnZoneArea, out isSectorDensityExceeded)) { if (!checkPlayersNearby || !ServerIsAnyPlayerNearby(spawnPosition, playersPositions)) { spawnedObject = this.ServerSpawn(trigger, zone, spawnProtoObject, spawnPosition); } } if (spawnedObject == null) { // cannot spawn if (++spawnRequest.FailedAttempts >= maxSpawnFailedAttemptsInRow) { // too many attempts failed - stop spawning this preset activeSpawnRequestsList.Remove(spawnRequest); } return; } // spawned successfully // register object in zone area if (spawnZoneArea == null) { throw new Exception("Should be impossible"); } spawnZoneArea.Add(spawnRequest.Preset, spawnPosition); if (spawnProtoObject is IProtoCharacterMob) { mobsTrackingManager.Add((ICharacter)spawnedObject); } spawnRequest.OnSpawn(); spawnRequest.FailedAttempts = 0; if (spawnRequest.CountToSpawn == 0) { activeSpawnRequestsList.Remove(spawnRequest); } } }