Загрузка данных
using System.Collections.Generic;
using UnityEngine;
namespace CodeBase.HexLayout
{
public sealed class HexesOnRectanglePlaneLayout : MonoBehaviour
{
[Header("Scene")]
[SerializeField] private Renderer _planeRenderer;
[SerializeField] private GameObject _hexPrefab;
[SerializeField] private Transform _hexRoot;
[Header("Layout")]
[SerializeField, Min(0)] private int _hexCount = 7;
[Tooltip("0.05 = зазор 5% от размера гекса.")]
[SerializeField, Range(0f, 0.5f)] private float _gapPercent = 0.05f;
[Tooltip("Для классической гекс-сетки по рядам обычно 0.75.")]
[SerializeField, Range(0.5f, 1f)] private float _rowDepthStepMultiplier = 0.75f;
[Tooltip("Смещение каждого второго ряда. Обычно 0.5.")]
[SerializeField, Range(0f, 1f)] private float _oddRowOffsetMultiplier = 0.5f;
[SerializeField] private float _surfaceOffsetY = 0.02f;
private readonly List<GameObject> _createdHexes = new();
[ContextMenu("Rebuild Hexes")]
public void Rebuild()
{
if (_planeRenderer == null)
{
Debug.LogError("Plane Renderer is not assigned.", this);
return;
}
if (_hexPrefab == null)
{
Debug.LogError("Hex Prefab is not assigned.", this);
return;
}
ClearCreatedHexes();
if (_hexCount <= 0)
return;
Transform parent = _hexRoot != null ? _hexRoot : transform;
GameObject firstHex = Instantiate(
_hexPrefab,
Vector3.zero,
_hexPrefab.transform.rotation,
parent);
_createdHexes.Add(firstHex);
if (!TryCalculateRendererBounds(firstHex, out Bounds baseHexBounds))
{
Debug.LogError("Hex prefab must contain at least one Renderer.", this);
ClearCreatedHexes();
return;
}
Bounds planeBounds = _planeRenderer.bounds;
Vector2 planeSize = new Vector2(
planeBounds.size.x,
planeBounds.size.z);
Vector2 baseHexSize = new Vector2(
baseHexBounds.size.x,
baseHexBounds.size.z);
LayoutResult layoutResult = FindBestLayout(
_hexCount,
planeSize,
baseHexSize);
if (!layoutResult.IsValid)
{
Debug.LogError("Failed to calculate hex layout.", this);
ClearCreatedHexes();
return;
}
Vector3 baseLocalScale = firstHex.transform.localScale;
Vector2 planeCenterXZ = new Vector2(
planeBounds.center.x,
planeBounds.center.z);
for (int hexIndex = 0; hexIndex < _hexCount; hexIndex++)
{
GameObject hexGameObject = hexIndex == 0
? firstHex
: Instantiate(_hexPrefab, Vector3.zero, _hexPrefab.transform.rotation, parent);
if (hexIndex > 0)
_createdHexes.Add(hexGameObject);
hexGameObject.transform.localScale = new Vector3(
baseLocalScale.x * layoutResult.Scale,
baseLocalScale.y,
baseLocalScale.z * layoutResult.Scale);
Vector2 localCenter = layoutResult.Centers[hexIndex] - layoutResult.BoundsCenter;
Vector2 worldCenterXZ = planeCenterXZ + localCenter * layoutResult.Scale;
if (!TryCalculateRendererBounds(hexGameObject, out Bounds scaledHexBounds))
continue;
Vector3 desiredBoundsCenter = new Vector3(
worldCenterXZ.x,
planeBounds.max.y + _surfaceOffsetY + scaledHexBounds.extents.y,
worldCenterXZ.y);
MoveBoundsCenterTo(hexGameObject, desiredBoundsCenter);
}
}
[ContextMenu("Clear Hexes")]
public void ClearCreatedHexes()
{
for (int index = _createdHexes.Count - 1; index >= 0; index--)
{
GameObject createdHex = _createdHexes[index];
if (createdHex == null)
continue;
if (Application.isPlaying)
Destroy(createdHex);
else
DestroyImmediate(createdHex);
}
_createdHexes.Clear();
}
private LayoutResult FindBestLayout(
int hexCount,
Vector2 planeSize,
Vector2 baseHexSize)
{
LayoutResult bestLayout = default;
if (planeSize.x <= Mathf.Epsilon ||
planeSize.y <= Mathf.Epsilon ||
baseHexSize.x <= Mathf.Epsilon ||
baseHexSize.y <= Mathf.Epsilon)
{
return bestLayout;
}
for (int rowCount = 1; rowCount <= hexCount; rowCount++)
{
int[] rowHexCounts = BuildRowHexCounts(hexCount, rowCount);
Vector2[] centers = BuildCenters(rowHexCounts, baseHexSize);
LayoutBounds layoutBounds = CalculateLayoutBounds(centers, baseHexSize);
float scaleByWidth = planeSize.x / layoutBounds.Size.x;
float scaleByDepth = planeSize.y / layoutBounds.Size.y;
float scale = Mathf.Min(scaleByWidth, scaleByDepth);
if (!bestLayout.IsValid || scale > bestLayout.Scale)
{
bestLayout = new LayoutResult(
centers,
layoutBounds.Center,
scale);
}
}
return bestLayout;
}
private static int[] BuildRowHexCounts(int hexCount, int rowCount)
{
int minimumColumns = hexCount / rowCount;
int rowsWithExtraColumn = hexCount % rowCount;
int[] rowHexCounts = new int[rowCount];
for (int rowIndex = 0; rowIndex < rowCount; rowIndex++)
rowHexCounts[rowIndex] = minimumColumns;
int firstExtraRow = (rowCount - rowsWithExtraColumn) / 2;
for (int extraIndex = 0; extraIndex < rowsWithExtraColumn; extraIndex++)
rowHexCounts[firstExtraRow + extraIndex]++;
return rowHexCounts;
}
private Vector2[] BuildCenters(int[] rowHexCounts, Vector2 baseHexSize)
{
List<Vector2> centers = new();
float gap = Mathf.Max(baseHexSize.x, baseHexSize.y) * Mathf.Max(0f, _gapPercent);
float columnStep = baseHexSize.x + gap;
float rowStep = baseHexSize.y * _rowDepthStepMultiplier + gap;
float firstRowZ = -rowStep * (rowHexCounts.Length - 1) * 0.5f;
for (int rowIndex = 0; rowIndex < rowHexCounts.Length; rowIndex++)
{
int columnCount = rowHexCounts[rowIndex];
float rowZ = firstRowZ + rowIndex * rowStep;
float rowXOffset = rowIndex % 2 == 1
? columnStep * _oddRowOffsetMultiplier
: 0f;
float firstColumnX = -columnStep * (columnCount - 1) * 0.5f;
for (int columnIndex = 0; columnIndex < columnCount; columnIndex++)
{
float columnX = firstColumnX + columnIndex * columnStep + rowXOffset;
centers.Add(new Vector2(columnX, rowZ));
}
}
return centers.ToArray();
}
private static LayoutBounds CalculateLayoutBounds(
Vector2[] centers,
Vector2 baseHexSize)
{
float halfWidth = baseHexSize.x * 0.5f;
float halfDepth = baseHexSize.y * 0.5f;
float minX = float.PositiveInfinity;
float maxX = float.NegativeInfinity;
float minZ = float.PositiveInfinity;
float maxZ = float.NegativeInfinity;
foreach (Vector2 center in centers)
{
minX = Mathf.Min(minX, center.x - halfWidth);
maxX = Mathf.Max(maxX, center.x + halfWidth);
minZ = Mathf.Min(minZ, center.y - halfDepth);
maxZ = Mathf.Max(maxZ, center.y + halfDepth);
}
Vector2 boundsCenter = new Vector2(
(minX + maxX) * 0.5f,
(minZ + maxZ) * 0.5f);
Vector2 boundsSize = new Vector2(
maxX - minX,
maxZ - minZ);
return new LayoutBounds(boundsCenter, boundsSize);
}
private static bool TryCalculateRendererBounds(
GameObject gameObject,
out Bounds bounds)
{
Renderer[] renderers = gameObject.GetComponentsInChildren<Renderer>();
bounds = default;
bool hasBounds = false;
foreach (Renderer renderer in renderers)
{
if (!renderer.enabled)
continue;
if (!hasBounds)
{
bounds = renderer.bounds;
hasBounds = true;
}
else
{
bounds.Encapsulate(renderer.bounds);
}
}
return hasBounds;
}
private static void MoveBoundsCenterTo(
GameObject gameObject,
Vector3 desiredBoundsCenter)
{
if (!TryCalculateRendererBounds(gameObject, out Bounds currentBounds))
return;
Vector3 objectToBoundsCenterOffset = currentBounds.center - gameObject.transform.position;
gameObject.transform.position = desiredBoundsCenter - objectToBoundsCenterOffset;
}
private readonly struct LayoutResult
{
public readonly Vector2[] Centers;
public readonly Vector2 BoundsCenter;
public readonly float Scale;
public bool IsValid => Centers != null && Centers.Length > 0 && Scale > 0f;
public LayoutResult(
Vector2[] centers,
Vector2 boundsCenter,
float scale)
{
Centers = centers;
BoundsCenter = boundsCenter;
Scale = scale;
}
}
private readonly struct LayoutBounds
{
public readonly Vector2 Center;
public readonly Vector2 Size;
public LayoutBounds(Vector2 center, Vector2 size)
{
Center = center;
Size = size;
}
}
}
}