using System.Collections.Generic; using System.Text; using UnityEngine; namespace FloodSWE.TileGraph { public sealed class LodTileManager : MonoBehaviour { [Header("LOD")] public int minLod = 1; public int maxLod = 4; public float baseTileSizeMeters = 1000f; public int maxActiveTiles = 128; [Tooltip("LOD0 is disabled by default because L0/L1 share the same world size in the plan.")] public bool enableLod0 = false; [Header("Test Grid")] public bool buildTestGridOnStart = true; public int testGridLod = 1; public Vector2Int testGridSize = new Vector2Int(4, 4); public int refineSteps = 2; public int refinePerStep = 1; public int randomSeed = 1; [Header("Debug")] public bool logActiveTiles = true; public bool logAdjacencyFixes = false; public bool logBudget = false; private const float AdjacencyEpsilon = 0.001f; private readonly Dictionary nodes = new Dictionary(); private readonly List scratchActive = new List(); private readonly List scratchSorted = new List(); private void Start() { NormalizeLodSettings(); if (!buildTestGridOnStart) { return; } BuildTestGrid(); AssignPriorityByDistance(); RefineByPriority(refineSteps, refinePerStep); EnforceAdjacency(); EnforceBudget(); EnforceAdjacency(); if (logActiveTiles) { LogActiveTilesPerLod(); } } public void BuildTestGrid() { nodes.Clear(); NormalizeLodSettings(); testGridLod = Mathf.Clamp(testGridLod, minLod, maxLod); testGridSize = new Vector2Int(Mathf.Max(1, testGridSize.x), Mathf.Max(1, testGridSize.y)); for (int y = 0; y < testGridSize.y; y++) { for (int x = 0; x < testGridSize.x; x++) { TileId id = new TileId(testGridLod, x, y); TileNode node = GetOrCreateNode(id); node.Active = true; node.Priority = 0.0f; } } } public void AssignPriorityByDistance() { GetActiveLeafNodes(scratchActive); if (scratchActive.Count == 0) { return; } float centerX = 0.0f; float centerY = 0.0f; for (int i = 0; i < scratchActive.Count; i++) { centerX += scratchActive[i].Id.X; centerY += scratchActive[i].Id.Y; } centerX /= scratchActive.Count; centerY /= scratchActive.Count; for (int i = 0; i < scratchActive.Count; i++) { TileNode node = scratchActive[i]; float dx = node.Id.X - centerX; float dy = node.Id.Y - centerY; float dist = Mathf.Sqrt(dx * dx + dy * dy); node.Priority = 1.0f / (1.0f + dist); } } public void RefineByPriority(int steps, int perStep) { steps = Mathf.Max(0, steps); perStep = Mathf.Max(0, perStep); for (int step = 0; step < steps; step++) { GetActiveLeafNodes(scratchActive); if (scratchActive.Count == 0) { return; } scratchSorted.Clear(); scratchSorted.AddRange(scratchActive); scratchSorted.Sort((a, b) => b.Priority.CompareTo(a.Priority)); int refineCount = Mathf.Min(perStep, scratchSorted.Count); for (int i = 0; i < refineCount; i++) { Refine(scratchSorted[i]); } } } public void EnforceAdjacency() { int guard = 0; while (guard < 200) { guard++; bool changed = false; GetActiveLeafNodes(scratchActive); for (int i = 0; i < scratchActive.Count && !changed; i++) { TileNode a = scratchActive[i]; for (int j = i + 1; j < scratchActive.Count; j++) { TileNode b = scratchActive[j]; if (!AreAdjacent(a, b)) { continue; } int diff = Mathf.Abs(a.Id.Lod - b.Id.Lod); if (diff <= 1) { continue; } TileNode coarse = a.Id.Lod < b.Id.Lod ? a : b; if (Refine(coarse)) { if (logAdjacencyFixes) { TileNode fine = coarse == a ? b : a; Debug.Log($"LOD adjacency fix: refined {coarse.Id} next to {fine.Id}."); } changed = true; break; } } } if (!changed) { break; } } } public void EnforceBudget() { if (maxActiveTiles <= 0) { return; } int guard = 0; while (CountActiveLeaves() > maxActiveTiles && guard < 200) { guard++; if (!TryCoarsenLowestPriority()) { break; } } } public void LogActiveTilesPerLod() { int lodCount = Mathf.Max(1, maxLod - minLod + 1); int[] counts = new int[lodCount]; foreach (TileNode node in nodes.Values) { if (!node.Active || node.HasActiveChildren) { continue; } int index = node.Id.Lod - minLod; if (index >= 0 && index < counts.Length) { counts[index]++; } } StringBuilder builder = new StringBuilder("Active tiles per LOD: "); for (int i = 0; i < counts.Length; i++) { int lod = minLod + i; builder.Append($"L{lod}={counts[i]}"); if (i < counts.Length - 1) { builder.Append(" "); } } Debug.Log(builder.ToString()); } private TileNode GetOrCreateNode(TileId id) { if (!nodes.TryGetValue(id, out TileNode node)) { node = new TileNode(id, GetTileSizeMeters(id.Lod), GetGridRes(id.Lod)); nodes.Add(id, node); } return node; } private float GetTileSizeMeters(int lod) { if (lod <= 1) { return Mathf.Max(0.01f, baseTileSizeMeters); } float size = baseTileSizeMeters / Mathf.Pow(2.0f, lod - 1); return Mathf.Max(0.01f, size); } private int GetGridRes(int lod) { return lod == 0 ? 128 : 256; } private void GetActiveLeafNodes(List results) { results.Clear(); foreach (TileNode node in nodes.Values) { if (node.Active && !node.HasActiveChildren) { results.Add(node); } } } private int CountActiveLeaves() { int count = 0; foreach (TileNode node in nodes.Values) { if (node.Active && !node.HasActiveChildren) { count++; } } return count; } private bool Refine(TileNode node) { if (node == null || !node.Active) { return false; } if (node.Id.Lod >= maxLod) { return false; } if (node.Id.Lod == 0) { if (enableLod0) { Debug.LogWarning("LodTileManager: L0->L1 refinement is not implemented. Start at L1 or disable LOD0."); } return false; } if (node.Children == null || node.Children.Length != 4) { node.Children = new TileNode[4]; } for (int childY = 0; childY < 2; childY++) { for (int childX = 0; childX < 2; childX++) { int index = childY * 2 + childX; TileId childId = node.Id.Child(childX, childY); TileNode child = GetOrCreateNode(childId); child.Parent = node; child.Active = true; child.Priority = node.Priority; node.Children[index] = child; } } node.Active = false; return true; } private bool TryCoarsenLowestPriority() { CoarsenCandidate best = default; bool hasCandidate = false; foreach (TileNode node in nodes.Values) { if (node.Id.Lod < minLod) { continue; } if (!node.HasChildren || !AreChildrenActiveLeaves(node)) { continue; } float priority = 0.0f; for (int i = 0; i < node.Children.Length; i++) { priority += node.Children[i].Priority; } priority /= node.Children.Length; if (!hasCandidate || priority < best.Priority) { best = new CoarsenCandidate(node, priority); hasCandidate = true; } } if (!hasCandidate) { return false; } bool result = Coarsen(best.Parent, best.Priority); if (result && logBudget) { Debug.Log($"Budget coarsen: {best.Parent.Id} priority={best.Priority:F3}."); } return result; } private bool Coarsen(TileNode parent, float priority) { if (parent == null || !parent.HasChildren) { return false; } for (int i = 0; i < parent.Children.Length; i++) { TileNode child = parent.Children[i]; if (child == null || !child.Active || child.HasActiveChildren) { return false; } } for (int i = 0; i < parent.Children.Length; i++) { parent.Children[i].Active = false; } parent.Active = true; parent.Priority = priority; return true; } private void NormalizeLodSettings() { if (!enableLod0 && minLod < 1) { minLod = 1; } if (!enableLod0 && testGridLod < 1) { testGridLod = 1; } if (maxLod < minLod) { maxLod = minLod; } } private bool AreChildrenActiveLeaves(TileNode parent) { if (!parent.HasChildren) { return false; } for (int i = 0; i < parent.Children.Length; i++) { TileNode child = parent.Children[i]; if (child == null || !child.Active || child.HasActiveChildren) { return false; } } return true; } private bool AreAdjacent(TileNode a, TileNode b) { GetBounds(a, out float aMinX, out float aMaxX, out float aMinY, out float aMaxY); GetBounds(b, out float bMinX, out float bMaxX, out float bMinY, out float bMaxY); bool xOverlap = Overlaps(aMinX, aMaxX, bMinX, bMaxX); bool yOverlap = Overlaps(aMinY, aMaxY, bMinY, bMaxY); bool northSouth = xOverlap && (Mathf.Abs(aMaxY - bMinY) <= AdjacencyEpsilon || Mathf.Abs(aMinY - bMaxY) <= AdjacencyEpsilon); bool eastWest = yOverlap && (Mathf.Abs(aMaxX - bMinX) <= AdjacencyEpsilon || Mathf.Abs(aMinX - bMaxX) <= AdjacencyEpsilon); return northSouth || eastWest; } private void GetBounds(TileNode node, out float minX, out float maxX, out float minY, out float maxY) { float size = GetTileSizeMeters(node.Id.Lod); minX = node.Id.X * size; maxX = minX + size; minY = node.Id.Y * size; maxY = minY + size; } private bool Overlaps(float aMin, float aMax, float bMin, float bMax) { return aMin < bMax - AdjacencyEpsilon && aMax > bMin + AdjacencyEpsilon; } private readonly struct CoarsenCandidate { public readonly TileNode Parent; public readonly float Priority; public CoarsenCandidate(TileNode parent, float priority) { Parent = parent; Priority = priority; } } } }