Загрузка данных
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;
}
}
}