diff --git a/simulation_parameters/Constants.cs b/simulation_parameters/Constants.cs index 2aa4dee87cb..49518ebd8ec 100644 --- a/simulation_parameters/Constants.cs +++ b/simulation_parameters/Constants.cs @@ -182,7 +182,7 @@ public static class Constants public const float CLOUD_CHEAT_DENSITY = 16000.0f; - public const float TERRAIN_GRID_SIZE = 200; + public const float TERRAIN_GRID_SIZE = 320; public const float TERRAIN_GRID_SIZE_INV = 1 / TERRAIN_GRID_SIZE; public const float TERRAIN_EDGE_PROTECTION_SIZE = 1; @@ -196,7 +196,16 @@ public static class Constants /// If too low, then pop in is visible and the spawn system can spawn stuff in that then gets covered /// by the terrain. /// - public const int TERRAIN_SPAWN_AREA_NUMBER = 4; + public const int TERRAIN_SPAWN_AREA_NUMBER = 1; + + public const int TERRAIN_VENT_SEGMENTS = 6; + public const int TERRAIN_VENT_RINGS_MAX = 5; + public const int TERRAIN_VENT_RINGS_MIN = 2; + public const int TERRAIN_SECOND_VENT_RINGS_MIN_THRESHOLD = 3; + public const float TERRAIN_SECOND_VENT_CHANCE = 0.4f; + public const int TERRAIN_VENT_OVERLAP_MARGIN = 2; + public const int TERRAIN_VENT_RING_HEIGHT_REDUCTION = 10; + public const int TERRAIN_VENT_OUTER_RING_HEIGHT = -27; public const int MEMBRANE_RESOLUTION = 10; public const int MEMBRANE_VERTICAL_RESOLUTION = 7; diff --git a/simulation_parameters/microbe_stage/biomes.json b/simulation_parameters/microbe_stage/biomes.json index 679f4427dea..e4787bf386e 100644 --- a/simulation_parameters/microbe_stage/biomes.json +++ b/simulation_parameters/microbe_stage/biomes.json @@ -543,11 +543,12 @@ } }, "Terrain": { - "MinClusters": 0, - "MaxClusters": 5, + "MinClusters": 2, + "MaxClusters": 8, "PotentialClusters": [ { "RelativeChance": 1, + "SpawnStrategy": 1, "TerrainGroups": [ { "RelativePosition": { @@ -559,32 +560,18 @@ { "Visuals": "PyriteTerrain1", "CollisionShapePath": "res://assets/models/microbe_terrain/PyriteTerrainChunk1.shape", - "Radius": 14.6, + "Radius": 13, "RelativePosition": { "x": 0, "y": 0, "z": 0 }, "RandomizeRotation": true - } - ] - } - ] - }, - { - "RelativeChance": 1, - "TerrainGroups": [ - { - "RelativePosition": { - "x": 0, - "y": 0, - "z": 0 - }, - "Chunks": [ + }, { "Visuals": "PyriteTerrain2", "CollisionShapePath": "res://assets/models/microbe_terrain/PyriteTerrainChunk2.shape", - "Radius": 18, + "Radius": 13, "RelativePosition": { "x": 0, "y": 0, @@ -598,6 +585,7 @@ }, { "RelativeChance": 1, + "SpawnStrategy": 1, "TerrainGroups": [ { "RelativePosition": { @@ -616,21 +604,7 @@ "z": 0 }, "RandomizeRotation": true - } - ] - } - ] - }, - { - "RelativeChance": 1, - "TerrainGroups": [ - { - "RelativePosition": { - "x": 0, - "y": 0, - "z": 0 - }, - "Chunks": [ + }, { "Visuals": "ChalcopyriteTerrain2", "CollisionShapePath": "res://assets/models/microbe_terrain/ChalcopyriteTerrainChunk2.shape", @@ -647,7 +621,8 @@ ] }, { - "RelativeChance": 1, + "RelativeChance": 2, + "SpawnStrategy": 1, "TerrainGroups": [ { "RelativePosition": { @@ -666,25 +641,11 @@ "z": 0 }, "RandomizeRotation": true - } - ] - } - ] - }, - { - "RelativeChance": 1, - "TerrainGroups": [ - { - "RelativePosition": { - "x": 0, - "y": 0, - "z": 0 - }, - "Chunks": [ + }, { "Visuals": "SerpentiniteTerrain2", "CollisionShapePath": "res://assets/models/microbe_terrain/SerpentiniteTerrainChunk2.shape", - "Radius": 17.5, + "Radius": 13.5, "RelativePosition": { "x": 0, "y": 0, @@ -697,7 +658,7 @@ ] }, { - "RelativeChance": 1, + "RelativeChance": 15, "TerrainGroups": [ { "RelativePosition": { @@ -716,21 +677,7 @@ "z": 0 }, "RandomizeRotation": true - } - ] - } - ] - }, - { - "RelativeChance": 1, - "TerrainGroups": [ - { - "RelativePosition": { - "x": 0, - "y": 0, - "z": 0 - }, - "Chunks": [ + }, { "Visuals": "BariteTerrainChunk2", "CollisionShapePath": "res://assets/models/microbe_terrain/BariteTerrainChunk2.shape", @@ -747,7 +694,8 @@ ] }, { - "RelativeChance": 1, + "RelativeChance": 3, + "SpawnStrategy": 1, "TerrainGroups": [ { "RelativePosition": { @@ -766,21 +714,7 @@ "z": 0 }, "RandomizeRotation": true - } - ] - } - ] - }, - { - "RelativeChance": 1, - "TerrainGroups": [ - { - "RelativePosition": { - "x": 0, - "y": 0, - "z": 0 - }, - "Chunks": [ + }, { "Visuals": "CalciteTerrainChunk2", "CollisionShapePath": "res://assets/models/microbe_terrain/CalciteTerrainChunk2.shape", @@ -797,7 +731,8 @@ ] }, { - "RelativeChance": 1, + "RelativeChance": 3, + "SpawnStrategy": 1, "TerrainGroups": [ { "RelativePosition": { @@ -816,21 +751,7 @@ "z": 0 }, "RandomizeRotation": true - } - ] - } - ] - }, - { - "RelativeChance": 1, - "TerrainGroups": [ - { - "RelativePosition": { - "x": 0, - "y": 0, - "z": 0 - }, - "Chunks": [ + }, { "Visuals": "QuartzTerrain2", "CollisionShapePath": "res://assets/models/microbe_terrain/QuartzTerrainChunk2.shape", @@ -841,25 +762,11 @@ "z": 0 }, "RandomizeRotation": true - } - ] - } - ] - }, - { - "RelativeChance": 1, - "TerrainGroups": [ - { - "RelativePosition": { - "x": 0, - "y": 0, - "z": 0 - }, - "Chunks": [ + }, { "Visuals": "QuartzTerrain3", "CollisionShapePath": "res://assets/models/microbe_terrain/QuartzTerrainChunk3.shape", - "Radius": 13.1, + "Radius": 12, "RelativePosition": { "x": 0, "y": 0, @@ -3265,11 +3172,11 @@ } }, "Terrain": { - "MinClusters": 4, - "MaxClusters": 8, + "MinClusters": 2, + "MaxClusters": 7, "PotentialClusters": [ { - "RelativeChance": 1, + "RelativeChance": 3, "TerrainGroups": [ { "RelativePosition": { @@ -3288,21 +3195,7 @@ "z": 0 }, "RandomizeRotation": true - } - ] - } - ] - }, - { - "RelativeChance": 1, - "TerrainGroups": [ - { - "RelativePosition": { - "x": 0, - "y": 0, - "z": 0 - }, - "Chunks": [ + }, { "Visuals": "CalciteTerrainChunk2", "CollisionShapePath": "res://assets/models/microbe_terrain/CalciteTerrainChunk2.shape", @@ -3319,7 +3212,7 @@ ] }, { - "RelativeChance": 1, + "RelativeChance": 2, "TerrainGroups": [ { "RelativePosition": { @@ -3338,21 +3231,7 @@ "z": 0 }, "RandomizeRotation": true - } - ] - } - ] - }, - { - "RelativeChance": 1, - "TerrainGroups": [ - { - "RelativePosition": { - "x": 0, - "y": 0, - "z": 0 - }, - "Chunks": [ + }, { "Visuals": "BasaltTerrainChunk2", "CollisionShapePath": "res://assets/models/microbe_terrain/BasaltTerrainChunk2.shape", diff --git a/src/microbe_stage/MicrobeCamera.cs b/src/microbe_stage/MicrobeCamera.cs index 050d41d7c72..abd188037a6 100644 --- a/src/microbe_stage/MicrobeCamera.cs +++ b/src/microbe_stage/MicrobeCamera.cs @@ -20,7 +20,7 @@ public partial class MicrobeCamera : Camera3D, ISaveLoadedTracked, IGameCamera, /// How fast the camera zooming is /// [Export] - public float ZoomSpeed = 1.4f; + public float ZoomSpeed = 50.4f; /// /// The height at which the camera starts at diff --git a/src/microbe_stage/MicrobeCamera.tscn b/src/microbe_stage/MicrobeCamera.tscn index 2aeaa475bd7..c5c2098ff1e 100644 --- a/src/microbe_stage/MicrobeCamera.tscn +++ b/src/microbe_stage/MicrobeCamera.tscn @@ -16,3 +16,4 @@ backgroundPlane = NodePath("BackgroundPlane") current = true [node name="BackgroundPlane" parent="." unique_id=949185612 instance=ExtResource("2_yrakl")] +transform = Transform3D(1.1, 0, 0, 0, 1.1, 0, 0, 0, 1.1, 0, 0, -10) diff --git a/src/microbe_stage/Spawners.cs b/src/microbe_stage/Spawners.cs index 28acbf1d948..86411f180d9 100644 --- a/src/microbe_stage/Spawners.cs +++ b/src/microbe_stage/Spawners.cs @@ -142,6 +142,14 @@ public static class SpawnHelpers typeof(MicrobeTerrainChunk), typeof(PredefinedVisuals), typeof(Physics), typeof(PhysicsShapeHolder), typeof(CollisionShapeLoader), typeof(StaticBodyMarker)); + private static readonly Signature TerrainWithoutCollisionSignature = new(typeof(WorldPosition), + typeof(SpatialInstance), typeof(MicrobeTerrainChunk), typeof(PredefinedVisuals), typeof(Physics), + typeof(StaticBodyMarker)); + + private static readonly Signature TerrainCollisionShapeSignature = new(typeof(WorldPosition), + typeof(SpatialInstance), typeof(MicrobeTerrainChunk), typeof(Physics), typeof(PhysicsShapeHolder), + typeof(StaticBodyMarker)); + [Flags] private enum ChunkComponentFlag : short { @@ -1046,18 +1054,8 @@ public static void SpawnCloud(CompoundCloudSystem clouds, Vector3 location, Comp clouds.AddCloud(compound, amount, location + new Vector3(0, 0, 0)); } - public static void SpawnMicrobeTerrain(IWorldSimulation worldSimulation, Vector3 location, Quaternion baseRotation, - TerrainConfiguration.TerrainChunkConfiguration chunkConfiguration, uint groupId, Random random) - { - var recorder = worldSimulation.StartRecordingEntityCommands(); - - SpawnMicrobeTerrainWithoutFinalizing(recorder, worldSimulation, location, baseRotation, chunkConfiguration, - groupId, random); - worldSimulation.FinishRecordingEntityCommands(recorder); - } - - public static void SpawnMicrobeTerrainWithoutFinalizing(CommandBuffer entityRecorder, - IWorldSimulation worldSimulation, Vector3 location, Quaternion baseRotation, + public static void SpawnTerrainWithoutFinalizing(CommandBuffer entityRecorder, + IWorldSimulation worldSimulation, Vector3 location, TerrainConfiguration.TerrainChunkConfiguration chunkConfiguration, uint groupId, Random random) { var entity = worldSimulation.CreateEntityDeferred(entityRecorder, TerrainSignature); @@ -1065,11 +1063,11 @@ public static void SpawnMicrobeTerrainWithoutFinalizing(CommandBuffer entityReco Quaternion rotation; if (chunkConfiguration.RandomizeRotation) { - rotation = baseRotation * new Quaternion(Vector3.Up, random.NextSingle() * MathF.Tau); + rotation = new Quaternion(Vector3.Up, random.NextSingle() * MathF.Tau); } else { - rotation = baseRotation * chunkConfiguration.DefaultRotation; + rotation = chunkConfiguration.DefaultRotation; } entityRecorder.Set(entity, new WorldPosition(location, rotation)); @@ -1085,6 +1083,7 @@ public static void SpawnMicrobeTerrainWithoutFinalizing(CommandBuffer entityReco { BodyIsStatic = true, }); + entityRecorder.Set(entity, new CollisionShapeLoader { Density = 1000, @@ -1100,6 +1099,66 @@ public static void SpawnMicrobeTerrainWithoutFinalizing(CommandBuffer entityReco entityRecorder.Set(entity); } + public static void SpawnTerrainWithoutCollisionWithoutFinalizing(CommandBuffer entityRecorder, + IWorldSimulation worldSimulation, Vector3 location, + TerrainConfiguration.TerrainChunkConfiguration chunkConfiguration, uint groupId, Random random) + { + var entity = worldSimulation.CreateEntityDeferred(entityRecorder, TerrainWithoutCollisionSignature); + + Quaternion rotation; + if (chunkConfiguration.RandomizeRotation) + { + rotation = new Quaternion(Vector3.Up, random.NextSingle() * MathF.Tau); + } + else + { + rotation = chunkConfiguration.DefaultRotation; + } + + entityRecorder.Set(entity, new WorldPosition(location, rotation)); + + entityRecorder.Set(entity); + entityRecorder.Set(entity, new PredefinedVisuals + { + VisualIdentifier = chunkConfiguration.Visuals, + }); + + entityRecorder.Set(entity); + + entityRecorder.Set(entity, new MicrobeTerrainChunk + { + TerrainGroupId = groupId, + }); + + entityRecorder.Set(entity); + } + + public static void SpawnTerrainCollisionShapeWithoutFinalizing(CommandBuffer entityRecorder, + IWorldSimulation worldSimulation, Vector3 location, uint groupId, float radius) + { + var entity = worldSimulation.CreateEntityDeferred(entityRecorder, TerrainCollisionShapeSignature); + + Quaternion rotation = Quaternion.Identity; + + entityRecorder.Set(entity, new WorldPosition(location, rotation)); + + entityRecorder.Set(entity); + + entityRecorder.Set(entity); + entityRecorder.Set(entity, new PhysicsShapeHolder + { + BodyIsStatic = true, + Shape = PhysicsShape.CreateSphere(radius), + }); + + entityRecorder.Set(entity, new MicrobeTerrainChunk + { + TerrainGroupId = groupId, + }); + + entityRecorder.Set(entity); + } + // TODO: move further stage spawners to their own file public static MacroscopicCreature SpawnCreature(Species species, Vector3 location, Node worldRoot, PackedScene multicellularScene, bool aiControlled, ISpawnSystem spawnSystem, diff --git a/src/microbe_stage/TerrainConfiguration.cs b/src/microbe_stage/TerrainConfiguration.cs index c15fd3467af..bf3f4144179 100644 --- a/src/microbe_stage/TerrainConfiguration.cs +++ b/src/microbe_stage/TerrainConfiguration.cs @@ -5,6 +5,12 @@ using SharedBase.Archive; using ThriveScriptsShared; +public enum TerrainSpawnStrategy +{ + Single, + Vent, +} + /// /// Configures how microbe terrain is spawned for a patch /// @@ -88,7 +94,7 @@ public class TerrainChunkConfiguration public readonly float Radius; [JsonProperty] - public readonly Vector3 RelativePosition; + public readonly Vector3 RelativePosition = Vector3.Zero; [JsonProperty] public readonly Quaternion DefaultRotation = Quaternion.Identity; @@ -127,15 +133,13 @@ public class TerrainGroupConfiguration public readonly List Chunks = new(); [JsonProperty] - public readonly Vector3 RelativePosition; + public readonly Vector3 RelativePosition = Vector3.Zero; [JsonProperty] public readonly bool RandomizeRotation; public float Radius; - public float OtherTerrainPreventionRadius; - public void Check(string name) { if (Chunks.Count < 1) @@ -160,10 +164,6 @@ public void Check(string name) throw new InvalidRegistryDataException(name, GetType().Name, "Terrain calculated radius is less than 1"); } - - // If the other terrain prevention radius is not set, set it automatically - if (OtherTerrainPreventionRadius < 1) - OtherTerrainPreventionRadius = Radius; } } @@ -185,8 +185,8 @@ public class TerrainClusterConfiguration [JsonProperty] public readonly bool SlideToFit = true; - public float OverallRadius; - public float OverallOverlapRadius; + [JsonProperty] + public readonly TerrainSpawnStrategy SpawnStrategy = TerrainSpawnStrategy.Single; public void Check(string name) { @@ -198,22 +198,9 @@ public void Check(string name) if (RelativeChance < 1) throw new InvalidRegistryDataException(name, GetType().Name, "RelativeChance must be above 0"); - OverallRadius = 0; - OverallOverlapRadius = 0; - foreach (var group in TerrainGroups) { group.Check(name); - - var groupPositionFactor = group.RelativePosition.Length(); - var currentRadius = groupPositionFactor + group.Radius; - - if (currentRadius > OverallRadius) - OverallRadius = currentRadius; - - var overlapRadius = groupPositionFactor + group.OtherTerrainPreventionRadius; - if (overlapRadius > OverallOverlapRadius) - OverallOverlapRadius = overlapRadius; } } } diff --git a/src/microbe_stage/systems/MicrobeTerrainSpawn.ShapesLogic.cs b/src/microbe_stage/systems/MicrobeTerrainSpawn.ShapesLogic.cs new file mode 100644 index 00000000000..2d26393b929 --- /dev/null +++ b/src/microbe_stage/systems/MicrobeTerrainSpawn.ShapesLogic.cs @@ -0,0 +1,332 @@ +namespace Systems; + +using System; +using System.Collections.Generic; +using AngleSharp.Common; +using Arch.Buffer; +using Godot; +using Xoshiro.PRNG32; + +/// +/// Handles terrain spawning shapes logic +/// +public partial class MicrobeTerrainSystem +{ + private bool SpawnNewCluster(Vector2I baseCell, List spawned, + CommandBuffer recorder, XoShiRo128starstar random) + { + var cluster = terrainConfiguration!.GetRandomCluster(random); + + switch (cluster.SpawnStrategy) + { + case TerrainSpawnStrategy.Single: + return SpawnSingleTerrain(baseCell, cluster, recorder, spawned, random); + case TerrainSpawnStrategy.Vent: + return SpawnVentTerrain(baseCell, cluster, recorder, spawned, random); + } + + return false; + } + + private (bool SkipSpawn, Vector3? Position) GetSpawnStartingPosition(Vector2I baseCell, + List spawned, XoShiRo128starstar random, float overlapRadius = 0.0f) + { + var (minX, maxX, minZ, maxZ) = GetCellBordersForGroup(baseCell, overlapRadius); + + var rangeX = maxX - minX; + var rangeZ = maxZ - minZ; + + var playerCheckSquared = MathUtils.Square(playerProtectionRadius); + + for (int i = 0; i < maxSpawnAttempts; ++i) + { + var position = new Vector3(minX + random.NextFloat() * rangeX, 0, minZ + random.NextFloat() * rangeZ); + + if (position.DistanceSquaredTo(playerPosition) + MathUtils.Square(overlapRadius) < playerCheckSquared) + { + // Too close to the player, don't spawn + // This returns true now so that the player position doesn't affect the final terrain configuration + // so that the seed leads to a reproducible terrain result + return (true, null); + } + + var overlaps = OverlapsWithAlreadySpawned(spawned, position); + + if (overlaps) + { + continue; + } + + return (false, position); + } + + return (false, null); + } + + private bool IsPositionInCell(Vector3 position, Vector2I cell, float overlapRadius = 0) + { + var (minX, maxX, minZ, maxZ) = GetCellBordersForGroup(cell, overlapRadius); + + var playerCheckSquared = MathUtils.Square(playerProtectionRadius); + + if (position.DistanceSquaredTo(playerPosition) + MathUtils.Square(overlapRadius) < playerCheckSquared) + { + // Too close to the player, don't spawn + // This returns true now so that the player position doesn't affect the final terrain configuration + // so that the seed leads to a reproducible terrain result + return false; + } + + return position.X >= minX && position.X <= maxX && + position.Z >= minZ && position.Z <= maxZ; + } + + private (float MinX, float MaxX, float MinZ, float MaxZ) GetCellBordersForGroup(Vector2I baseCell, + float overlapRadius) + { + var minX = baseCell.X * Constants.TERRAIN_GRID_SIZE + Constants.TERRAIN_EDGE_PROTECTION_SIZE + overlapRadius; + var maxX = (baseCell.X + 1) * Constants.TERRAIN_GRID_SIZE - Constants.TERRAIN_EDGE_PROTECTION_SIZE - + overlapRadius; + var minZ = baseCell.Y * Constants.TERRAIN_GRID_SIZE + Constants.TERRAIN_EDGE_PROTECTION_SIZE + overlapRadius; + var maxZ = (baseCell.Y + 1) * Constants.TERRAIN_GRID_SIZE - Constants.TERRAIN_EDGE_PROTECTION_SIZE - + overlapRadius; + return (minX, maxX, minZ, maxZ); + } + + private bool OverlapsWithAlreadySpawned(List spawned, + Vector3 position, float overlapRadius = 0) + { + foreach (var spawnedClusters in spawned) + { + foreach (var spawnedGroup in spawnedClusters.TerrainGroups) + { + var distanceSquared = position.DistanceSquaredTo(spawnedGroup.Position); + if (distanceSquared <= MathUtils.Square(spawnedGroup.Radius + overlapRadius)) + { + return true; + } + } + } + + return false; + } + + private bool SpawnSingleTerrain(Vector2I baseCell, TerrainConfiguration.TerrainClusterConfiguration cluster, + CommandBuffer recorder, List spawned, XoShiRo128starstar random) + { + var (skipSpawn, startingPosition) = + GetSpawnStartingPosition(baseCell, spawned, random); + + if (skipSpawn) + return true; + + if (startingPosition is null) + return false; + + var position = startingPosition.Value; + + var chosenGroup = cluster.TerrainGroups.GetItemByIndex(random.Next(cluster.TerrainGroups.Count)); + var groupSpawnData = new GroupSpawnData(position, [position], chosenGroup.Radius, + cluster.TerrainGroups.GetItemByIndex(0).Chunks, chosenGroup.RandomizeRotation); + var clusterSpawnData = new ClusterSpawnData(position, [groupSpawnData], cluster.RandomizeRotation); + + spawned.Add(SpawnTerrainCluster(clusterSpawnData, recorder, random)); + return true; + } + + private bool SpawnVentTerrain(Vector2I baseCell, TerrainConfiguration.TerrainClusterConfiguration cluster, + CommandBuffer recorder, List spawned, + XoShiRo128starstar random) + { + var chosenGroup = cluster.TerrainGroups.GetItemByIndex(random.Next(cluster.TerrainGroups.Count)); + var radius = chosenGroup.Radius; + var layers = random.Next(Constants.TERRAIN_VENT_RINGS_MIN, Constants.TERRAIN_VENT_RINGS_MAX); + var collisionRadius = radius * layers; + var overlapRadius = radius * (layers + Constants.TERRAIN_VENT_OVERLAP_MARGIN); + + var (skipSpawn, startingPosition) = + GetSpawnStartingPosition(baseCell, spawned, random, overlapRadius); + + if (skipSpawn) + return true; + + if (startingPosition is null) + return false; + + var position = startingPosition.Value; + + var overlaps = OverlapsWithAlreadySpawned(spawned, position, overlapRadius); + if (overlaps) + { + return false; + } + + var groupSpawnData = new List(); + var chunksPositions = GetNewVentTerrainPositions(radius, position, random, layers); + + groupSpawnData.Add(new GroupSpawnData(position, chunksPositions, overlapRadius, + chosenGroup.Chunks, chosenGroup.RandomizeRotation, collisionRadius)); + + // Generates a second smaller vent right beside the first one if it fits the grid and doesn't overlap + // with other terrain. Therefore TERRAIN_SECOND_VENT_CHANCE is lower in practise + if (layers >= Constants.TERRAIN_SECOND_VENT_RINGS_MIN_THRESHOLD && + random.NextFloat() <= Constants.TERRAIN_SECOND_VENT_CHANCE) + { + var newPositionDistance = overlapRadius - 1; + var angle = random.NextFloat() * MathF.Tau; + var secondVentPosition = position + new Vector3(MathF.Cos(angle) * newPositionDistance, + 0, + MathF.Sin(angle) * newPositionDistance); + + layers = random.Next(Constants.TERRAIN_VENT_RINGS_MIN, layers + 1); + collisionRadius = radius * layers; + overlapRadius = radius * (layers + Constants.TERRAIN_VENT_OVERLAP_MARGIN); + + if (!OverlapsWithAlreadySpawned(spawned, position, overlapRadius) + && IsPositionInCell(secondVentPosition, baseCell, overlapRadius)) + { + var secondVentChunksPositions = GetNewVentTerrainPositions(radius, secondVentPosition, random, layers); + groupSpawnData.Add(new GroupSpawnData(secondVentPosition, secondVentChunksPositions, overlapRadius, + chosenGroup.Chunks, chosenGroup.RandomizeRotation, collisionRadius)); + } + } + + var clusterSpawnData = new ClusterSpawnData(position, groupSpawnData, cluster.RandomizeRotation); + + spawned.Add(SpawnTerrainCluster(clusterSpawnData, recorder, random)); + + return true; + } + + private List GetNewVentTerrainPositions(float radius, + Vector3 chunkGroupPosition, + XoShiRo128starstar random, int layers) + { + var segments = Constants.TERRAIN_VENT_SEGMENTS; + var segmentRadius = radius; + var yLevel = Constants.TERRAIN_VENT_RING_HEIGHT_REDUCTION * layers + Constants.TERRAIN_VENT_OUTER_RING_HEIGHT; + + var chunksPositions = new List(); + + for (var j = 0; j < layers; ++j) + { + for (var i = 0; i < segments; ++i) + { + var angle = i * MathF.Tau / segments; + var x = chunkGroupPosition.X + MathF.Cos(angle) * segmentRadius + random.Next(-2, 2); + var z = chunkGroupPosition.Z + MathF.Sin(angle) * segmentRadius + random.Next(-2, 2); + var position = new Vector3(x, yLevel + random.Next(-1, 1), z); + chunksPositions.Add(position); + } + + yLevel -= Constants.TERRAIN_VENT_RING_HEIGHT_REDUCTION; + segments += Constants.TERRAIN_VENT_SEGMENTS; + segmentRadius += radius; + } + + return chunksPositions; + } + + private SpawnedTerrainCluster SpawnTerrainCluster(ClusterSpawnData clusterSpawnData, CommandBuffer recorder, + XoShiRo128starstar random) + { + var groupData = new SpawnedTerrainGroup[clusterSpawnData.Groups.Count]; + var index = 0; + + var clusterRotation = Quaternion.Identity; + + if (clusterSpawnData.RandomizeRotation) + clusterRotation = new Quaternion(Vector3.Up, random.NextSingle() * MathF.Tau); + + for (var i = 0; i < clusterSpawnData.Groups.Count; ++i) + { + var group = clusterSpawnData.Groups[i]; + var data = SpawnTerrainGroup(group, clusterRotation, recorder, random); + + groupData[index] = data; + ++index; + } + + return new SpawnedTerrainCluster(clusterSpawnData.Position, groupData); + } + + private SpawnedTerrainGroup SpawnTerrainGroup(GroupSpawnData groupSpawnData, Quaternion clusterRotation, + CommandBuffer recorder, + XoShiRo128starstar random) + { + var groupId = nextGroupId++; + var data = new SpawnedTerrainGroup(groupSpawnData.Position, + groupSpawnData.OverlapRadius, + groupId); + + var groupRotation = clusterRotation; + + if (groupSpawnData.RandomizeRotation) + groupRotation += new Quaternion(Vector3.Up, random.NextSingle() * MathF.Tau); + + var skipDefaultCollisionsLoading = groupSpawnData.CollisionRadius > 0; + if (skipDefaultCollisionsLoading) + { + SpawnHelpers.SpawnTerrainCollisionShapeWithoutFinalizing(recorder, worldSimulation, + groupSpawnData.Position, groupId, groupSpawnData.CollisionRadius); + data.ExpectedMemberCount += 1; + } + + foreach (var position in groupSpawnData.ChunksPositions) + { + var chunk = groupSpawnData.Chunks.GetItemByIndex(random.Next(groupSpawnData.Chunks.Count)); + var yOffset = new Vector3(0, random.NextSingle() * Constants.TERRAIN_HEIGHT_RANDOMNESS, 0); + + if (skipDefaultCollisionsLoading) + { + SpawnHelpers.SpawnTerrainWithoutCollisionWithoutFinalizing(recorder, worldSimulation, + position * groupRotation + yOffset, chunk, groupId, random); + } + else + { + SpawnHelpers.SpawnTerrainWithoutFinalizing(recorder, worldSimulation, + position * groupRotation + yOffset, chunk, groupId, random); + } + + data.ExpectedMemberCount += 1; + } + + return data; + } + + /// + /// Data required to spawn a terrain group + /// + /// Main position of the group + /// Positions of all the chunks of the group + /// Radius of the group in which no other chunk can spawn + /// Data of the terrain chunks to spawn + /// + /// Used to create one sphere collision shape of the group if greater than 0. If that condition is met, + /// other chunk's collision meshes won't be loaded + /// + private class GroupSpawnData(Vector3 position, List chunksPositions, float overlapRadius, + List chunks, bool randomizeRotation, float collisionRadius = 0) + { + public readonly Vector3 Position = position; + public readonly List ChunksPositions = chunksPositions; + public readonly float OverlapRadius = overlapRadius; + public readonly float CollisionRadius = collisionRadius; + public readonly List Chunks = chunks; + public readonly bool RandomizeRotation = randomizeRotation; + } + + /// + /// Data required to spawn a terrain cluster. The cluster can create more complex shapes by combining + /// multiple groups together + /// + /// Main position of the cluster + /// Groups that compose the cluster + private class ClusterSpawnData(Vector3 position, + List groups, bool randomizeRotation) + { + public readonly Vector3 Position = position; + public readonly List Groups = groups; + public readonly bool RandomizeRotation = randomizeRotation; + } +} diff --git a/src/microbe_stage/systems/MicrobeTerrainSpawn.ShapesLogic.cs.uid b/src/microbe_stage/systems/MicrobeTerrainSpawn.ShapesLogic.cs.uid new file mode 100644 index 00000000000..2b0566a9d6f --- /dev/null +++ b/src/microbe_stage/systems/MicrobeTerrainSpawn.ShapesLogic.cs.uid @@ -0,0 +1 @@ +uid://cqmm5gg2jkxqn diff --git a/src/microbe_stage/systems/MicrobeTerrainSystem.cs b/src/microbe_stage/systems/MicrobeTerrainSystem.SpawnLogic.cs similarity index 65% rename from src/microbe_stage/systems/MicrobeTerrainSystem.cs rename to src/microbe_stage/systems/MicrobeTerrainSystem.SpawnLogic.cs index a806853d4fc..a1b49152b7f 100644 --- a/src/microbe_stage/systems/MicrobeTerrainSystem.cs +++ b/src/microbe_stage/systems/MicrobeTerrainSystem.SpawnLogic.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; using System.Runtime.CompilerServices; -using Arch.Buffer; using Arch.Core; using Arch.System; using Components; @@ -17,7 +16,7 @@ [RunsBefore(typeof(SpawnSystem))] [ReadsComponent(typeof(MicrobeTerrainChunk))] [RuntimeCost(1)] -public class MicrobeTerrainSystem : BaseSystem, IArchivable +public partial class MicrobeTerrainSystem : BaseSystem, IArchivable { public const ushort SERIALIZATION_VERSION = 1; @@ -38,10 +37,10 @@ public class MicrobeTerrainSystem : BaseSystem, IArchivable private Vector3 nextPlayerPosition; - private float playerProtectionRadius = 20; + private float playerProtectionRadius = 50; private int maxSpawnAttempts = 10; - private int differentClusterTypeAttempts = 3; + private int differentClusterTypeAttempts = 9; private long baseSeed; @@ -79,8 +78,8 @@ private MicrobeTerrainSystem(IWorldSimulation worldSimulation, World world, public static Vector2I PositionToTerrainCell(Vector3 position) { - return new Vector2I((int)(position.X * Constants.TERRAIN_GRID_SIZE_INV), - (int)(position.Z * Constants.TERRAIN_GRID_SIZE_INV)); + return new Vector2I((int)Math.Floor(position.X * Constants.TERRAIN_GRID_SIZE_INV), + (int)Math.Floor(position.Z * Constants.TERRAIN_GRID_SIZE_INV)); } public static void WriteToArchive(ISArchiveWriter writer, ArchiveObjectType type, object obj) @@ -150,27 +149,7 @@ public bool IsPositionBlocked(Vector3 position, float checkRadius = 5) if (!terrainGridData.TryGetValue(cell, out var clusters)) return false; - // Find if too close to any terrain group - foreach (var cluster in clusters) - { - // Can filter by cluster distance first to cut down on overall checks - if (position.DistanceSquaredTo(cluster.CenterPosition) > MathUtils.Square(checkRadius + cluster.MaxRadius)) - continue; - - foreach (var terrainGroup in cluster.Parts) - { - // And then check individual groups in a cluster the spawn position is too close to - if (position.DistanceSquaredTo(terrainGroup.Position) <= - MathUtils.Square(checkRadius + terrainGroup.Radius)) - { - // Found a colliding part - return true; - } - } - } - - // No terrain encountered - return false; + return OverlapsWithAlreadySpawned(clusters, position, checkRadius); } public override void Update(in float delta) @@ -234,9 +213,15 @@ public override void Update(in float delta) // Queue despawning of terrain cells that are out of range foreach (var entry in terrainGridData) { - var distance = Math.Abs(entry.Key.X - playerGrid.X) + Math.Abs(entry.Key.Y - playerGrid.Y); + var minX = playerGrid.X - Constants.TERRAIN_SPAWN_AREA_NUMBER; + var maxX = playerGrid.X + Constants.TERRAIN_SPAWN_AREA_NUMBER; + var minZ = playerGrid.Y - Constants.TERRAIN_SPAWN_AREA_NUMBER; + var maxZ = playerGrid.Y + Constants.TERRAIN_SPAWN_AREA_NUMBER; + + var shouldDespawn = entry.Key.X < minX || entry.Key.X > maxX || + entry.Key.Y < minZ || entry.Key.Y > maxZ; - if (distance > Constants.TERRAIN_SPAWN_AREA_NUMBER) + if (shouldDespawn) { // We don't process any despawns immediately here, as they shouldn't be totally time-critical despawnQueue.Add(entry.Key); @@ -255,36 +240,31 @@ public override void Update(in float delta) { var currentPos = new Vector2I(x, z); - var distance = Math.Abs(currentPos.X - playerGrid.X) + Math.Abs(currentPos.Y - playerGrid.Y); - - if (distance <= Constants.TERRAIN_SPAWN_AREA_NUMBER) + if (!terrainGridData.TryGetValue(currentPos, out _)) { - if (!terrainGridData.TryGetValue(currentPos, out _)) + // Limited spawns per frame. But if initially spawning in terrain then we want to spawn + // everything at once to not leave an empty world for a little bit + if (spawns < spawnsPerUpdate || initialSpawn) { - // Limited spawns per frame. But if initially spawning in terrain then we want to spawn - // everything at once to not leave an empty world for a little bit - if (spawns < spawnsPerUpdate || initialSpawn) - { - SpawnTerrainCell(currentPos); - ++spawns; - } - else - { - // And queue the other ones. - // This contains check here is in case the queue is long, and the player is moving - // superfast causing duplicate terrain load requests. - if (!spawnQueue.Contains(currentPos)) - { - spawnQueue.Add(currentPos); - } - } + SpawnTerrainCell(currentPos); + ++spawns; } else { - // Make sure existing data won't be deleted - despawnQueue.Remove(currentPos); + // And queue the other ones. + // This contains check here is in case the queue is long, and the player is moving + // superfast causing duplicate terrain load requests. + if (!spawnQueue.Contains(currentPos)) + { + spawnQueue.Add(currentPos); + } } } + else + { + // Make sure existing data won't be deleted + despawnQueue.Remove(currentPos); + } } } } @@ -329,7 +309,7 @@ private void DespawnGridArea(List clusters) bool missing = false; foreach (var cluster in clusters) { - foreach (var group in cluster.Parts) + foreach (var group in cluster.TerrainGroups) { foreach (var entity in group.GroupMembers) { @@ -356,7 +336,7 @@ private void DespawnGridArea(List clusters) foreach (var cluster in clusters) { - foreach (var group in cluster.Parts) + foreach (var group in cluster.TerrainGroups) { foreach (var entity in group.GroupMembers) { @@ -435,7 +415,6 @@ private void SpawnTerrainCell(Vector2I cell) } } - // TODO: might want to remove this print entirely as it's only really useful when tweaking chunk spawn rates #if DEBUG if (result.Count < wantedClusters) { @@ -450,170 +429,13 @@ private void SpawnTerrainCell(Vector2I cell) unsuccessfulFetches = 0; } - private bool SpawnNewCluster(Vector2I baseCell, List spawned, CommandBuffer recorder, - XoShiRo128starstar random) - { - var cluster = terrainConfiguration!.GetRandomCluster(random); - - var minX = baseCell.X * Constants.TERRAIN_GRID_SIZE + Constants.TERRAIN_EDGE_PROTECTION_SIZE + - cluster.OverallRadius; - var maxX = (baseCell.X + 1) * Constants.TERRAIN_GRID_SIZE - Constants.TERRAIN_EDGE_PROTECTION_SIZE - - cluster.OverallRadius; - - var minZ = baseCell.Y * Constants.TERRAIN_GRID_SIZE + Constants.TERRAIN_EDGE_PROTECTION_SIZE + - cluster.OverallRadius; - var maxZ = (baseCell.Y + 1) * Constants.TERRAIN_GRID_SIZE - Constants.TERRAIN_EDGE_PROTECTION_SIZE - - cluster.OverallRadius; - - var rangeX = maxX - minX; - var rangeZ = maxZ - minZ; - - var currentOverlap = cluster.OverallOverlapRadius; - - var playerCheckSquared = MathUtils.Square(playerProtectionRadius + cluster.OverallRadius); - - // Find a good spot, avoiding overlap with already spawned, or the player position - for (int i = 0; i < maxSpawnAttempts; ++i) - { - var position = new Vector3(minX + random.NextFloat() * rangeX, 0, minZ + random.NextFloat() * rangeZ); - - // Keep randomness more consistent - float slideAngleIfNeeded = random.NextFloat() * MathF.PI * 2; - - if (position.DistanceSquaredTo(playerPosition) < playerCheckSquared) - { - // Too close to the player, don't spawn - // This returns true now so that the player position doesn't affect the final terrain configuration - // so that the seed leads to a reproducible terrain result - return true; - - // continue; - } - - bool overlaps = false; - bool adjusted = false; - - while (true) - { - bool retry = false; - - // Then check against already spawned terrain - foreach (var alreadySpawned in spawned) - { - var distanceSquared = position.DistanceSquaredTo(alreadySpawned.CenterPosition); - if (distanceSquared >= MathUtils.Square(alreadySpawned.OverlapRadius + currentOverlap)) - continue; - - // Try sliding to make both clusters fit - if (cluster.SlideToFit && !adjusted) - { - // TODO: this could alternatively not take a random angle but instead just slide outwards - // to the closest edge with a normalized vector from CenterPosition to position - - // var overlap = (alreadySpawned.OverlapRadius + currentOverlap) - MathF.Sqrt(distanceSquared); - // var slideVector = new Vector3(MathF.Cos(slideAngleIfNeeded), 0, - // MathF.Sin(slideAngleIfNeeded)) * overlap * 1.02f; - // position += (alreadySpawned.CenterPosition - position).Normalized() * slideVector; - - var neededDistance = alreadySpawned.OverlapRadius + currentOverlap; - var offset = new Vector3(MathF.Cos(slideAngleIfNeeded), 0, - MathF.Sin(slideAngleIfNeeded)) * neededDistance * 1.02f; - - position = alreadySpawned.CenterPosition + offset; - - // Make sure the position is not outside the target grid - if (position.X < minX || position.X > maxX || position.Z < minZ || position.Z > maxZ) - { - overlaps = true; - break; - } - -#if DEBUG - var newDistance = position.DistanceSquaredTo(alreadySpawned.CenterPosition); - if (newDistance < MathUtils.Square(alreadySpawned.OverlapRadius + currentOverlap)) - { - GD.Print($"Still overlaps after sliding with the original, " + - $"old dist: {MathF.Sqrt(distanceSquared)} new: {MathF.Sqrt(newDistance)}, " + - $"needed distance: {alreadySpawned.OverlapRadius + currentOverlap}"); - } -#endif - - retry = true; - adjusted = true; - break; - } - - // Will overlap existing - overlaps = true; - break; - } - - if (retry) - continue; - - break; - } - - if (overlaps) - continue; - - // No problems can spawn the cluster - spawned.Add(SpawnCluster(cluster, position, recorder, random)); - return true; - } - - return false; - } - - private SpawnedTerrainCluster SpawnCluster(TerrainConfiguration.TerrainClusterConfiguration cluster, - Vector3 position, CommandBuffer recorder, XoShiRo128starstar random) - { - var groupData = new SpawnedTerrainGroup[cluster.TerrainGroups.Count]; - - int index = 0; - - var clusterRotation = Quaternion.Identity; - - if (cluster.RandomizeRotation) - clusterRotation = new Quaternion(Vector3.Up, random.NextSingle() * MathF.Tau); - - foreach (var terrainGroup in cluster.TerrainGroups) - { - var groupId = nextGroupId++; - var data = new SpawnedTerrainGroup(position + terrainGroup.RelativePosition, terrainGroup.Radius, groupId); - - var groupRotation = Quaternion.Identity; - - if (terrainGroup.RandomizeRotation) - groupRotation = new Quaternion(Vector3.Up, random.NextSingle() * MathF.Tau); - - foreach (var chunk in terrainGroup.Chunks) - { - var rotation = clusterRotation * groupRotation; - - var yOffset = new Vector3(0, random.NextSingle() * Constants.TERRAIN_HEIGHT_RANDOMNESS, 0); - - SpawnHelpers.SpawnMicrobeTerrainWithoutFinalizing(recorder, worldSimulation, - position + rotation * (terrainGroup.RelativePosition + chunk.RelativePosition) + yOffset, - rotation, chunk, groupId, random); - - data.ExpectedMemberCount += 1; - } - - groupData[index] = data; - ++index; - } - - return new SpawnedTerrainCluster(position, cluster.OverallRadius, groupData, cluster.OverallOverlapRadius); - } - private void FetchSpawnedChunksToOurData() { foreach (var entry in terrainGridData) { foreach (var terrainCluster in entry.Value) { - foreach (var spawnedTerrainGroup in terrainCluster.Parts) + foreach (var spawnedTerrainGroup in terrainCluster.TerrainGroups) { if (!spawnedTerrainGroup.MembersFetched) { @@ -652,17 +474,13 @@ private void FetchSpawnedChunksToOurData() } // This is internal to make archive registration work - internal struct SpawnedTerrainCluster(Vector3 centerPosition, float maxRadius, SpawnedTerrainGroup[] parts, - float overlapRadius) : IArchivable + internal struct SpawnedTerrainCluster(Vector3 centerPosition, SpawnedTerrainGroup[] terrainGroups) : IArchivable { - public const ushort SERIALIZATION_VERSION_CLUSTER = 2; + public const ushort SERIALIZATION_VERSION_CLUSTER = 3; public Vector3 CenterPosition = centerPosition; - public float MaxRadius = maxRadius; - - public SpawnedTerrainGroup[] Parts = parts; - public float OverlapRadius = overlapRadius; + public SpawnedTerrainGroup[] TerrainGroups = terrainGroups; public ushort CurrentArchiveVersion => SERIALIZATION_VERSION_CLUSTER; public ArchiveObjectType ArchiveObjectType => (ArchiveObjectType)ThriveArchiveObjectType.SpawnedTerrainCluster; @@ -684,17 +502,12 @@ public static SpawnedTerrainCluster ReadFromArchive(ISArchiveReader reader, usho throw new InvalidArchiveVersionException(version, SERIALIZATION_VERSION_CLUSTER); var center = reader.ReadVector3(); - var maxRadius = reader.ReadFloat(); + if (version < 3) + _ = reader.ReadFloat(); var parts = reader.ReadObject(); - var overlapRadius = reader.ReadFloat(); - var instance = new SpawnedTerrainCluster(center, maxRadius, parts, overlapRadius); - - // Override the values that got incorrectly doubled from old save load - if (version < 2) - { - instance.MaxRadius = MathF.Sqrt(maxRadius); - instance.OverlapRadius = MathF.Sqrt(overlapRadius); - } + if (version < 3) + _ = reader.ReadFloat(); + var instance = new SpawnedTerrainCluster(center, parts); return instance; } @@ -707,9 +520,7 @@ public static object ReadFromArchiveBoxed(ISArchiveReader reader, ushort version public void WriteToArchive(ISArchiveWriter writer) { writer.Write(CenterPosition); - writer.Write(MaxRadius); - writer.WriteObject(Parts); - writer.Write(OverlapRadius); + writer.WriteObject(TerrainGroups); } } diff --git a/src/microbe_stage/systems/MicrobeTerrainSystem.cs.uid b/src/microbe_stage/systems/MicrobeTerrainSystem.SpawnLogic.cs.uid similarity index 100% rename from src/microbe_stage/systems/MicrobeTerrainSystem.cs.uid rename to src/microbe_stage/systems/MicrobeTerrainSystem.SpawnLogic.cs.uid diff --git a/src/microbe_stage/systems/SpawnSystem.cs b/src/microbe_stage/systems/SpawnSystem.cs index a77f3fc14ed..40ccc412214 100644 --- a/src/microbe_stage/systems/SpawnSystem.cs +++ b/src/microbe_stage/systems/SpawnSystem.cs @@ -80,7 +80,7 @@ public partial class SpawnSystem : BaseSystem, ISpawnSystem, IArch private int despawnedCount; private float spawnRadiusCheck = 5.5f; - private int maxDifferentPositionsCheck = 10; + private int maxDifferentPositionsCheck = 20; public SpawnSystem(IWorldSimulation worldSimulation, World world, IsSpawnPositionBad badSpawnPositionCheck) : base(world)