public SpawnRequest( ObjectSpawnPreset preset, int desiredCount, int currentCount, int spawnCount, double density, NoiseSelector tileRandomSelector, bool useSectorDensity) { this.Preset = preset; this.DesiredCount = desiredCount; this.CurrentCount = currentCount; this.CountToSpawn = spawnCount; this.Density = density; this.TileRandomSelector = tileRandomSelector; this.UseSectorDensity = useSectorDensity; }
protected override void PrepareProtoTile(Settings settings) { settings.AmbientSoundProvider = new TileAmbientSoundProvider( new AmbientSoundPreset(new SoundResource("Ambient/Pit"))); settings.AddGroundTexture( new ProtoTileGroundTexture( texture: GroundTexture1, blendMaskTexture: BlendMaskTextureGeneric2Smooth, noiseSelector: null)); // add clay stones decals var clayStonesTextures = ProtoTileDecal.CollectTextures("Terrain/Clay/ClayStones*"); var clayStonesSize = new Vector2Ushort(2, 2); var clayStonesNoiseSelector = new NoiseSelector( from: 0.5, to: 0.55, noise: new PerlinNoise(seed: 642571234, scale: 5, octaves: 4, persistance: 0.9, lacunarity: 3.9)); for (ushort x = 0; x <= 1; x++) { for (ushort y = 0; y <= 1; y++) { settings.AddDecal( new ProtoTileDecal(clayStonesTextures, size: clayStonesSize, offset: (x, y), noiseSelector: clayStonesNoiseSelector)); } } // add clay pit decals settings.AddDecal( new ProtoTileDecal("Terrain/Clay/ClayPit*", size: (2, 2), drawOrder: DrawOrder.GroundDecalsUnder, noiseSelector: new NoiseSelector( from: 0.95, to: 1, noise: new WhiteNoise(seed: 306721460)))); }
protected override void PrepareProtoTile(Settings settings) { settings.AmbientSoundProvider = new TileForestAmbientSoundProvider( daySoundPresetPlains: new AmbientSoundPreset(new SoundResource("Ambient/BorealPlains")), daySoundPresetForest: new AmbientSoundPreset(new SoundResource("Ambient/BorealForest")), nightSoundPresetPlains: new AmbientSoundPreset(new SoundResource("Ambient/BorealPlainsNight")), nightSoundPresetForest: new AmbientSoundPreset(new SoundResource("Ambient/BorealForestNight"))); var groundTextureForest3 = new ProtoTileGroundTexture( texture: GroundTextureAtlas3, blendMaskTexture: BlendMaskTextureSprayRough, noiseSelector: null); var groundTextureForest2 = new ProtoTileGroundTexture( texture: GroundTexture2, blendMaskTexture: BlendMaskTextureSprayRough, noiseSelector: new NoiseSelector(from: 0.5, to: 1, noise: new PerlinNoise(seed: 392721487, scale: 15, octaves: 5, persistance: 0.6, lacunarity: 1.4))); var groundTextureForest1 = new ProtoTileGroundTexture( texture: GroundTexture1, blendMaskTexture: BlendMaskTextureSprayRough, noiseSelector: new NoiseSelector(from: 0.4, to: 0.65, noise: new PerlinNoise(seed: 498651212, scale: 23, octaves: 5, persistance: 0.5, lacunarity: 1.4))); settings.AddGroundTexture(groundTextureForest3); settings.AddGroundTexture(groundTextureForest2); settings.AddGroundTexture(groundTextureForest1); // add smallBushes decals var smallBushesTextures = ProtoTileDecal.CollectTextures("Terrain/ForestBoreal/SmallBushes*"); var smallBushesDrawOrder = DrawOrder.GroundDecalsUnder; var smallBushesSize = new Vector2Ushort(2, 2); var smallBushesNoiseSelector = new CombinedNoiseSelector( new NoiseSelector( from: 0.8, to: 0.97, noise: new PerlinNoise(seed: 209123674, scale: 10, octaves: 3, persistance: 0.7, lacunarity: 1.5)), new NoiseSelector( from: 0.85, to: 1, noise: new PerlinNoise(seed: 129308572, scale: 11, octaves: 3, persistance: 0.7, lacunarity: 3))); // add smallBushes decals with a four different offsets (to avoid empty space between neighbor smallBushes sprites) for (ushort x = 0; x <= 1; x++) { for (ushort y = 0; y <= 1; y++) { settings.AddDecal( new ProtoTileDecal(smallBushesTextures, size: smallBushesSize, offset: (x, y), drawOrder: smallBushesDrawOrder, canFlipHorizontally: false, noiseSelector: smallBushesNoiseSelector)); } } // some random small bushes settings.AddDecal( new ProtoTileDecal(smallBushesTextures, size: smallBushesSize, drawOrder: smallBushesDrawOrder, canFlipHorizontally: false, noiseSelector: new NoiseSelector( from: 0.85, to: 1, noise: new WhiteNoise(seed: 643613342)))); // add WhiteFlowers decals var whiteFlowersTextures = ProtoTileDecal.CollectTextures("Terrain/ForestBoreal/WhiteFlowers*"); var whiteFlowersSize = new Vector2Ushort(2, 2); var whiteFlowersNoiseSelector = new NoiseSelector( from: 0.6, to: 0.65, noise: new PerlinNoise(seed: 25343634, scale: 8, octaves: 3, persistance: 0.6, lacunarity: 1.7)); settings.AddDecal( new ProtoTileDecal(whiteFlowersTextures, size: whiteFlowersSize, noiseSelector: whiteFlowersNoiseSelector, requiredGroundTextures: new[] { groundTextureForest1, groundTextureForest3 })); // add the same whiteFlowers but with a little offset (to make a more dense diagonal placement) settings.AddDecal( new ProtoTileDecal(whiteFlowersTextures, size: whiteFlowersSize, offset: Vector2Ushort.One, noiseSelector: whiteFlowersNoiseSelector, requiredGroundTextures: new[] { groundTextureForest1, groundTextureForest3 })); // add VioletFlowers decals var violetFlowersTextures = ProtoTileDecal.CollectTextures("Terrain/ForestBoreal/VioletFlowers*"); var violetFlowersSize = new Vector2Ushort(2, 2); var violetFlowersNoiseSelector = new NoiseSelector( from: 0.55, to: 0.65, noise: new PerlinNoise(seed: 245751427, scale: 5, octaves: 3, persistance: 0.6, lacunarity: 1.7)); settings.AddDecal( new ProtoTileDecal(violetFlowersTextures, size: violetFlowersSize, noiseSelector: violetFlowersNoiseSelector)); // add the same violetFlowers but with a little offset (to make a more dense diagonal placement) settings.AddDecal( new ProtoTileDecal(violetFlowersTextures, size: violetFlowersSize, offset: Vector2Ushort.One, noiseSelector: violetFlowersNoiseSelector)); // add clover bush decal settings.AddDecal( new ProtoTileDecal("Terrain/ForestBoreal/CloverBush01*", size: Vector2Ushort.One, drawOrder: DrawOrder.GroundDecalsOver, hidingSetting: DecalHidingSetting.AnyObject, noiseSelector: new NoiseSelector( from: 0.985, to: 1, noise: new WhiteNoise(seed: 982356342)))); // add fungi decal settings.AddDecal( new ProtoTileDecal("Terrain/ForestBoreal/Fungi01*", size: Vector2Ushort.One, drawOrder: DrawOrder.GroundDecalsOver, hidingSetting: DecalHidingSetting.AnyObject, noiseSelector: new NoiseSelector( from: 0.985, to: 1, noise: new WhiteNoise(seed: 474631892)))); }
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)Math.Min(int.MaxValue, 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(); // calculate how many objects we need to spawn using var allSpawnRequests = Api.Shared.WrapInTempList( this.SpawnList .Where(preset => preset.Density > 0) .Select( preset => { var density = preset.Density * config.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)); })); var mobsTrackingManager = SpawnedMobsTrackingManagersStore.Get(this, zone); using var tempPlayersPositions = Api.Shared.WrapInTempList( Server.Characters.EnumerateAllPlayerCharacters( onlyOnline: true, exceptSpectators: true) .Select(p => p.TilePosition)); var playersPositions = tempPlayersPositions.AsList(); var physicsSpace = Server.World.GetPhysicsSpace(); // 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, checkPlayersNearby: !this.CanSpawnIfPlayersNearby, out _, out _); } stopwatchTotal.Stop(); var timeSpentSpawnGlobal = stopwatchTotal.Elapsed; await yieldIfOutOfTime(); // stage 2: sector spawn stopwatchTotal.Restart(); if (allSpawnRequests.Any(r => r.UseSectorDensity)) { maxSpawnFailedAttemptsInRow = DefaultAreaSpawnAttempsCountPerPreset; if (isInitialSpawn) { maxSpawnFailedAttemptsInRow *= 16; } using var areasList = Api.Shared.WrapInTempList(spawnZoneAreas.Values); 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 (!this.CanSpawnIfPlayersNearby && ServerIsAnyPlayerNearby(area, playersPositions)) { continue; } // make a few attempts to spawn in this area var attempts = activeSpawnRequestsList.Count * DefaultAreaSpawnAttempsCountPerPreset; 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; } 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); } } }
protected override void PrepareProtoTile(Settings settings) { settings.AmbientSoundProvider = new TileAmbientSoundProvider( new AmbientSoundPreset(new SoundResource("Ambient/Pit"))); settings.AddGroundTexture( new ProtoTileGroundTexture( texture: GroundTexture1, blendMaskTexture: BlendMaskTextureGeneric2Smooth, noiseSelector: null)); // add formations decals var formationsTextures = ProtoTileDecal.CollectTextures("Terrain/SaltFlats/SaltFlatsFormations01*"); var formationsSize = new Vector2Ushort(2, 2); var formationsNoiseSelector = new NoiseSelector( from: 0.5, to: 0.58, noise: new PerlinNoise(seed: 470398528, scale: 5, octaves: 4, persistance: 0.9, lacunarity: 3.9)); for (ushort x = 0; x <= 1; x++) { for (ushort y = 0; y <= 1; y++) { settings.AddDecal( new ProtoTileDecal(formationsTextures, size: formationsSize, offset: (x, y), noiseSelector: formationsNoiseSelector)); } } // add pothole decals settings.AddDecal( new ProtoTileDecal("Terrain/SaltFlats/SaltFlatsPothole*", size: (2, 2), drawOrder: DrawOrder.GroundDecals, noiseSelector: new CombinedNoiseSelector( new NoiseSelector( from: 0.97, to: 1, noise: new WhiteNoise(seed: 901267462)), new NoiseSelector( from: 0.97, to: 1, noise: new WhiteNoise(seed: 68091273))))); // add gully decals settings.AddDecal( new ProtoTileDecal(ProtoTileDecal.CollectTextures("Terrain/SaltFlats/SaltFlatsGully*"), size: (2, 2), drawOrder: DrawOrder.GroundDecalsUnder, noiseSelector: new CombinedNoiseSelector( new NoiseSelector( from: 0.98, to: 1, noise: new WhiteNoise(seed: 981461233)), new NoiseSelector( from: 0.98, to: 1, noise: new WhiteNoise(seed: 598236742)), new NoiseSelector( from: 0.98, to: 1, noise: new WhiteNoise(seed: 98347443)), new NoiseSelector( from: 0.98, to: 1, noise: new WhiteNoise(seed: 1324234897)), new NoiseSelector( from: 0.98, to: 1, noise: new WhiteNoise(seed: 324643342))))); }