using System.Collections.Generic; using UnityEngine; namespace InfiniteWorld.VoxelWorld { [CreateAssetMenu(menuName = "Infinite World/Voxel World Config", fileName = "VoxelWorldConfig")] public sealed class VoxelWorldConfig : ScriptableObject { [Header("Streaming")] [Min(8)] public int chunkSize = 16; [Min(1)] public int generationRadius = 3; [Min(0)] public int blockingGenerationRadius; public int seed = 12345; [Min(1)] public int maxMountainHeight = 3; [Header("Shape Noise")] public float macroNoiseScale = 0.05f; public float detailNoiseScale = 0.12f; public float ridgeNoiseScale = 0.18f; public float wallThreshold = 0.6f; public float rockBias = 0.04f; [Min(0)] public int smoothingPasses = 2; [Header("Global Passes")] public float passNoiseScale = 0.018f; public float passDetailScale = 0.041f; public float passThreshold = 0.22f; public float passFeather = 0.12f; [Header("Height")] public float heightNoiseScale = 0.08f; public float terraceNoiseScale = 0.17f; public float heightBias = 0.05f; [Header("Biomes")] public List biomeProfiles = new List(); public float biomeNoiseScale = 0.02f; [Min(1f)] public float biomeSize = 48f; [Header("Placements")] public List placementCollections = new List(); [Header("Runtime")] [Min(1)] public int maxAsyncChunkJobs = 2; [Min(1)] public int maxChunkBuildsPerFrame = 1; [Min(1)] public int maxChunkMeshBuildsPerFrame = 1; [Min(1)] public int maxColliderAppliesPerFrame = 1; [Min(0)] public int maxNeighborRefreshesPerFrame = 2; [Min(1)] public int renderRegionSizeInChunks = 4; [Min(1)] public int maxRegionBuildsPerFrame = 1; } internal readonly struct VoxelWorldResolvedSettings { private static readonly IReadOnlyList EmptyBiomes = System.Array.Empty(); private static readonly IReadOnlyList EmptyPlacementCollections = System.Array.Empty(); public static readonly VoxelWorldResolvedSettings Default = Resolve(null); public VoxelWorldResolvedSettings( int chunkSize, int generationRadius, int blockingGenerationRadius, int seed, int maxMountainHeight, float macroNoiseScale, float detailNoiseScale, float ridgeNoiseScale, float wallThreshold, float rockBias, int smoothingPasses, float passNoiseScale, float passDetailScale, float passThreshold, float passFeather, float heightNoiseScale, float terraceNoiseScale, float heightBias, IReadOnlyList biomeProfiles, float biomeNoiseScale, float biomeSize, IReadOnlyList placementCollections, int maxAsyncChunkJobs, int maxChunkBuildsPerFrame, int maxChunkMeshBuildsPerFrame, int maxColliderAppliesPerFrame, int maxNeighborRefreshesPerFrame, int renderRegionSizeInChunks, int maxRegionBuildsPerFrame) { ChunkSize = chunkSize; GenerationRadius = generationRadius; BlockingGenerationRadius = blockingGenerationRadius; Seed = seed; MaxMountainHeight = maxMountainHeight; MacroNoiseScale = macroNoiseScale; DetailNoiseScale = detailNoiseScale; RidgeNoiseScale = ridgeNoiseScale; WallThreshold = wallThreshold; RockBias = rockBias; SmoothingPasses = smoothingPasses; PassNoiseScale = passNoiseScale; PassDetailScale = passDetailScale; PassThreshold = passThreshold; PassFeather = passFeather; HeightNoiseScale = heightNoiseScale; TerraceNoiseScale = terraceNoiseScale; HeightBias = heightBias; BiomeProfiles = biomeProfiles; BiomeNoiseScale = biomeNoiseScale; BiomeSize = biomeSize; PlacementCollections = placementCollections; MaxAsyncChunkJobs = maxAsyncChunkJobs; MaxChunkBuildsPerFrame = maxChunkBuildsPerFrame; MaxChunkMeshBuildsPerFrame = maxChunkMeshBuildsPerFrame; MaxColliderAppliesPerFrame = maxColliderAppliesPerFrame; MaxNeighborRefreshesPerFrame = maxNeighborRefreshesPerFrame; RenderRegionSizeInChunks = renderRegionSizeInChunks; MaxRegionBuildsPerFrame = maxRegionBuildsPerFrame; } public int ChunkSize { get; } public int GenerationRadius { get; } public int BlockingGenerationRadius { get; } public int Seed { get; } public int MaxMountainHeight { get; } public float MacroNoiseScale { get; } public float DetailNoiseScale { get; } public float RidgeNoiseScale { get; } public float WallThreshold { get; } public float RockBias { get; } public int SmoothingPasses { get; } public float PassNoiseScale { get; } public float PassDetailScale { get; } public float PassThreshold { get; } public float PassFeather { get; } public float HeightNoiseScale { get; } public float TerraceNoiseScale { get; } public float HeightBias { get; } public IReadOnlyList BiomeProfiles { get; } public float BiomeNoiseScale { get; } public float BiomeSize { get; } public IReadOnlyList PlacementCollections { get; } public int MaxAsyncChunkJobs { get; } public int MaxChunkBuildsPerFrame { get; } public int MaxChunkMeshBuildsPerFrame { get; } public int MaxColliderAppliesPerFrame { get; } public int MaxNeighborRefreshesPerFrame { get; } public int RenderRegionSizeInChunks { get; } public int MaxRegionBuildsPerFrame { get; } public static VoxelWorldResolvedSettings Resolve(VoxelWorldConfig config) { IReadOnlyList biomes = config != null && config.biomeProfiles != null ? config.biomeProfiles : EmptyBiomes; IReadOnlyList placements = ResolvePlacementCollections(config); return new VoxelWorldResolvedSettings( Mathf.Max(8, config != null ? config.chunkSize : 16), Mathf.Max(1, config != null ? config.generationRadius : 3), Mathf.Max(0, config != null ? config.blockingGenerationRadius : 0), config != null ? config.seed : 12345, Mathf.Max(1, config != null ? config.maxMountainHeight : 3), config != null ? config.macroNoiseScale : 0.05f, config != null ? config.detailNoiseScale : 0.12f, config != null ? config.ridgeNoiseScale : 0.18f, config != null ? config.wallThreshold : 0.6f, config != null ? config.rockBias : 0.04f, Mathf.Max(0, config != null ? config.smoothingPasses : 2), config != null ? config.passNoiseScale : 0.018f, config != null ? config.passDetailScale : 0.041f, config != null ? config.passThreshold : 0.22f, config != null ? config.passFeather : 0.12f, config != null ? config.heightNoiseScale : 0.08f, config != null ? config.terraceNoiseScale : 0.17f, config != null ? config.heightBias : 0.05f, biomes, config != null ? config.biomeNoiseScale : 0.02f, Mathf.Max(1f, config != null ? config.biomeSize : 48f), placements, Mathf.Max(1, config != null ? config.maxAsyncChunkJobs : 2), Mathf.Max(1, config != null ? config.maxChunkBuildsPerFrame : 1), Mathf.Max(1, config != null ? config.maxChunkMeshBuildsPerFrame : 1), Mathf.Max(1, config != null ? config.maxColliderAppliesPerFrame : 1), Mathf.Max(0, config != null ? config.maxNeighborRefreshesPerFrame : 2), Mathf.Max(1, config != null ? config.renderRegionSizeInChunks : 4), Mathf.Max(1, config != null ? config.maxRegionBuildsPerFrame : 1)); } private static IReadOnlyList ResolvePlacementCollections(VoxelWorldConfig config) { if (config == null || config.placementCollections == null || config.placementCollections.Count == 0) { return EmptyPlacementCollections; } List result = new List(config.placementCollections.Count); for (int collectionIndex = 0; collectionIndex < config.placementCollections.Count; collectionIndex++) { WorldPrefabCollection collection = config.placementCollections[collectionIndex]; if (WorldPlacementValidation.TryGetCollectionBlockerReason(collection, out _)) { continue; } List entries = new List(); HashSet usedIds = new HashSet(); if (collection.entries != null) { for (int entryIndex = 0; entryIndex < collection.entries.Count; entryIndex++) { WorldPrefabEntry entry = collection.entries[entryIndex]; if (WorldPlacementValidation.TryGetEntryBlockerReason(entry, usedIds, out _)) { continue; } entries.Add(new WorldPrefabEntryRuntime( entryIndex, entry.id, entry.weight, entry.spawnChancePercent, entry.placementMode, entry.footprint, entry.clearance, entry.flattenPadding, entry.flattenSearchRadius, entry.allowRotations, entry.prefab != null)); } } result.Add(new WorldPrefabCollectionRuntime( collectionIndex, Mathf.Max(0, collection.maxPlacementsPerChunk), Mathf.Max(1, collection.attemptsPerPlacement), Mathf.Max(0, collection.chunkEdgePadding), entries)); } return result.Count > 0 ? result : EmptyPlacementCollections; } } }