Загрузка данных
using System.Collections.Generic;
using Sirenix.OdinInspector;
using UnityEngine;
[ExecuteAlways]
public sealed class HexInsideHexLayout : MonoBehaviour
{
public enum HexOrientation
{
FlatTop,
PointyTop
}
private const float Sqrt3 = 1.7320508f;
private const float HexApothemFactor = Sqrt3 * 0.5f;
private const string GeneratedRootName = "Generated Inner Hexes";
private static 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)
};
[Header("Sources")]
[SerializeField] private Transform outerVisualRoot;
[SerializeField] private GameObject innerHexPrefab;
[SerializeField] private Transform generatedRoot;
[Header("Layout")]
[Min(1)]
[SerializeField] private int innerHexCount = 7;
[Tooltip("Относительный зазор между внутренними гексами. 0.1 = зазор 10% от итогового flat-to-flat размера внутреннего гекса.")]
[Min(0f)]
[SerializeField] private float gapRatio = 0f;
[Tooltip("Относительный отступ от границы внешнего гекса. 0.1 = доступная область уменьшается примерно на 10%.")]
[Range(0f, 0.95f)]
[SerializeField] private float outerPaddingRatio = 0f;
[Tooltip("Локальная высота размещения внутренних гексов по Y.")]
[SerializeField] private float localY = 0f;
[SerializeField] private HexOrientation orientation = HexOrientation.FlatTop;
[Header("Optional radius overrides")]
[Tooltip("Если больше 0, используется вместо радиуса, вычисленного по bounds внешнего гекса.")]
[SerializeField] private float outerRadiusOverride = 0f;
[Tooltip("Если больше 0, используется вместо радиуса, вычисленного по bounds внутреннего prefab.")]
[SerializeField] private float innerRadiusOverride = 0f;
[Header("Debug")]
[SerializeField] private float lastGeneratedScale;
[SerializeField] private float lastGeneratedInnerRadius;
[SerializeField] private float lastGeneratedGap;
[SerializeField] private float lastOuterRadius;
[SerializeField] private float lastEffectiveOuterRadius;
[SerializeField] private float lastOuterPadding;
[Button("Generate")]
public void Generate()
{
if (innerHexPrefab == null)
{
Debug.LogWarning($"{nameof(HexInsideHexLayout)}: inner hex prefab is not assigned.", this);
return;
}
Transform root = ResolveGeneratedRoot(true);
if (root == null)
{
Debug.LogWarning($"{nameof(HexInsideHexLayout)}: cannot resolve generated root.", this);
return;
}
PrepareGeneratedRoot(root);
ClearChildren(root);
Transform actualOuterVisualRoot = outerVisualRoot != null ? outerVisualRoot : transform;
if (!TryCalculateLocalBounds(actualOuterVisualRoot, transform, out Bounds outerLocalBounds, root))
{
Debug.LogWarning($"{nameof(HexInsideHexLayout)}: cannot calculate outer bounds.", this);
return;
}
if (!TryCalculatePrefabLocalBounds(innerHexPrefab, out Bounds innerLocalBounds))
{
Debug.LogWarning($"{nameof(HexInsideHexLayout)}: cannot calculate inner prefab bounds.", this);
return;
}
float outerRadius = ResolveHexRadius(outerLocalBounds, true);
float innerBaseRadius = ResolveHexRadius(innerLocalBounds, false);
float effectiveOuterRadius = ApplyPaddingRatioToHexRadius(
outerRadius,
outerPaddingRatio);
lastOuterRadius = outerRadius;
lastEffectiveOuterRadius = effectiveOuterRadius;
lastOuterPadding = CalculateOuterPaddingUnits(outerRadius, outerPaddingRatio);
if (outerRadius <= 0f)
{
Debug.LogWarning($"{nameof(HexInsideHexLayout)}: outer radius is zero.", this);
return;
}
if (effectiveOuterRadius <= 0f)
{
Debug.LogWarning(
$"{nameof(HexInsideHexLayout)}: effective outer radius is zero. Reduce outer padding ratio.",
this);
return;
}
if (innerBaseRadius <= 0f)
{
Debug.LogWarning($"{nameof(HexInsideHexLayout)}: inner radius is zero.", this);
return;
}
int count = Mathf.Max(1, innerHexCount);
List<Vector2Int> cells = BuildSpiralCells(count);
float finalScale = FindMaximumFittingScale(
cells,
effectiveOuterRadius,
innerBaseRadius,
gapRatio,
orientation);
float finalInnerRadius = innerBaseRadius * finalScale;
float finalGap = CalculateGapUnits(finalInnerRadius, gapRatio);
lastGeneratedScale = finalScale;
lastGeneratedInnerRadius = finalInnerRadius;
lastGeneratedGap = finalGap;
Vector2 outerCenterXZ = GetXZPosition(outerLocalBounds.center);
List<Vector2> localXZPositions = BuildCenteredXZPositions(
cells,
finalInnerRadius,
gapRatio,
orientation);
for (int index = 0; index < localXZPositions.Count; index++)
{
Vector2 finalXZPosition = outerCenterXZ + localXZPositions[index];
GameObject instance = Instantiate(innerHexPrefab, root, false);
instance.name = $"{innerHexPrefab.name} {index:000}";
instance.transform.localPosition = new Vector3(
finalXZPosition.x,
localY,
finalXZPosition.y);
instance.transform.localRotation = innerHexPrefab.transform.localRotation;
instance.transform.localScale = innerHexPrefab.transform.localScale * finalScale;
}
}
[Button("Clear Generated")]
public void ClearGenerated()
{
Transform root = ResolveGeneratedRoot(false);
if (root == null)
{
return;
}
ClearChildren(root);
}
private Transform ResolveGeneratedRoot(bool createIfMissing)
{
if (IsValidGeneratedRoot(generatedRoot))
{
return generatedRoot;
}
Transform existingRoot = transform.Find(GeneratedRootName);
if (existingRoot != null)
{
generatedRoot = existingRoot;
return generatedRoot;
}
if (!createIfMissing)
{
return null;
}
GameObject rootObject = new GameObject(GeneratedRootName);
Transform rootTransform = rootObject.transform;
rootTransform.SetParent(transform, false);
generatedRoot = rootTransform;
return generatedRoot;
}
private bool IsValidGeneratedRoot(Transform candidate)
{
if (candidate == null)
{
return false;
}
if (candidate == transform)
{
return false;
}
if (outerVisualRoot != null && candidate == outerVisualRoot)
{
return false;
}
if (outerVisualRoot != null && outerVisualRoot.IsChildOf(candidate))
{
return false;
}
return true;
}
private void PrepareGeneratedRoot(Transform root)
{
if (root == transform)
{
return;
}
root.SetParent(transform, false);
root.localPosition = Vector3.zero;
root.localRotation = Quaternion.identity;
root.localScale = Vector3.one;
}
private static void ClearChildren(Transform root)
{
for (int childIndex = root.childCount - 1; childIndex >= 0; childIndex--)
{
DestroySmart(root.GetChild(childIndex).gameObject);
}
}
private static void DestroySmart(Object target)
{
if (target == null)
{
return;
}
if (Application.isPlaying)
{
Object.Destroy(target);
}
else
{
Object.DestroyImmediate(target);
}
}
private bool TryCalculatePrefabLocalBounds(GameObject prefab, out Bounds localBounds)
{
GameObject probe = Instantiate(prefab, transform, false);
probe.name = $"{prefab.name}_BoundsProbe";
probe.hideFlags = HideFlags.HideAndDontSave;
probe.SetActive(true);
bool result = TryCalculateLocalBounds(
probe.transform,
transform,
out localBounds,
null);
DestroySmart(probe);
return result;
}
private static 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 static 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, bool isOuter)
{
float radiusOverride = isOuter ? outerRadiusOverride : innerRadiusOverride;
if (radiusOverride > 0f)
{
return radiusOverride;
}
Vector2 extentsXZ = GetXZExtents(localBounds);
if (orientation == HexOrientation.FlatTop)
{
return Mathf.Min(
extentsXZ.x,
extentsXZ.y * 2f / Sqrt3);
}
return Mathf.Min(
extentsXZ.x * 2f / Sqrt3,
extentsXZ.y);
}
private static Vector2 GetXZExtents(Bounds bounds)
{
return new Vector2(bounds.extents.x, bounds.extents.z);
}
private static Vector2 GetXZPosition(Vector3 position)
{
return new Vector2(position.x, position.z);
}
private static float ApplyPaddingRatioToHexRadius(
float radius,
float paddingRatio)
{
float clampedPaddingRatio = Mathf.Clamp01(paddingRatio);
return radius * (1f - clampedPaddingRatio);
}
private static float CalculateOuterPaddingUnits(
float outerRadius,
float paddingRatio)
{
float clampedPaddingRatio = Mathf.Clamp01(paddingRatio);
float outerApothem = outerRadius * HexApothemFactor;
return outerApothem * clampedPaddingRatio;
}
private static float CalculateGapUnits(
float innerRadius,
float gapRatio)
{
float safeGapRatio = Mathf.Max(0f, gapRatio);
float innerFlatToFlatSize = innerRadius * Sqrt3;
return innerFlatToFlatSize * safeGapRatio;
}
private static List<Vector2Int> BuildSpiralCells(int count)
{
List<Vector2Int> cells = new List<Vector2Int>(count)
{
Vector2Int.zero
};
for (int radius = 1; cells.Count < count; radius++)
{
Vector2Int cell = Multiply(AxialDirections[4], radius);
for (int sideIndex = 0; sideIndex < 6 && cells.Count < count; sideIndex++)
{
for (int stepIndex = 0; stepIndex < radius && cells.Count < count; stepIndex++)
{
cells.Add(cell);
cell += AxialDirections[sideIndex];
}
}
}
return cells;
}
private static Vector2Int Multiply(Vector2Int value, int multiplier)
{
return new Vector2Int(
value.x * multiplier,
value.y * multiplier);
}
private static List<Vector2> BuildCenteredXZPositions(
IReadOnlyList<Vector2Int> cells,
float innerRadius,
float gapRatio,
HexOrientation orientation)
{
float gap = CalculateGapUnits(innerRadius, gapRatio);
float pitchRadius = innerRadius + gap / Sqrt3;
List<Vector2> positions = new List<Vector2>(cells.Count);
Vector2 positionsSum = Vector2.zero;
for (int cellIndex = 0; cellIndex < cells.Count; cellIndex++)
{
Vector2 position = AxialToXZ(
cells[cellIndex],
pitchRadius,
orientation);
positions.Add(position);
positionsSum += position;
}
Vector2 groupCenter = positionsSum / cells.Count;
for (int positionIndex = 0; positionIndex < positions.Count; positionIndex++)
{
positions[positionIndex] -= groupCenter;
}
return positions;
}
private static Vector2 AxialToXZ(
Vector2Int axial,
float pitchRadius,
HexOrientation orientation)
{
float q = axial.x;
float r = axial.y;
if (orientation == HexOrientation.FlatTop)
{
return new Vector2(
1.5f * pitchRadius * q,
Sqrt3 * pitchRadius * (r + q * 0.5f));
}
return new Vector2(
Sqrt3 * pitchRadius * (q + r * 0.5f),
1.5f * pitchRadius * r);
}
private static float FindMaximumFittingScale(
IReadOnlyList<Vector2Int> cells,
float outerRadius,
float innerBaseRadius,
float gapRatio,
HexOrientation orientation)
{
float low = 0f;
float high = outerRadius / innerBaseRadius;
for (int iteration = 0; iteration < 48; iteration++)
{
float middle = (low + high) * 0.5f;
bool fits = CanFitScale(
cells,
outerRadius,
innerBaseRadius,
gapRatio,
orientation,
middle);
if (fits)
{
low = middle;
}
else
{
high = middle;
}
}
return low;
}
private static bool CanFitScale(
IReadOnlyList<Vector2Int> cells,
float outerRadius,
float innerBaseRadius,
float gapRatio,
HexOrientation orientation,
float scale)
{
float innerRadius = innerBaseRadius * scale;
List<Vector2> positions = BuildCenteredXZPositions(
cells,
innerRadius,
gapRatio,
orientation);
for (int positionIndex = 0; positionIndex < positions.Count; positionIndex++)
{
Vector2[] innerVertices = CreateHexVertices(
positions[positionIndex],
innerRadius,
orientation);
for (int vertexIndex = 0; vertexIndex < innerVertices.Length; vertexIndex++)
{
if (!IsPointInsideHex(
innerVertices[vertexIndex],
outerRadius,
orientation))
{
return false;
}
}
}
return true;
}
private static Vector2[] CreateHexVertices(
Vector2 center,
float radius,
HexOrientation orientation)
{
Vector2[] vertices = new Vector2[6];
float angleOffsetDegrees = orientation == HexOrientation.FlatTop ? 0f : 30f;
for (int vertexIndex = 0; vertexIndex < vertices.Length; vertexIndex++)
{
float angleRadians = Mathf.Deg2Rad * (angleOffsetDegrees + 60f * vertexIndex);
vertices[vertexIndex] = center + new Vector2(
Mathf.Cos(angleRadians) * radius,
Mathf.Sin(angleRadians) * radius);
}
return vertices;
}
private static bool IsPointInsideHex(
Vector2 point,
float radius,
HexOrientation orientation)
{
const float epsilon = 0.0001f;
float apothem = radius * HexApothemFactor;
if (orientation == HexOrientation.FlatTop)
{
return Mathf.Abs(point.y) <= apothem + epsilon
&& Mathf.Abs(Sqrt3 * 0.5f * point.x + 0.5f * point.y) <= apothem + epsilon
&& Mathf.Abs(Sqrt3 * 0.5f * point.x - 0.5f * point.y) <= apothem + epsilon;
}
return Mathf.Abs(point.x) <= apothem + epsilon
&& Mathf.Abs(0.5f * point.x + Sqrt3 * 0.5f * point.y) <= apothem + epsilon
&& Mathf.Abs(0.5f * point.x - Sqrt3 * 0.5f * point.y) <= apothem + epsilon;
}
}