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


public sealed class HexPlaneGenerator : MonoBehaviour
{
    private const string DefaultRootName = "__Generated Hexes";
    private const float HexDepth = 1.7320508f;
    private const float RowDensityFactor = 1.16f;

    [Header("Generation")]
    [SerializeField] private GameObject hexPrefab;
    [SerializeField, Min(1)] private int hexCount = 24;
    [SerializeField] private bool autoRegenerate = true;
    [SerializeField] private bool clearOnDisable;
    [SerializeField] private bool saveGeneratedObjects;
    [SerializeField] private string rootName = DefaultRootName;

    [Header("Plane Fit")]
    [SerializeField, Min(0f)] private float planePadding = 0.4f;
    [SerializeField] private float surfaceOffset = 0.04f;
    [SerializeField, Min(0.01f)] private float prefabRadius = 1f;
    [SerializeField, Range(0f, 0.5f)] private float gapRatio = 0.04f;

    private bool needsRegenerate = true;

    private void OnEnable()
    {
        QueueRegenerate();
    }

    private void OnDisable()
    {
        if (clearOnDisable && !Application.isPlaying)
        {
            ClearGenerated();
        }
    }

    private void OnValidate()
    {
        hexCount = Mathf.Max(1, hexCount);
        prefabRadius = Mathf.Max(0.01f, prefabRadius);
        planePadding = Mathf.Max(0f, planePadding);
        gapRatio = Mathf.Clamp(gapRatio, 0f, 0.5f);
        QueueRegenerate();
    }

    private void Update()
    {
        if (!autoRegenerate || !needsRegenerate)
        {
            return;
        }

        Generate();
    }

    [ContextMenu("Generate Hexes")]
    public void Generate()
    {
        needsRegenerate = false;

        if (!gameObject.scene.IsValid() || hexPrefab == null)
        {
            return;
        }

        Transform root = GetOrCreateRoot();
        ApplyRootTransform(root);
        ClearChildren(root);

        Vector2 planeSize = GetPlaneSizeInWorldUnits();
        float availableWidth = Mathf.Max(0f, planeSize.x - planePadding * 2f);
        float availableDepth = Mathf.Max(0f, planeSize.y - planePadding * 2f);

        if (availableWidth <= 0f || availableDepth <= 0f)
        {
            return;
        }

        List<Vector2> positions = BuildPlaneFillingLayout(hexCount, availableWidth, availableDepth);
        if (positions.Count == 0)
        {
            return;
        }

        LayoutBounds bounds = CalculateBounds(positions);
        float radius = Mathf.Min(availableWidth / bounds.Size.x, availableDepth / bounds.Size.y);
        if (radius <= 0f)
        {
            return;
        }

        CreateHexes(root, positions, bounds.Center, radius);
    }

    [ContextMenu("Clear Generated Hexes")]
    public void ClearGenerated()
    {
        string safeRootName = GetSafeRootName();
        for (int i = transform.childCount - 1; i >= 0; i--)
        {
            Transform child = transform.GetChild(i);
            if (child.name == safeRootName)
            {
                DestroyGeneratedObject(child.gameObject);
            }
        }
    }

    private void QueueRegenerate()
    {
        needsRegenerate = true;
    }

    private List<Vector2> BuildPlaneFillingLayout(int count, float availableWidth, float availableDepth)
    {
        int rows = GetPreferredRowCount(count, availableWidth, availableDepth);
        int[] rowLengths = GetBalancedRowLengths(count, rows);
        return BuildRowPositions(rowLengths);
    }

    private int GetPreferredRowCount(int count, float availableWidth, float availableDepth)
    {
        int rowsByShape = GetRowsByNaturalCapacity(count);
        float planeAspect = availableWidth / Mathf.Max(0.001f, availableDepth);
        int rowsByPlane = Mathf.CeilToInt(Mathf.Sqrt(count / Mathf.Max(0.001f, planeAspect)) * RowDensityFactor);
        int rows = Mathf.Max(rowsByShape, rowsByPlane);
        return Mathf.Clamp(rows, 1, count);
    }

    private int GetRowsByNaturalCapacity(int count)
    {
        int rows = 1;
        while (count > rows * rows + rows - 1)
        {
            rows++;
        }

        return rows;
    }

    private int[] GetBalancedRowLengths(int count, int rows)
    {
        int[] rowLengths = new int[rows];
        int baseLength = count / rows;
        int extra = count % rows;

        for (int row = 0; row < rows; row++)
        {
            rowLengths[row] = baseLength;
        }

        for (int i = 0; i < extra; i++)
        {
            rowLengths[GetExtraRowIndex(rows, i)]++;
        }

        return rowLengths;
    }

