490 lines
19 KiB
C#
490 lines
19 KiB
C#
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
namespace InfiniteWorld.VoxelWorld
|
|
{
|
|
internal static class WorldSpawnPlanner
|
|
{
|
|
public static WorldChunkPlacementPlan PlanChunk(
|
|
int worldSeed,
|
|
Vector2Int chunkCoord,
|
|
int chunkSize,
|
|
IReadOnlyList<WorldPrefabCollection> collections,
|
|
System.Func<Vector2Int, int> getBaseHeight)
|
|
{
|
|
if (collections == null || collections.Count == 0)
|
|
{
|
|
return WorldChunkPlacementPlan.Empty;
|
|
}
|
|
|
|
List<WorldSpawnPoint> spawnPoints = new List<WorldSpawnPoint>();
|
|
List<WorldTerrainPatch> terrainPatches = new List<WorldTerrainPatch>();
|
|
HashSet<Vector2Int> flattenedCells = new HashSet<Vector2Int>();
|
|
HashSet<Vector2Int> occupiedCells = new HashSet<Vector2Int>();
|
|
int spawnOrdinal = 0;
|
|
|
|
for (int collectionIndex = 0; collectionIndex < collections.Count; collectionIndex++)
|
|
{
|
|
WorldPrefabCollection collection = collections[collectionIndex];
|
|
if (collection == null || collection.entries == null || collection.entries.Count == 0 || collection.maxPlacementsPerChunk <= 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
for (int placementIndex = 0; placementIndex < collection.maxPlacementsPerChunk; placementIndex++)
|
|
{
|
|
if (!TryPickEntry(collection, worldSeed, chunkCoord, collectionIndex, placementIndex, out int entryIndex, out WorldPrefabEntry entry))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!PassesSpawnChance(entry, worldSeed, chunkCoord, collectionIndex, placementIndex))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!TryPlanPlacement(worldSeed, chunkCoord, chunkSize, collection, collectionIndex, placementIndex, entryIndex, entry, spawnOrdinal, occupiedCells, flattenedCells, getBaseHeight, out WorldSpawnPoint spawnPoint, out WorldTerrainPatch patch, out List<Vector2Int> reservedCells))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
spawnPoints.Add(spawnPoint);
|
|
if (patch != null)
|
|
{
|
|
terrainPatches.Add(patch);
|
|
}
|
|
|
|
for (int i = 0; i < reservedCells.Count; i++)
|
|
{
|
|
occupiedCells.Add(reservedCells[i]);
|
|
}
|
|
|
|
spawnOrdinal++;
|
|
}
|
|
}
|
|
|
|
if (spawnPoints.Count == 0 && terrainPatches.Count == 0)
|
|
{
|
|
return WorldChunkPlacementPlan.Empty;
|
|
}
|
|
|
|
return new WorldChunkPlacementPlan(spawnPoints, terrainPatches, flattenedCells.Count > 0 ? flattenedCells : null);
|
|
}
|
|
|
|
private static bool TryPickEntry(WorldPrefabCollection collection, int worldSeed, Vector2Int chunkCoord, int collectionIndex, int placementIndex, out int entryIndex, out WorldPrefabEntry entry)
|
|
{
|
|
float totalWeight = 0f;
|
|
for (int i = 0; i < collection.entries.Count; i++)
|
|
{
|
|
WorldPrefabEntry candidate = collection.entries[i];
|
|
if (candidate == null || candidate.prefab == null || candidate.weight <= 0f)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
totalWeight += candidate.weight;
|
|
}
|
|
|
|
if (totalWeight <= 0f)
|
|
{
|
|
entryIndex = -1;
|
|
entry = null;
|
|
return false;
|
|
}
|
|
|
|
float pick = WorldSeedUtility.Value01(WorldSeedUtility.Hash(worldSeed, chunkCoord.x, chunkCoord.y, collectionIndex, placementIndex, 1)) * totalWeight;
|
|
float accumulated = 0f;
|
|
for (int i = 0; i < collection.entries.Count; i++)
|
|
{
|
|
WorldPrefabEntry candidate = collection.entries[i];
|
|
if (candidate == null || candidate.prefab == null || candidate.weight <= 0f)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
accumulated += candidate.weight;
|
|
if (pick <= accumulated)
|
|
{
|
|
entryIndex = i;
|
|
entry = candidate;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
for (int i = collection.entries.Count - 1; i >= 0; i--)
|
|
{
|
|
WorldPrefabEntry candidate = collection.entries[i];
|
|
if (candidate == null || candidate.prefab == null || candidate.weight <= 0f)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
entryIndex = i;
|
|
entry = candidate;
|
|
return true;
|
|
}
|
|
|
|
entryIndex = -1;
|
|
entry = null;
|
|
return false;
|
|
}
|
|
|
|
private static bool PassesSpawnChance(WorldPrefabEntry entry, int worldSeed, Vector2Int chunkCoord, int collectionIndex, int placementIndex)
|
|
{
|
|
if (entry == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (entry.spawnChancePercent <= 0f)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (entry.spawnChancePercent >= 100f)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
float roll = WorldSeedUtility.Value01(WorldSeedUtility.Hash(worldSeed, chunkCoord.x, chunkCoord.y, collectionIndex, placementIndex, 11)) * 100f;
|
|
return roll <= entry.spawnChancePercent;
|
|
}
|
|
|
|
private static bool TryPlanPlacement(
|
|
int worldSeed,
|
|
Vector2Int chunkCoord,
|
|
int chunkSize,
|
|
WorldPrefabCollection collection,
|
|
int collectionIndex,
|
|
int placementIndex,
|
|
int entryIndex,
|
|
WorldPrefabEntry entry,
|
|
int spawnOrdinal,
|
|
HashSet<Vector2Int> occupiedCells,
|
|
HashSet<Vector2Int> flattenedCells,
|
|
System.Func<Vector2Int, int> getBaseHeight,
|
|
out WorldSpawnPoint spawnPoint,
|
|
out WorldTerrainPatch patch,
|
|
out List<Vector2Int> reservedCells)
|
|
{
|
|
reservedCells = null;
|
|
spawnPoint = default;
|
|
patch = null;
|
|
|
|
Vector2Int chunkOrigin = new Vector2Int(chunkCoord.x * chunkSize, chunkCoord.y * chunkSize);
|
|
int attempts = Mathf.Max(1, collection.attemptsPerPlacement);
|
|
for (int attempt = 0; attempt < attempts; attempt++)
|
|
{
|
|
int rotationSteps = entry.allowRotations ? WorldSeedUtility.Range(WorldSeedUtility.Hash(worldSeed, chunkCoord.x, chunkCoord.y, collectionIndex, placementIndex, attempt, 2), 0, 4) : 0;
|
|
Vector2Int footprint = GetFootprint(entry.footprint, rotationSteps);
|
|
if (footprint.x <= 0 || footprint.y <= 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
int localMinX = collection.chunkEdgePadding;
|
|
int localMinZ = collection.chunkEdgePadding;
|
|
int localMaxX = chunkSize - collection.chunkEdgePadding - footprint.x;
|
|
int localMaxZ = chunkSize - collection.chunkEdgePadding - footprint.y;
|
|
if (localMaxX < localMinX || localMaxZ < localMinZ)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
int localX = WorldSeedUtility.Range(WorldSeedUtility.Hash(worldSeed, chunkCoord.x, chunkCoord.y, collectionIndex, placementIndex, attempt, 3), localMinX, localMaxX + 1);
|
|
int localZ = WorldSeedUtility.Range(WorldSeedUtility.Hash(worldSeed, chunkCoord.x, chunkCoord.y, collectionIndex, placementIndex, attempt, 4), localMinZ, localMaxZ + 1);
|
|
RectInt footprintRect = new RectInt(localX, localZ, footprint.x, footprint.y);
|
|
RectInt reservedRect = ExpandRect(footprintRect, Mathf.Max(0, entry.clearance), chunkSize);
|
|
|
|
if (IntersectsOccupied(chunkOrigin, reservedRect, occupiedCells))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (entry.placementMode == WorldPlacementMode.GroundOnly)
|
|
{
|
|
if (!IsGroundArea(chunkOrigin, reservedRect, flattenedCells, getBaseHeight))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
reservedCells = CollectCells(chunkOrigin, reservedRect);
|
|
}
|
|
else
|
|
{
|
|
if (!TryCreateFlattenPatch(chunkOrigin, chunkSize, footprintRect, reservedRect, entry, occupiedCells, flattenedCells, getBaseHeight, out patch, out reservedCells))
|
|
{
|
|
patch = null;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
float rotationY = rotationSteps * 90f;
|
|
Vector3 position = new Vector3(
|
|
chunkOrigin.x + footprintRect.x + footprintRect.width * 0.5f,
|
|
0f,
|
|
chunkOrigin.y + footprintRect.y + footprintRect.height * 0.5f);
|
|
long spawnId = WorldSeedUtility.ToStableId(
|
|
WorldSeedUtility.Hash(worldSeed, chunkCoord.x, chunkCoord.y, collectionIndex),
|
|
WorldSeedUtility.Hash(entryIndex, placementIndex, spawnOrdinal, footprintRect.x, footprintRect.y));
|
|
spawnPoint = new WorldSpawnPoint(spawnId, chunkCoord, spawnOrdinal, collectionIndex, entryIndex, entry, position, Quaternion.Euler(0f, rotationY, 0f));
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static bool TryCreateFlattenPatch(
|
|
Vector2Int chunkOrigin,
|
|
int chunkSize,
|
|
RectInt footprintRect,
|
|
RectInt reservedRect,
|
|
WorldPrefabEntry entry,
|
|
HashSet<Vector2Int> occupiedCells,
|
|
HashSet<Vector2Int> flattenedCells,
|
|
System.Func<Vector2Int, int> getBaseHeight,
|
|
out WorldTerrainPatch patch,
|
|
out List<Vector2Int> reservedCells)
|
|
{
|
|
int flattenPadding = Mathf.Max(entry.clearance, entry.flattenPadding);
|
|
RectInt flattenRect = ExpandRect(footprintRect, flattenPadding, chunkSize);
|
|
List<Vector2Int> flattened = CollectCells(chunkOrigin, flattenRect);
|
|
if (IntersectsOccupied(flattened, occupiedCells))
|
|
{
|
|
patch = null;
|
|
reservedCells = null;
|
|
return false;
|
|
}
|
|
|
|
List<Vector2Int> corridor = TryBuildAccessCorridor(chunkOrigin, chunkSize, flattenRect, Mathf.Max(1, entry.flattenSearchRadius), occupiedCells, flattenedCells, getBaseHeight);
|
|
if (corridor == null)
|
|
{
|
|
patch = null;
|
|
reservedCells = null;
|
|
return false;
|
|
}
|
|
|
|
for (int i = 0; i < corridor.Count; i++)
|
|
{
|
|
flattened.Add(corridor[i]);
|
|
}
|
|
|
|
HashSet<Vector2Int> uniqueFlattened = new HashSet<Vector2Int>(flattened);
|
|
foreach (Vector2Int worldCell in uniqueFlattened)
|
|
{
|
|
flattenedCells.Add(worldCell);
|
|
}
|
|
|
|
patch = new WorldTerrainPatch(flattened);
|
|
reservedCells = CollectCells(chunkOrigin, reservedRect);
|
|
reservedCells.AddRange(corridor);
|
|
return true;
|
|
}
|
|
|
|
private static List<Vector2Int> TryBuildAccessCorridor(
|
|
Vector2Int chunkOrigin,
|
|
int chunkSize,
|
|
RectInt flattenRect,
|
|
int searchRadius,
|
|
HashSet<Vector2Int> occupiedCells,
|
|
HashSet<Vector2Int> flattenedCells,
|
|
System.Func<Vector2Int, int> getBaseHeight)
|
|
{
|
|
if (HasAccessibleGroundNeighbor(chunkOrigin, flattenRect, flattenedCells, getBaseHeight))
|
|
{
|
|
return new List<Vector2Int>();
|
|
}
|
|
|
|
Vector2Int center = new Vector2Int(flattenRect.x + flattenRect.width / 2, flattenRect.y + flattenRect.height / 2);
|
|
Vector2Int? target = FindNearestGroundCell(chunkOrigin, chunkSize, center, flattenRect, searchRadius, occupiedCells, flattenedCells, getBaseHeight);
|
|
if (!target.HasValue)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
Vector2Int entryCell = new Vector2Int(
|
|
Mathf.Clamp(target.Value.x, chunkOrigin.x + flattenRect.xMin, chunkOrigin.x + flattenRect.xMax - 1),
|
|
Mathf.Clamp(target.Value.y, chunkOrigin.y + flattenRect.yMin, chunkOrigin.y + flattenRect.yMax - 1));
|
|
|
|
List<Vector2Int> corridor = new List<Vector2Int>();
|
|
Vector2Int cursor = target.Value;
|
|
while (cursor.x != entryCell.x)
|
|
{
|
|
if (!occupiedCells.Contains(cursor))
|
|
{
|
|
corridor.Add(cursor);
|
|
}
|
|
|
|
cursor.x += cursor.x < entryCell.x ? 1 : -1;
|
|
}
|
|
|
|
while (cursor.y != entryCell.y)
|
|
{
|
|
if (!occupiedCells.Contains(cursor))
|
|
{
|
|
corridor.Add(cursor);
|
|
}
|
|
|
|
cursor.y += cursor.y < entryCell.y ? 1 : -1;
|
|
}
|
|
|
|
return corridor;
|
|
}
|
|
|
|
private static bool HasAccessibleGroundNeighbor(Vector2Int chunkOrigin, RectInt flattenRect, HashSet<Vector2Int> flattenedCells, System.Func<Vector2Int, int> getBaseHeight)
|
|
{
|
|
for (int z = flattenRect.yMin - 1; z <= flattenRect.yMax; z++)
|
|
{
|
|
Vector2Int left = new Vector2Int(chunkOrigin.x + flattenRect.xMin - 1, chunkOrigin.y + z);
|
|
Vector2Int right = new Vector2Int(chunkOrigin.x + flattenRect.xMax, chunkOrigin.y + z);
|
|
if (GetPlannedHeight(left, flattenedCells, getBaseHeight) == 0 || GetPlannedHeight(right, flattenedCells, getBaseHeight) == 0)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
for (int x = flattenRect.xMin; x < flattenRect.xMax; x++)
|
|
{
|
|
Vector2Int bottom = new Vector2Int(chunkOrigin.x + x, chunkOrigin.y + flattenRect.yMin - 1);
|
|
Vector2Int top = new Vector2Int(chunkOrigin.x + x, chunkOrigin.y + flattenRect.yMax);
|
|
if (GetPlannedHeight(bottom, flattenedCells, getBaseHeight) == 0 || GetPlannedHeight(top, flattenedCells, getBaseHeight) == 0)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static Vector2Int? FindNearestGroundCell(
|
|
Vector2Int chunkOrigin,
|
|
int chunkSize,
|
|
Vector2Int localCenter,
|
|
RectInt excludedRect,
|
|
int searchRadius,
|
|
HashSet<Vector2Int> occupiedCells,
|
|
HashSet<Vector2Int> flattenedCells,
|
|
System.Func<Vector2Int, int> getBaseHeight)
|
|
{
|
|
for (int radius = 1; radius <= searchRadius; radius++)
|
|
{
|
|
for (int dz = -radius; dz <= radius; dz++)
|
|
{
|
|
for (int dx = -radius; dx <= radius; dx++)
|
|
{
|
|
if (Mathf.Max(Mathf.Abs(dx), Mathf.Abs(dz)) != radius)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
int localX = localCenter.x + dx;
|
|
int localZ = localCenter.y + dz;
|
|
if (localX < 0 || localZ < 0 || localX >= chunkSize || localZ >= chunkSize)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (excludedRect.Contains(new Vector2Int(localX, localZ)))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
Vector2Int worldCell = new Vector2Int(chunkOrigin.x + localX, chunkOrigin.y + localZ);
|
|
if (occupiedCells.Contains(worldCell))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (GetPlannedHeight(worldCell, flattenedCells, getBaseHeight) == 0)
|
|
{
|
|
return worldCell;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static int GetPlannedHeight(Vector2Int worldCell, HashSet<Vector2Int> flattenedCells, System.Func<Vector2Int, int> getBaseHeight)
|
|
{
|
|
return flattenedCells.Contains(worldCell) ? 0 : getBaseHeight(worldCell);
|
|
}
|
|
|
|
private static bool IsGroundArea(Vector2Int chunkOrigin, RectInt rect, HashSet<Vector2Int> flattenedCells, System.Func<Vector2Int, int> getBaseHeight)
|
|
{
|
|
for (int z = rect.yMin; z < rect.yMax; z++)
|
|
{
|
|
for (int x = rect.xMin; x < rect.xMax; x++)
|
|
{
|
|
if (GetPlannedHeight(new Vector2Int(chunkOrigin.x + x, chunkOrigin.y + z), flattenedCells, getBaseHeight) > 0)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static bool IntersectsOccupied(Vector2Int chunkOrigin, RectInt rect, HashSet<Vector2Int> occupiedCells)
|
|
{
|
|
for (int z = rect.yMin; z < rect.yMax; z++)
|
|
{
|
|
for (int x = rect.xMin; x < rect.xMax; x++)
|
|
{
|
|
if (occupiedCells.Contains(new Vector2Int(chunkOrigin.x + x, chunkOrigin.y + z)))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static bool IntersectsOccupied(List<Vector2Int> cells, HashSet<Vector2Int> occupiedCells)
|
|
{
|
|
for (int i = 0; i < cells.Count; i++)
|
|
{
|
|
if (occupiedCells.Contains(cells[i]))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static List<Vector2Int> CollectCells(Vector2Int chunkOrigin, RectInt rect)
|
|
{
|
|
List<Vector2Int> result = new List<Vector2Int>(rect.width * rect.height);
|
|
for (int z = rect.yMin; z < rect.yMax; z++)
|
|
{
|
|
for (int x = rect.xMin; x < rect.xMax; x++)
|
|
{
|
|
result.Add(new Vector2Int(chunkOrigin.x + x, chunkOrigin.y + z));
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static RectInt ExpandRect(RectInt rect, int amount, int chunkSize)
|
|
{
|
|
return new RectInt(
|
|
Mathf.Max(0, rect.xMin - amount),
|
|
Mathf.Max(0, rect.yMin - amount),
|
|
Mathf.Min(chunkSize, rect.xMax + amount) - Mathf.Max(0, rect.xMin - amount),
|
|
Mathf.Min(chunkSize, rect.yMax + amount) - Mathf.Max(0, rect.yMin - amount));
|
|
}
|
|
|
|
private static Vector2Int GetFootprint(Vector2Int footprint, int rotationSteps)
|
|
{
|
|
footprint.x = Mathf.Max(1, footprint.x);
|
|
footprint.y = Mathf.Max(1, footprint.y);
|
|
return rotationSteps % 2 == 0 ? footprint : new Vector2Int(footprint.y, footprint.x);
|
|
}
|
|
}
|
|
}
|