Добавить детерминированное размещение dungeon prefab в voxel-мире через stamp/carve в данных чанков. #6
@@ -19,20 +19,10 @@ MonoBehaviour:
|
||||
- id: EntranceCrypt
|
||||
prefab: {fileID: 155468, guid: ea0c7071e67bf1c43940a8ab3ea121f8, type: 3}
|
||||
weight: 10
|
||||
spawnChancePercent: 100
|
||||
spawnChancePercent: 15
|
||||
placementMode: 1
|
||||
footprint: {x: 2, y: 2}
|
||||
clearance: 4
|
||||
flattenPadding: 4
|
||||
flattenSearchRadius: 4
|
||||
allowRotations: 1
|
||||
- id: GlowingOrb
|
||||
prefab: {fileID: 190932, guid: 20e0bf298681fcd4693097c822277593, type: 3}
|
||||
weight: 0
|
||||
spawnChancePercent: 100
|
||||
placementMode: 0
|
||||
footprint: {x: 2, y: 2}
|
||||
clearance: 4
|
||||
flattenPadding: 4
|
||||
flattenSearchRadius: 4
|
||||
allowRotations: 1
|
||||
|
||||
@@ -199,18 +199,19 @@ namespace InfiniteWorld.VoxelWorld
|
||||
for (int collectionIndex = 0; collectionIndex < config.placementCollections.Count; collectionIndex++)
|
||||
{
|
||||
WorldPrefabCollection collection = config.placementCollections[collectionIndex];
|
||||
if (collection == null)
|
||||
if (WorldPlacementValidation.TryGetCollectionBlockerReason(collection, out _))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
List<WorldPrefabEntryRuntime> entries = new List<WorldPrefabEntryRuntime>();
|
||||
HashSet<string> usedIds = new HashSet<string>();
|
||||
if (collection.entries != null)
|
||||
{
|
||||
for (int entryIndex = 0; entryIndex < collection.entries.Count; entryIndex++)
|
||||
{
|
||||
WorldPrefabEntry entry = collection.entries[entryIndex];
|
||||
if (entry == null)
|
||||
if (WorldPlacementValidation.TryGetEntryBlockerReason(entry, usedIds, out _))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace InfiniteWorld.VoxelWorld
|
||||
{
|
||||
private readonly Dictionary<Vector2Int, WorldChunkPlacementPlan> chunkPlacementPlans = new Dictionary<Vector2Int, WorldChunkPlacementPlan>();
|
||||
private readonly object placementPlanLock = new object();
|
||||
private int placementValidationSignature = int.MinValue;
|
||||
private IReadOnlyList<WorldPrefabCollectionRuntime> placementCollections => settings.PlacementCollections;
|
||||
|
||||
public WorldChunkPlacementPlan GetChunkPlacementPlan(Vector2Int chunkCoord)
|
||||
@@ -88,5 +89,105 @@ namespace InfiniteWorld.VoxelWorld
|
||||
chunkPlacementPlans.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidatePlacementCollectionsIfNeeded()
|
||||
{
|
||||
int signature = ComputePlacementValidationSignature();
|
||||
if (signature == placementValidationSignature)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
placementValidationSignature = signature;
|
||||
ValidatePlacementCollections();
|
||||
}
|
||||
|
||||
private int ComputePlacementValidationSignature()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
int hash = 17;
|
||||
hash = hash * 31 + (config != null ? config.GetInstanceID() : 0);
|
||||
if (config == null || config.placementCollections == null)
|
||||
{
|
||||
return hash;
|
||||
}
|
||||
|
||||
hash = hash * 31 + config.placementCollections.Count;
|
||||
for (int collectionIndex = 0; collectionIndex < config.placementCollections.Count; collectionIndex++)
|
||||
{
|
||||
WorldPrefabCollection collection = config.placementCollections[collectionIndex];
|
||||
hash = hash * 31 + (collection != null ? collection.GetInstanceID() : 0);
|
||||
if (collection == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
hash = hash * 31 + collection.maxPlacementsPerChunk;
|
||||
hash = hash * 31 + collection.attemptsPerPlacement;
|
||||
hash = hash * 31 + collection.chunkEdgePadding;
|
||||
hash = hash * 31 + (collection.entries != null ? collection.entries.Count : 0);
|
||||
if (collection.entries == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int entryIndex = 0; entryIndex < collection.entries.Count; entryIndex++)
|
||||
{
|
||||
WorldPrefabEntry entry = collection.entries[entryIndex];
|
||||
hash = hash * 31 + (entry != null ? entry.id?.GetHashCode() ?? 0 : 0);
|
||||
if (entry == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
hash = hash * 31 + (entry.prefab != null ? entry.prefab.GetInstanceID() : 0);
|
||||
hash = hash * 31 + entry.weight.GetHashCode();
|
||||
hash = hash * 31 + entry.spawnChancePercent.GetHashCode();
|
||||
hash = hash * 31 + (int)entry.placementMode;
|
||||
hash = hash * 31 + entry.footprint.x;
|
||||
hash = hash * 31 + entry.footprint.y;
|
||||
hash = hash * 31 + entry.clearance;
|
||||
hash = hash * 31 + entry.flattenPadding;
|
||||
hash = hash * 31 + entry.flattenSearchRadius;
|
||||
hash = hash * 31 + (entry.allowRotations ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidatePlacementCollections()
|
||||
{
|
||||
if (config == null || config.placementCollections == null || config.placementCollections.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int collectionIndex = 0; collectionIndex < config.placementCollections.Count; collectionIndex++)
|
||||
{
|
||||
WorldPrefabCollection collection = config.placementCollections[collectionIndex];
|
||||
string collectionName = collection != null ? collection.name : $"Collection[{collectionIndex}]";
|
||||
if (WorldPlacementValidation.TryGetCollectionBlockerReason(collection, out string collectionReason))
|
||||
{
|
||||
Debug.LogError($"[VoxelWorld] Collection '{collectionName}' will not spawn: {collectionReason}", collection != null ? collection : this);
|
||||
continue;
|
||||
}
|
||||
|
||||
HashSet<string> usedIds = new HashSet<string>();
|
||||
for (int entryIndex = 0; entryIndex < collection.entries.Count; entryIndex++)
|
||||
{
|
||||
WorldPrefabEntry entry = collection.entries[entryIndex];
|
||||
string entryId = entry != null && !string.IsNullOrWhiteSpace(entry.id) ? entry.id : $"entry #{entryIndex}";
|
||||
if (!WorldPlacementValidation.TryGetEntryBlockerReason(entry, usedIds, out string entryReason))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Debug.LogError($"[VoxelWorld] Collection '{collectionName}' entry #{entryIndex} ('{entryId}') will not spawn: {entryReason}", collection);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +131,7 @@ namespace InfiniteWorld.VoxelWorld
|
||||
private void EnsureRuntimeData()
|
||||
{
|
||||
settings = VoxelWorldResolvedSettings.Resolve(config);
|
||||
ValidatePlacementCollectionsIfNeeded();
|
||||
int configuredBiomeCount = CountConfiguredBiomes();
|
||||
|
||||
if (atlas != null && atlasBiomeCount == configuredBiomeCount)
|
||||
|
||||
@@ -106,6 +106,111 @@ namespace InfiniteWorld.VoxelWorld
|
||||
public bool HasPrefab { get; }
|
||||
}
|
||||
|
||||
internal static class WorldPlacementValidation
|
||||
{
|
||||
public static bool TryGetCollectionBlockerReason(WorldPrefabCollection collection, out string reason)
|
||||
{
|
||||
if (collection == null)
|
||||
{
|
||||
reason = "collection reference is missing.";
|
||||
return true;
|
||||
}
|
||||
|
||||
if (collection.maxPlacementsPerChunk <= 0)
|
||||
{
|
||||
reason = "max placements per chunk must be greater than 0.";
|
||||
return true;
|
||||
}
|
||||
|
||||
if (collection.attemptsPerPlacement < 1)
|
||||
{
|
||||
reason = "attempts per placement must be at least 1.";
|
||||
return true;
|
||||
}
|
||||
|
||||
if (collection.chunkEdgePadding < 0)
|
||||
{
|
||||
reason = "chunk edge padding cannot be negative.";
|
||||
return true;
|
||||
}
|
||||
|
||||
if (collection.entries == null || collection.entries.Count == 0)
|
||||
{
|
||||
reason = "collection has no entries configured.";
|
||||
return true;
|
||||
}
|
||||
|
||||
reason = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool TryGetEntryBlockerReason(WorldPrefabEntry entry, HashSet<string> usedIds, out string reason)
|
||||
{
|
||||
if (entry == null)
|
||||
{
|
||||
reason = "entry reference is missing.";
|
||||
return true;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(entry.id))
|
||||
{
|
||||
reason = "id is empty. A stable id is required for deterministic spawns and save data.";
|
||||
return true;
|
||||
}
|
||||
|
||||
if (usedIds != null && !usedIds.Add(entry.id))
|
||||
{
|
||||
reason = $"duplicate id '{entry.id}' in the same collection.";
|
||||
return true;
|
||||
}
|
||||
|
||||
if (entry.prefab == null)
|
||||
{
|
||||
reason = "prefab is missing.";
|
||||
return true;
|
||||
}
|
||||
|
||||
if (entry.weight <= 0f)
|
||||
{
|
||||
reason = "weight must be greater than 0.";
|
||||
return true;
|
||||
}
|
||||
|
||||
if (entry.spawnChancePercent <= 0f)
|
||||
{
|
||||
reason = "spawn chance must be greater than 0%.";
|
||||
return true;
|
||||
}
|
||||
|
||||
if (entry.footprint.x <= 0 || entry.footprint.y <= 0)
|
||||
{
|
||||
reason = "footprint must be at least 1x1.";
|
||||
return true;
|
||||
}
|
||||
|
||||
if (entry.clearance < 0)
|
||||
{
|
||||
reason = "clearance cannot be negative.";
|
||||
return true;
|
||||
}
|
||||
|
||||
if (entry.flattenPadding < 0)
|
||||
{
|
||||
reason = "flatten padding cannot be negative.";
|
||||
return true;
|
||||
}
|
||||
|
||||
if (entry.placementMode == WorldPlacementMode.FlattenTerrain && entry.flattenSearchRadius < 1)
|
||||
{
|
||||
reason = "flatten search radius must be at least 1 for FlattenTerrain mode.";
|
||||
return true;
|
||||
}
|
||||
|
||||
reason = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class WorldTerrainPatch
|
||||
{
|
||||
private readonly List<Vector2Int> flattenedCells;
|
||||
|
||||
Reference in New Issue
Block a user