    private int GetExtraRowIndex(int rows, int order)
    {
        if (rows % 2 == 0)
        {
            int half = rows / 2;
            int evenRing = order / 2;
            return order % 2 == 0 ? half - 1 - evenRing : half + evenRing;
        }

        int center = rows / 2;
        if (order == 0)
        {
            return center;
        }

        int adjustedOrder = order - 1;
        int cycleIndex = adjustedOrder % 3;
        int oddRing = adjustedOrder / 3 + 1;

        if (cycleIndex == 0)
        {
            return Mathf.Max(0, center - oddRing);
        }

        if (cycleIndex == 1)
        {
            return Mathf.Min(rows - 1, center + oddRing);
        }

        return center;
    }

    private int[] GetRowsByCenterOut(int rows)
    {
        int[] order = new int[rows];
        int writeIndex = 0;

        if (rows % 2 == 1)
        {
            order[writeIndex++] = rows / 2;
        }

        int upper = rows / 2 - 1;
        int lower = rows % 2 == 0 ? rows / 2 : rows / 2 + 1;
        while (writeIndex < rows)
        {
            if (upper >= 0)
            {
                order[writeIndex++] = upper--;
            }

            if (writeIndex < rows && lower < rows)
            {
                order[writeIndex++] = lower++;
            }
        }

        return order;
    }

    private List<Vector2> BuildRowPositions(int[] rowLengths)
    {
        var rowPositions = new List<Vector2>[rowLengths.Length];
        float xStep = GetColumnStep(1f);
        float rowStep = GetRowStep(1f);
        float centerRow = (rowLengths.Length - 1) * 0.5f;

        for (int row = 0; row < rowLengths.Length; row++)
        {
            rowPositions[row] = new List<Vector2>(rowLengths[row]);
            float xOffset = GetRowOffset(row, rowLengths, xStep);
            float startX = -(rowLengths[row] - 1) * xStep * 0.5f + xOffset;
            float z = (centerRow - row) * rowStep;

            for (int column = 0; column < rowLengths[row]; column++)
            {
                rowPositions[row].Add(new Vector2(startX + column * xStep, z));
            }
        }

        return FlattenRowsCenterOut(rowPositions);
    }

    private float GetRowOffset(int row, int[] rowLengths, float xStep)
    {
        bool halfStepOffset = false;
        for (int i = 1; i <= row; i++)
        {
            bool sameParityAsPreviousRow = Mathf.Abs(rowLengths[i] - rowLengths[i - 1]) % 2 == 0;
            if (sameParityAsPreviousRow)
            {
                halfStepOffset = !halfStepOffset;
            }
        }

        return halfStepOffset ? xStep * 0.5f : 0f;
    }

    private List<Vector2> FlattenRowsCenterOut(List<Vector2>[] rowPositions)
    {
        int count = 0;
        for (int row = 0; row < rowPositions.Length; row++)
        {
            count += rowPositions[row].Count;
        }

        var positions = new List<Vector2>(count);
        int[] rowOrder = GetRowsByCenterOut(rowPositions.Length);
        int[] usedColumns = new int[rowPositions.Length];

        while (positions.Count < count)
        {
            for (int i = 0; i < rowOrder.Length && positions.Count < count; i++)
            {
                int row = rowOrder[i];
                if (usedColumns[row] >= rowPositions[row].Count)
                {
                    continue;
                }

                positions.Add(rowPositions[row][usedColumns[row]]);
                usedColumns[row]++;
            }
        }

        return positions;
    }

    private LayoutBounds CalculateBounds(List<Vector2> positions)
    {
        float minX = float.PositiveInfinity;
        float maxX = float.NegativeInfinity;
        float minZ = float.PositiveInfinity;
        float maxZ = float.NegativeInfinity;
        float halfDepth = HexDepth * 0.5f;

        for (int i = 0; i < positions.Count; i++)
        {
            Vector2 position = positions[i];
            minX = Mathf.Min(minX, position.x - 1f);
            maxX = Mathf.Max(maxX, position.x + 1f);
            minZ = Mathf.Min(minZ, position.y - halfDepth);
            maxZ = Mathf.Max(maxZ, position.y + halfDepth);
        }

        Vector2 center = new Vector2((minX + maxX) * 0.5f, (minZ + maxZ) * 0.5f);
        Vector2 size = new Vector2(maxX - minX, maxZ - minZ);
        return new LayoutBounds(center, size);
    }

    private void CreateHexes(Transform root, List<Vector2> positions, Vector2 center, float radius)
    {
        float scale = radius / prefabRadius;

        for (int i = 0; i < positions.Count; i++)
        {
            Vector2 centeredPosition = (positions[i] - center) * radius;
            CreateHex(root, i, centeredPosition, scale);
        }
    }

    private float GetColumnStep(float radius)
    {
        return 3f * radius * (1f + gapRatio);
    }

    private float GetRowStep(float radius)
    {
        return HexDepth * 0.5f * radius * (1f + gapRatio);
    }

