Загрузка данных
public class HexInsideHexGenerator : IInitializable
{
private const float Sqrt3 = 1.7320508f;
private const float HexApothemFactor = Sqrt3 * 0.5f;
private const float ScoreEpsilon = 0.0001f;
private readonly Vector2Int[] AxialDirections =
{
new Vector2Int(1, 0),
new Vector2Int(1, -1),
new Vector2Int(0, -1),
new Vector2Int(-1, 0),
new Vector2Int(-1, 1),
new Vector2Int(0, 1)
};
private readonly BusinesCapabilitiesFragmentFactory fragmentFactory;
private readonly MainConfigHolder mainConfigHolder;
private readonly DataManager dataManager;
private BusinessCapabilitiesInDepartmentConfig config;
public HexInsideHexGenerator(BusinesCapabilitiesFragmentFactory fragmentFactory,
MainConfigHolder mainConfigHolder, DataManager dataManager)
{
this.fragmentFactory = fragmentFactory;
this.mainConfigHolder = mainConfigHolder;
this.dataManager = dataManager;
}
public void Initialize()
{
config = mainConfigHolder.BusinessCapabilitiesInDepartmentConfig;
}
public List<FbcDataFragment> Generate(int count, List<long> ids, FbcDataFragment prefab, GameObject emptyPrefab,
Transform root, Transform outerHex)
{
if (prefab == null)
{
Debug.LogWarning("inner hex prefab is not assigned.");
return new List<FbcDataFragment>();
}
if (ids == null || ids.Count == 0)
{
Debug.LogWarning("ids is empty");
return new List<FbcDataFragment>();
}
if (count <= 0)
{
Debug.LogWarning("generation count is zero.");
return new List<FbcDataFragment>();
}
if (root == null)
{
Debug.LogWarning("cannot resolve generated root.");
return new List<FbcDataFragment>();
}
if (outerHex == null)
{
Debug.LogWarning("outer hex is not assigned.");
return new List<FbcDataFragment>();
}
count = Mathf.Min(count, ids.Count);
ClearChildren(root, prefab, emptyPrefab);
if (!TryCalculateLocalBounds(outerHex, outerHex, out Bounds outerLocalBounds, root))
{
Debug.LogWarning("cannot calculate outer bounds.");
return new List<FbcDataFragment>();
}
if (!TryCalculatePrefabLocalBounds(prefab.gameObject, outerHex, out Bounds innerLocalBounds))
{
Debug.LogWarning("cannot calculate inner prefab bounds.");
return new List<FbcDataFragment>();
}
float outerRadius = ResolveHexRadius(outerLocalBounds);
float innerBaseRadius = ResolveHexRadius(innerLocalBounds);
float effectiveOuterRadius = ApplyPaddingRatioToHexRadius(outerRadius, config.InHexPadding);
if (outerRadius <= 0f)
{
Debug.LogWarning("outer radius is zero.");
return new List<FbcDataFragment>();
}
if (effectiveOuterRadius <= 0f)
{
Debug.LogWarning("effective outer radius is zero. Reduce outer padding ratio.");
return new List<FbcDataFragment>();
}
if (innerBaseRadius <= 0f)
{
Debug.LogWarning("inner radius is zero.");
return new List<FbcDataFragment>();
}
// Большие гексы формируются процедурно. Слои ориентированы так же,
// как внешний flat-top гекс. Для неполного слоя выбирается наиболее
// компактное продолжение текущей фигуры.
List<Vector2Int> cells = BuildOuterAlignedCells(count, config.InHexGapRatio);
float finalScale = FindMaximumFittingScale(
cells,
effectiveOuterRadius,
innerBaseRadius,
config.InHexGapRatio);
float finalInnerRadius = innerBaseRadius * finalScale;
float gap = CalculateGapUnits(finalInnerRadius, config.InHexGapRatio);
float pitchRadius = finalInnerRadius + gap / Sqrt3;
Vector2 outerCenterXZ = GetXZPosition(outerLocalBounds.center);
Vector2 groupCenterXZ = CalculateCellsCenterXZ(cells, pitchRadius);
float outerTopWorldY = CalculateWorldMaxY(outerLocalBounds, outerHex);
if (!TryResolveScaledPrefabLocalY(
prefab.gameObject,
finalScale,
root,
outerTopWorldY,
out float largeHexLocalY))
{
Debug.LogWarning("cannot resolve inner prefab Y position.");
return new List<FbcDataFragment>();
}
List<FbcDataFragment> results = new List<FbcDataFragment>(cells.Count);
for (int i = 0; i < cells.Count; i++)
{
Vector2 localXZ = AxialToXZ(cells[i], pitchRadius) - groupCenterXZ;
Vector2 finalXZ = outerCenterXZ + localXZ;
FbcDataFragment fragment = fragmentFactory.CreateFBC(
prefab,
new Vector3(finalXZ.x, config.InHexSurfaceOffset, finalXZ.y),
prefab.transform.localRotation,
finalScale,
root,
ids[i]);
fragment.name = $"{prefab.name} {i}";
fragment.transform.localPosition = new Vector3(finalXZ.x, largeHexLocalY, finalXZ.y);
fragment.transform.localRotation = prefab.transform.localRotation;
fragment.transform.localScale = prefab.transform.localScale * finalScale;
results.Add(fragment);
}
SpawnDecorativeHexes(
cells,
emptyPrefab,
root,
outerCenterXZ,
groupCenterXZ,
pitchRadius,
finalScale,
outerTopWorldY);
return results;
}
private void SpawnDecorativeHexes(
IReadOnlyList<Vector2Int> largeCells,
GameObject emptyPrefab,
Transform root,
Vector2 outerCenterXZ,
Vector2 largeGroupCenterXZ,
float pitchRadius,
float finalScale,
float outerTopWorldY)
{
// Для одного большого гекса декоративные элементы не создаются.
if (largeCells == null || largeCells.Count <= 1)
return;
if (emptyPrefab == null)
{
Debug.LogWarning("empty prefab is not assigned. Decorative hexes were not generated.");
return;
}
if (!TryResolveScaledPrefabLocalY(
emptyPrefab,
finalScale,
root,
outerTopWorldY,
out float emptyHexLocalY))
{
Debug.LogWarning("cannot resolve decorative prefab Y position.");
return;
}
List<Vector2Int> decorativeCells = BuildDecorativeCells(largeCells, pitchRadius);
for (int i = 0; i < decorativeCells.Count; i++)
{
Vector2 localXZ = AxialToXZ(decorativeCells[i], pitchRadius) - largeGroupCenterXZ;
Vector2 finalXZ = outerCenterXZ + localXZ;
GameObject decorativeHex = UnityEngine.Object.Instantiate(emptyPrefab, root, false);
decorativeHex.name = $"{emptyPrefab.name} {i}";
decorativeHex.transform.localPosition = new Vector3(finalXZ.x, emptyHexLocalY, finalXZ.y);
decorativeHex.transform.localRotation = emptyPrefab.transform.localRotation;
decorativeHex.transform.localScale = emptyPrefab.transform.localScale * finalScale;
decorativeHex.SetActive(true);
}
}
private List<Vector2Int> BuildDecorativeCells(
IReadOnlyList<Vector2Int> largeCells,
float pitchRadius)
{
HashSet<Vector2Int> occupied = new HashSet<Vector2Int>();
for (int i = 0; i < largeCells.Count; i++)
occupied.Add(largeCells[i]);
HashSet<Vector2Int> candidates = new HashSet<Vector2Int>();
foreach (Vector2Int cell in occupied)
{
for (int directionIndex = 0; directionIndex < AxialDirections.Length; directionIndex++)
{
Vector2Int neighbour = cell + AxialDirections[directionIndex];
if (!occupied.Contains(neighbour))
candidates.Add(neighbour);
}
}
List<Vector2Int> result = new List<Vector2Int>();
foreach (Vector2Int candidate in candidates)
{
int occupiedNeighbourCount = CountOccupiedNeighbours(candidate, occupied);
// Маленький гекс появляется только в свободной ячейке,
// которая касается минимум двух больших гексов.
if (occupiedNeighbourCount >= 2)
result.Add(candidate);
}
// HashSet не гарантирует порядок. Сортировка делает генерацию стабильной.
result.Sort((left, right) => CompareCellsByAngle(left, right, pitchRadius));
return result;
}
private int CountOccupiedNeighbours(Vector2Int cell, HashSet<Vector2Int> occupied)
{
int count = 0;
for (int directionIndex = 0; directionIndex < AxialDirections.Length; directionIndex++)
{
if (occupied.Contains(cell + AxialDirections[directionIndex]))
count++;
}
return count;
}
private void ClearChildren(Transform root, FbcDataFragment prefab, GameObject emptyPrefab)
{
for (int childIndex = root.childCount - 1; childIndex >= 0; childIndex--)
{
GameObject target = root.GetChild(childIndex).gameObject;
if (prefab != null && prefab.gameObject == target)
continue;
if (emptyPrefab != null && emptyPrefab == target)
continue;
DestroySmart(target);
}
}
private void DestroySmart(Object target)
{
if (target == null)
return;
if (Application.isPlaying)
Object.Destroy(target);
else
Object.DestroyImmediate(target);
}
private bool TryResolveScaledPrefabLocalY(
GameObject sourcePrefab,
float scale,
Transform root,
float outerTopWorldY,
out float localY)
{
localY = 0f;
if (sourcePrefab == null || root == null)
return false;
GameObject probe = UnityEngine.Object.Instantiate(sourcePrefab, root, false);
probe.name = $"{sourcePrefab.name}_YProbe";
probe.SetActive(true);
probe.transform.localScale *= scale;
Bounds prefabBounds = ObjectToFrustumFitter.GetBoundsWithChildren(probe);
if (prefabBounds.size == Vector3.zero)
{
DestroySmart(probe);
return false;
}
// Сохраняет исходную логику вертикального размещения:
// верх префаба совмещается с верхней поверхностью внешнего объекта
// с учётом высоты модели и её исходного pivot.
probe.transform.SetY(outerTopWorldY - prefabBounds.size.y);
localY = probe.transform.localPosition.y;
DestroySmart(probe);
return true;
}
private float CalculateWorldMaxY(Bounds localBounds, Transform relativeTo)
{
Vector3 min = localBounds.min;
Vector3 max = localBounds.max;
float maxWorldY = float.MinValue;
for (int xIndex = 0; xIndex <= 1; xIndex++)
{
for (int yIndex = 0; yIndex <= 1; yIndex++)
{
for (int zIndex = 0; zIndex <= 1; zIndex++)
{
Vector3 localCorner = new Vector3(
xIndex == 0 ? min.x : max.x,
yIndex == 0 ? min.y : max.y,
zIndex == 0 ? min.z : max.z);
float worldY = relativeTo.TransformPoint(localCorner).y;
maxWorldY = Mathf.Max(maxWorldY, worldY);
}
}
}
return maxWorldY;
}
private bool TryCalculatePrefabLocalBounds(GameObject prefab, Transform outerHex, out Bounds localBounds)
{
GameObject probe = UnityEngine.Object.Instantiate(prefab, outerHex, false);
probe.name = $"{prefab.name}_BoundsProbe";
probe.SetActive(true);
bool result = TryCalculateLocalBounds(probe.transform, outerHex, out localBounds, null);
DestroySmart(probe);
return result;
}
private bool TryCalculateLocalBounds(
Transform sourceRoot,
Transform relativeTo,
out Bounds localBounds,
Transform excludedRoot)
{
localBounds = default;
bool hasBounds = false;
Renderer[] renderers = sourceRoot.GetComponentsInChildren<Renderer>(true);
foreach (Renderer renderer in renderers)
{
if (excludedRoot != null && renderer.transform.IsChildOf(excludedRoot))
continue;
EncapsulateWorldBoundsAsLocal(renderer.bounds, relativeTo, ref localBounds, ref hasBounds);
}
if (!hasBounds)
{
Collider[] colliders = sourceRoot.GetComponentsInChildren<Collider>(true);
foreach (Collider collider in colliders)
{
if (excludedRoot != null && collider.transform.IsChildOf(excludedRoot))
continue;
EncapsulateWorldBoundsAsLocal(collider.bounds, relativeTo, ref localBounds, ref hasBounds);
}
}
return hasBounds;
}
private void EncapsulateWorldBoundsAsLocal(
Bounds worldBounds,
Transform relativeTo,
ref Bounds localBounds,
ref bool hasBounds)
{
Vector3 min = worldBounds.min;
Vector3 max = worldBounds.max;
for (int xIndex = 0; xIndex <= 1; xIndex++)
{
for (int yIndex = 0; yIndex <= 1; yIndex++)
{
for (int zIndex = 0; zIndex <= 1; zIndex++)
{
Vector3 worldCorner = new Vector3(
xIndex == 0 ? min.x : max.x,
yIndex == 0 ? min.y : max.y,
zIndex == 0 ? min.z : max.z);
Vector3 localCorner = relativeTo.InverseTransformPoint(worldCorner);
if (!hasBounds)
{
localBounds = new Bounds(localCorner, Vector3.zero);
hasBounds = true;
}
else
{
localBounds.Encapsulate(localCorner);
}
}
}
}
}
private float ResolveHexRadius(Bounds localBounds)
{
Vector2 extentsXZ = GetXZExtents(localBounds);
return Mathf.Min(extentsXZ.x, extentsXZ.y * 2f / Sqrt3);
}
private Vector2 GetXZExtents(Bounds bounds)
{
return new Vector2(bounds.extents.x, bounds.extents.z);
}
private Vector2 GetXZPosition(Vector3 position)
{
return new Vector2(position.x, position.z);
}
private float ApplyPaddingRatioToHexRadius(float radius, float paddingRatio)
{
float clampedPaddingRatio = Mathf.Clamp01(paddingRatio);
return radius * (1f - clampedPaddingRatio);
}
private float CalculateGapUnits(float innerRadius, float gapRatio)
{
float safeGapRatio = Mathf.Max(0f, gapRatio);
float innerFlatToFlatSize = innerRadius * Sqrt3;
return innerFlatToFlatSize * safeGapRatio;
}
private List<Vector2Int> BuildOuterAlignedCells(int count, float gapRatio)
{
if (count <= 0)
return new List<Vector2Int>();
int searchRadius = Mathf.Max(2, Mathf.CeilToInt(Mathf.Sqrt(count)) * 2);
while (true)
{
List<Vector2Int> candidates = BuildSearchCells(searchRadius);
candidates.Sort((left, right) => CompareOuterAlignedCells(left, right, 1f));
if (candidates.Count < count)
{
searchRadius *= 2;
continue;
}
int cutoffLayer = GetOuterAlignedLayer(candidates[count - 1]);
if (LayerTouchesSearchBoundary(candidates, cutoffLayer, searchRadius))
{
searchRadius *= 2;
continue;
}
return SelectCellsByLayers(candidates, count, gapRatio);
}
}
private List<Vector2Int> BuildSearchCells(int searchRadius)
{
int sideLength = searchRadius * 2 + 1;
List<Vector2Int> result = new List<Vector2Int>(sideLength * sideLength);
for (int q = -searchRadius; q <= searchRadius; q++)
{
for (int r = -searchRadius; r <= searchRadius; r++)
result.Add(new Vector2Int(q, r));
}
return result;
}
private bool LayerTouchesSearchBoundary(
IReadOnlyList<Vector2Int> candidates,
int cutoffLayer,
int searchRadius)
{
for (int i = 0; i < candidates.Count; i++)
{
Vector2Int cell = candidates[i];
if (GetOuterAlignedLayer(cell) > cutoffLayer)
break;
if (Mathf.Abs(cell.x) == searchRadius || Mathf.Abs(cell.y) == searchRadius)
return true;
}
return false;
}
private List<Vector2Int> SelectCellsByLayers(
IReadOnlyList<Vector2Int> orderedCandidates,
int count,
float gapRatio)
{
List<Vector2Int> selected = new List<Vector2Int>(count);
HashSet<Vector2Int> occupied = new HashSet<Vector2Int>();
int candidateIndex = 0;
while (selected.Count < count)
{
int layerStart = candidateIndex;
int layer = GetOuterAlignedLayer(orderedCandidates[layerStart]);
candidateIndex++;
while (candidateIndex < orderedCandidates.Count &&
GetOuterAlignedLayer(orderedCandidates[candidateIndex]) == layer)
{
candidateIndex++;
}
int layerCount = candidateIndex - layerStart;
int remaining = count - selected.Count;
if (layerCount <= remaining)
{
for (int i = layerStart; i < candidateIndex; i++)
{
Vector2Int cell = orderedCandidates[i];
selected.Add(cell);
occupied.Add(cell);
}
continue;
}
List<Vector2Int> partialLayer = new List<Vector2Int>(layerCount);
for (int i = layerStart; i < candidateIndex; i++)
partialLayer.Add(orderedCandidates[i]);
SelectCompactPartialLayer(
selected,
occupied,
partialLayer,
remaining,
gapRatio);
}
return selected;
}
private void SelectCompactPartialLayer(
List<Vector2Int> selected,
HashSet<Vector2Int> occupied,
List<Vector2Int> available,
int amount,
float gapRatio)
{
for (int selectionIndex = 0; selectionIndex < amount; selectionIndex++)
{
int bestIndex = -1;
int bestNeighbourCount = int.MinValue;
float bestRequiredRadius = float.MaxValue;
float bestAngle = float.MaxValue;
for (int candidateIndex = 0; candidateIndex < available.Count; candidateIndex++)
{
Vector2Int candidate = available[candidateIndex];
int neighbourCount = CountOccupiedNeighbours(candidate, occupied);
float requiredRadius = CalculateRequiredOuterRadiusAfterAdding(
selected,
candidate,
gapRatio);
float angle = GetCellAngle(candidate, 1f);
bool isBetter = neighbourCount > bestNeighbourCount;
if (!isBetter && neighbourCount == bestNeighbourCount)
{
isBetter = requiredRadius < bestRequiredRadius - ScoreEpsilon;
}
if (!isBetter &&
neighbourCount == bestNeighbourCount &&
Mathf.Abs(requiredRadius - bestRequiredRadius) <= ScoreEpsilon)
{
isBetter = angle < bestAngle;
}
if (!isBetter)
continue;
bestIndex = candidateIndex;
bestNeighbourCount = neighbourCount;
bestRequiredRadius = requiredRadius;
bestAngle = angle;
}
Vector2Int selectedCell = available[bestIndex];
available.RemoveAt(bestIndex);
selected.Add(selectedCell);
occupied.Add(selectedCell);
}
}
private float CalculateRequiredOuterRadiusAfterAdding(
IReadOnlyList<Vector2Int> selected,
Vector2Int candidate,
float gapRatio)
{
List<Vector2Int> testCells = new List<Vector2Int>(selected.Count + 1);
for (int i = 0; i < selected.Count; i++)
testCells.Add(selected[i]);
testCells.Add(candidate);
const float unitInnerRadius = 1f;
List<Vector2> positions = BuildCenteredXZPositions(testCells, unitInnerRadius, gapRatio);
float requiredRadius = 0f;
for (int positionIndex = 0; positionIndex < positions.Count; positionIndex++)
{
Vector2[] vertices = CreateHexVertices(positions[positionIndex], unitInnerRadius);
for (int vertexIndex = 0; vertexIndex < vertices.Length; vertexIndex++)
{
float vertexRequiredRadius = CalculateRequiredOuterRadiusForPoint(vertices[vertexIndex]);
requiredRadius = Mathf.Max(requiredRadius, vertexRequiredRadius);
}
}
return requiredRadius;
}
private int GetOuterAlignedLayer(Vector2Int cell)
{
int first = Mathf.Abs(2 * cell.y + cell.x);
int second = Mathf.Abs(2 * cell.x + cell.y);
int third = Mathf.Abs(cell.x - cell.y);
return Mathf.Max(first, Mathf.Max(second, third));
}
private int CompareOuterAlignedCells(Vector2Int left, Vector2Int right, float pitchRadius)
{
int layerComparison = GetOuterAlignedLayer(left).CompareTo(GetOuterAlignedLayer(right));
if (layerComparison != 0)
return layerComparison;
return CompareCellsByAngle(left, right, pitchRadius);
}
private int CompareCellsByAngle(Vector2Int left, Vector2Int right, float pitchRadius)
{
float leftAngle = GetCellAngle(left, pitchRadius);
float rightAngle = GetCellAngle(right, pitchRadius);
int angleComparison = leftAngle.CompareTo(rightAngle);
if (angleComparison != 0)
return angleComparison;
int qComparison = left.x.CompareTo(right.x);
if (qComparison != 0)
return qComparison;
return left.y.CompareTo(right.y);
}
private float GetCellAngle(Vector2Int cell, float pitchRadius)
{
Vector2 position = AxialToXZ(cell, pitchRadius);
return Mathf.Repeat(Mathf.Atan2(position.y, position.x) * Mathf.Rad2Deg, 360f);
}
private Vector2 CalculateCellsCenterXZ(IReadOnlyList<Vector2Int> cells, float pitchRadius)
{
Vector2 sum = Vector2.zero;
for (int i = 0; i < cells.Count; i++)
sum += AxialToXZ(cells[i], pitchRadius);
return sum / cells.Count;
}
private List<Vector2> BuildCenteredXZPositions(
IReadOnlyList<Vector2Int> cells,
float innerRadius,
float gapRatio)
{
float gap = CalculateGapUnits(innerRadius, gapRatio);
float pitchRadius = innerRadius + gap / Sqrt3;
Vector2 center = CalculateCellsCenterXZ(cells, pitchRadius);
List<Vector2> positions = new List<Vector2>(cells.Count);
for (int i = 0; i < cells.Count; i++)
positions.Add(AxialToXZ(cells[i], pitchRadius) - center);
return positions;
}
private Vector2 AxialToXZ(Vector2Int axial, float pitchRadius)
{
float q = axial.x;
float r = axial.y;
return new Vector2(
1.5f * pitchRadius * q,
Sqrt3 * pitchRadius * (r + q * 0.5f));
}
private float FindMaximumFittingScale(
IReadOnlyList<Vector2Int> cells,
float outerRadius,
float innerBaseRadius,
float gapRatio)
{
float low = 0f;
float high = outerRadius / innerBaseRadius;
for (int iteration = 0; iteration < 48; iteration++)
{
float middle = (low + high) * 0.5f;
if (CanFitScale(cells, outerRadius, innerBaseRadius, gapRatio, middle))
low = middle;
else
high = middle;
}
return low;
}
private bool CanFitScale(
IReadOnlyList<Vector2Int> cells,
float outerRadius,
float innerBaseRadius,
float gapRatio,
float scale)
{
float innerRadius = innerBaseRadius * scale;
List<Vector2> positions = BuildCenteredXZPositions(cells, innerRadius, gapRatio);
for (int positionIndex = 0; positionIndex < positions.Count; positionIndex++)
{
Vector2[] innerVertices = CreateHexVertices(positions[positionIndex], innerRadius);
for (int vertexIndex = 0; vertexIndex < innerVertices.Length; vertexIndex++)
{
if (!IsPointInsideHex(innerVertices[vertexIndex], outerRadius))
return false;
}
}
return true;
}
private Vector2[] CreateHexVertices(Vector2 center, float radius)
{
Vector2[] vertices = new Vector2[6];
for (int vertexIndex = 0; vertexIndex < vertices.Length; vertexIndex++)
{
float angleRadians = Mathf.Deg2Rad * (60f * vertexIndex);
vertices[vertexIndex] = center + new Vector2(
Mathf.Cos(angleRadians) * radius,
Mathf.Sin(angleRadians) * radius);
}
return vertices;
}
private float CalculateRequiredOuterRadiusForPoint(Vector2 point)
{
float requiredApothem = Mathf.Max(
Mathf.Abs(point.y),
Mathf.Max(
Mathf.Abs(HexApothemFactor * point.x + 0.5f * point.y),
Mathf.Abs(HexApothemFactor * point.x - 0.5f * point.y)));
return requiredApothem / HexApothemFactor;
}
private bool IsPointInsideHex(Vector2 point, float radius)
{
float epsilon = Mathf.Epsilon;
float apothem = radius * HexApothemFactor;
return Mathf.Abs(point.y) <= apothem + epsilon &&
Mathf.Abs(HexApothemFactor * point.x + 0.5f * point.y) <= apothem + epsilon &&
Mathf.Abs(HexApothemFactor * point.x - 0.5f * point.y) <= apothem + epsilon;
}
}