Загрузка данных


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;
            }
        }
    }
}