    private void CreateHex(Transform root, int index, Vector2 position, float scale)
    {
        GameObject hex = InstantiatePrefab();
        if (hex == null)
        {
            return;
        }

        hex.name = "Hex_" + index;
        hex.hideFlags = GetGeneratedHideFlags();
        hex.transform.SetParent(root, false);
        hex.transform.localPosition = new Vector3(position.x, surfaceOffset, position.y);
        hex.transform.localRotation = Quaternion.identity;

        Vector3 baseScale = hex.transform.localScale;
        hex.transform.localScale = new Vector3(baseScale.x * scale, baseScale.y, baseScale.z * scale);
    }

    private GameObject InstantiatePrefab()
    {
#if UNITY_EDITOR
        if (!Application.isPlaying)
        {
            if (PrefabUtility.IsPartOfPrefabAsset(hexPrefab))
            {
                return PrefabUtility.InstantiatePrefab(hexPrefab) as GameObject;
            }
        }
#endif

        return Instantiate(hexPrefab);
    }

    private Transform GetOrCreateRoot()
    {
        string safeRootName = GetSafeRootName();
        Transform root = null;
        for (int i = transform.childCount - 1; i >= 0; i--)
        {
            Transform child = transform.GetChild(i);
            if (child.name != safeRootName)
            {
                continue;
            }

            if (root == null)
            {
                root = child;
            }
            else
            {
                DestroyGeneratedObject(child.gameObject);
            }
        }

        if (root != null)
        {
            root.gameObject.hideFlags = GetGeneratedHideFlags();
            return root;
        }

        var rootObject = new GameObject(safeRootName);
        rootObject.hideFlags = GetGeneratedHideFlags();
        rootObject.transform.SetParent(transform, false);
        return rootObject.transform;
    }

    private string GetSafeRootName()
    {
        return string.IsNullOrWhiteSpace(rootName) ? DefaultRootName : rootName;
    }

    private void ApplyRootTransform(Transform root)
    {
        root.localPosition = Vector3.zero;
        root.localRotation = Quaternion.identity;
        Vector3 scale = transform.localScale;
        root.localScale = new Vector3(SafeInverse(scale.x), SafeInverse(scale.y), SafeInverse(scale.z));
    }

    private float SafeInverse(float value)
    {
        return Mathf.Abs(value) < 0.0001f ? 1f : 1f / value;
    }

    private HideFlags GetGeneratedHideFlags()
    {
        return saveGeneratedObjects ? HideFlags.None : HideFlags.DontSaveInEditor | HideFlags.DontSaveInBuild;
    }

    private Vector2 GetPlaneSizeInWorldUnits()
    {
        MeshFilter meshFilter = GetComponent<MeshFilter>();
        if (meshFilter != null && meshFilter.sharedMesh != null)
        {
            Bounds bounds = meshFilter.sharedMesh.bounds;
            Vector3 scale = transform.lossyScale;
            return new Vector2(Mathf.Abs(bounds.size.x * scale.x), Mathf.Abs(bounds.size.z * scale.z));
        }

        Vector3 fallbackScale = transform.lossyScale;
        return new Vector2(10f * Mathf.Abs(fallbackScale.x), 10f * Mathf.Abs(fallbackScale.z));
    }

    private void ClearChildren(Transform root)
    {
        for (int i = root.childCount - 1; i >= 0; i--)
        {
            DestroyGeneratedObject(root.GetChild(i).gameObject);
        }
    }

    private void DestroyGeneratedObject(UnityEngine.Object target)
    {
        if (target == null)
        {
            return;
        }

        if (Application.isPlaying)
        {
            Destroy(target);
        }
        else
        {
#if UNITY_EDITOR
            MoveSelectionFromDestroyedObject(target);
#endif
            DestroyImmediate(target);
        }
    }

#if UNITY_EDITOR
    private void MoveSelectionFromDestroyedObject(UnityEngine.Object target)
    {
        GameObject targetGameObject = GetTargetGameObject(target);
        if (targetGameObject == null)
        {
            return;
        }

        Object[] selectedObjects = Selection.objects;
        for (int i = 0; i < selectedObjects.Length; i++)
        {
            GameObject selectedGameObject = GetTargetGameObject(selectedObjects[i]);
            if (selectedGameObject == null)
            {
                continue;
            }

            if (selectedGameObject == targetGameObject || selectedGameObject.transform.IsChildOf(targetGameObject.transform))
            {
                Selection.activeGameObject = gameObject;
                return;
            }
        }
    }

    private GameObject GetTargetGameObject(UnityEngine.Object target)
    {
        if (target is GameObject targetGameObject)
        {
            return targetGameObject;
        }

        if (target is Component targetComponent)
        {
            return targetComponent.gameObject;
        }

        return null;
    }
#endif

    private readonly struct LayoutBounds
    {
        public readonly Vector2 Center;
        public readonly Vector2 Size;

        public LayoutBounds(Vector2 center, Vector2 size)
        {
            Center = center;
            Size = size;
        }
    }
}