Files
MVP1/Assets/Scripts/Modules/Settlement/BuildGrid.cs
T

369 lines
16 KiB
C#
Raw Normal View History

using System.Collections.Generic;
using UnityEngine;
// МОДУЛЬ ПОСЕЛЕНИЯ. Документ: Docs/Modules/Settlement.md
// Сетка свободной стройки: игрок сам выбирает, где поставить здание.
//
// - Клетка = 1 единица мира. Сетка прямоугольная (арт города не изометрический).
// - Где можно строить: прямоугольник вокруг старых участков (BuildSlot) и Ратуши с запасом padding.
// Можно задать руками: autoArea = false и areaMin / areaMax.
// - Занятые клетки: под каждым построенным зданием (его размер на сетке, BuildingData.footprint) и под Ратушей.
// Старые сохранения: здание стоит, где стояло, занятость считается по тому, какие клетки оно накрывает.
// - В режиме стройки сетка рисуется поверх земли: свободные клетки -- светлые линии, занятые -- красные.
// - Старые участки и площадки «Застройка» прячутся при запуске (свободная стройка их заменила).
//
// Объект создаётся сам (BuildGrid.Instance), руками на сцену ставить не нужно.
// Если поставить руками -- можно настроить поля в инспекторе.
public class BuildGrid : MonoBehaviour
{
static BuildGrid instance;
public static BuildGrid Instance
{
get
{
if (instance == null)
{
instance = FindFirstObjectByType<BuildGrid>();
if (instance == null) instance = new GameObject("BuildGrid").AddComponent<BuildGrid>();
}
return instance;
}
}
[Header("Где можно строить")]
[Tooltip("Посчитать область самому: старые участки + Ратуша + запас")]
public bool autoArea = true;
[Tooltip("Запас вокруг старых участков, клеток")]
public int padding = 3;
public Vector2 areaMin = new Vector2(-34, -10);
public Vector2 areaMax = new Vector2(4, 12);
[Header("Вид сетки")]
public int overlaySortingOrder = 1;
public Color lineColor = new Color(1f, 1f, 1f, 0.28f);
public Color freeColor = new Color(0.4f, 1f, 0.5f, 0.06f);
public Color busyColor = new Color(1f, 0.15f, 0.15f, 0.38f);
[Tooltip("Спрятать старые участки и площадки «Застройка» при запуске")]
public bool hideLegacySlots = true;
public Vector2Int Origin { get; private set; } // мировые координаты левого нижнего угла клетки (0, 0)
public int Width { get; private set; }
public int Height { get; private set; }
bool[,] busy;
bool initialized;
float nextRefresh;
SpriteRenderer overlay;
Texture2D overlayTex;
string lastOverlayKey;
const int PixelsPerCell = 16;
void Awake()
{
if (instance != null && instance != this) { Destroy(this); return; }
instance = this;
}
void OnDestroy()
{
if (instance == this) instance = null;
if (overlayTex != null) Destroy(overlayTex);
}
void Update()
{
if (overlay != null && overlay.enabled && Time.unscaledTime >= nextRefresh) Refresh();
}
// ---------------- область ----------------
void EnsureInit()
{
if (initialized) return;
initialized = true;
Rect area = autoArea ? AutoArea() : Rect.MinMaxRect(areaMin.x, areaMin.y, areaMax.x, areaMax.y);
Origin = new Vector2Int(Mathf.FloorToInt(area.xMin), Mathf.FloorToInt(area.yMin));
Width = Mathf.Max(1, Mathf.CeilToInt(area.xMax) - Origin.x);
Height = Mathf.Max(1, Mathf.CeilToInt(area.yMax) - Origin.y);
busy = new bool[Width, Height];
if (hideLegacySlots) HideLegacySlots();
Debug.Log($"[СТРОЙКА] Сетка {Width}×{Height} клеток, левый нижний угол {Origin}");
}
Rect AutoArea()
{
bool any = false;
Bounds b = new Bounds();
foreach (var slot in FindObjectsByType<BuildSlot>(FindObjectsInactive.Include, FindObjectsSortMode.None))
{
Bounds sb = new Bounds(slot.transform.position, Vector3.one);
var col = slot.GetComponent<Collider2D>();
if (col != null && col.enabled && col.gameObject.activeInHierarchy) sb.Encapsulate(col.bounds);
if (!any) { b = sb; any = true; } else b.Encapsulate(sb);
}
if (TownHall.Instance != null)
{
Bounds tb = TownHallBounds();
if (!any) { b = tb; b.Expand(new Vector3(24, 16, 0)); any = true; }
else b.Encapsulate(tb);
}
if (!any) b = new Bounds(Vector3.zero, new Vector3(30, 20, 0));
b.Expand(new Vector3(padding * 2, padding * 2, 0));
return Rect.MinMaxRect(b.min.x, b.min.y, b.max.x, b.max.y);
}
static Bounds TownHallBounds()
{
var th = TownHall.Instance;
var col = th.GetComponent<Collider2D>();
if (col != null && col.enabled) return col.bounds;
var sr = th.GetComponentInChildren<SpriteRenderer>();
return sr != null ? sr.bounds : new Bounds(th.transform.position, new Vector3(3, 3, 0));
}
static void HideLegacySlots()
{
foreach (var slot in FindObjectsByType<BuildSlot>(FindObjectsInactive.Include, FindObjectsSortMode.None))
{
HideOn(slot.gameObject);
var parent = slot.transform.parent;
if (parent != null && parent.name.StartsWith("Застройка")) HideOn(parent.gameObject);
}
}
// Прячем только сам объект (картинку и коллайдер), не детей -- и сцену не меняем
static void HideOn(GameObject go)
{
foreach (var sr in go.GetComponents<SpriteRenderer>()) sr.enabled = false;
foreach (var c in go.GetComponents<Collider2D>()) c.enabled = false;
}
// ---------------- клетки ----------------
public bool InBounds(int x, int y) => x >= 0 && y >= 0 && x < Width && y < Height;
public bool IsBusy(int x, int y) { EnsureInit(); return !InBounds(x, y) || busy[x, y]; }
// Клетка мира -> индекс сетки
public Vector2Int WorldToCell(Vector2 world)
{
EnsureInit();
return new Vector2Int(Mathf.FloorToInt(world.x) - Origin.x, Mathf.FloorToInt(world.y) - Origin.y);
}
// Центр прямоугольника клеток (x, y, w, h) в мире
public Vector2 RectCenter(Vector2Int cell, Vector2Int size)
{
EnsureInit();
return new Vector2(Origin.x + cell.x + size.x * 0.5f, Origin.y + cell.y + size.y * 0.5f);
}
// Левая нижняя клетка для здания размера size, чей центр ближе всего к точке мира
public Vector2Int SnapCell(Vector2 worldCenter, Vector2Int size)
{
EnsureInit();
return new Vector2Int(Mathf.RoundToInt(worldCenter.x - Origin.x - size.x * 0.5f),
Mathf.RoundToInt(worldCenter.y - Origin.y - size.y * 0.5f));
}
// Можно ли поставить: все клетки внутри области и свободны
public bool CanPlace(Vector2Int cell, Vector2Int size)
{
EnsureInit();
for (int x = cell.x; x < cell.x + size.x; x++)
for (int y = cell.y; y < cell.y + size.y; y++)
if (!InBounds(x, y) || busy[x, y]) return false;
return true;
}
// Ближайшее свободное место для здания размера size рядом с точкой (по спирали). false -- места нет
public bool FindFreeNear(Vector2 worldCenter, Vector2Int size, out Vector2Int cell)
{
Refresh();
Vector2Int start = SnapCell(worldCenter, size);
int maxR = Mathf.Max(Width, Height);
for (int r = 0; r <= maxR; r++)
{
for (int dx = -r; dx <= r; dx++)
for (int dy = -r; dy <= r; dy++)
{
if (Mathf.Max(Mathf.Abs(dx), Mathf.Abs(dy)) != r) continue; // только кольцо радиуса r
var c = new Vector2Int(start.x + dx, start.y + dy);
if (CanPlace(c, size)) { cell = c; return true; }
}
}
cell = start;
return false;
}
// Пересчитать занятые клетки по тому, что стоит в городе сейчас
public void Refresh()
{
EnsureInit();
nextRefresh = Time.unscaledTime + 0.3f;
System.Array.Clear(busy, 0, busy.Length);
if (TownHall.Instance != null)
{
Bounds tb = TownHallBounds();
MarkRect(new Rect(tb.min.x, tb.min.y, tb.size.x, tb.size.y));
}
var bm = BuildManager.Instance;
foreach (var id in FindObjectsByType<PlacedBuildingId>(FindObjectsSortMode.None))
{
if (string.IsNullOrEmpty(id.buildingName)) continue; // здание сносится в этом кадре
BuildingData data = bm != null ? bm.FindData(id.buildingName) : null;
Rect r;
if (data != null)
{
var fp = BuildFootprint.Of(data);
Vector2 center = (Vector2)id.transform.position + fp.anchorOffset;
r = new Rect(center.x - fp.size.x * 0.5f, center.y - fp.size.y * 0.5f, fp.size.x, fp.size.y);
}
else
{
var col = id.GetComponent<Collider2D>();
if (col == null) continue;
r = new Rect(col.bounds.min.x, col.bounds.min.y, col.bounds.size.x, col.bounds.size.y);
}
MarkRect(r);
}
if (overlay != null && overlay.enabled) RedrawOverlay();
}
// Отметить все клетки, которые прямоугольник мира накрывает (касание краем не считается)
void MarkRect(Rect r)
{
const float eps = 0.05f;
int x0 = Mathf.FloorToInt(r.xMin + eps) - Origin.x, x1 = Mathf.CeilToInt(r.xMax - eps) - Origin.x;
int y0 = Mathf.FloorToInt(r.yMin + eps) - Origin.y, y1 = Mathf.CeilToInt(r.yMax - eps) - Origin.y;
for (int x = Mathf.Max(0, x0); x < Mathf.Min(Width, x1); x++)
for (int y = Mathf.Max(0, y0); y < Mathf.Min(Height, y1); y++)
busy[x, y] = true;
}
// ---------------- рисунок сетки ----------------
public void ShowOverlay(bool show)
{
EnsureInit();
if (show && overlay == null) CreateOverlay();
if (overlay == null) return;
overlay.enabled = show;
if (show) Refresh();
}
void CreateOverlay()
{
overlayTex = new Texture2D(Width * PixelsPerCell, Height * PixelsPerCell, TextureFormat.RGBA32, false);
overlayTex.filterMode = FilterMode.Point;
overlayTex.wrapMode = TextureWrapMode.Clamp;
var sprite = Sprite.Create(overlayTex, new Rect(0, 0, overlayTex.width, overlayTex.height), Vector2.zero, PixelsPerCell);
var go = new GameObject("BuildGridOverlay");
go.transform.SetParent(transform, false);
go.transform.position = new Vector3(Origin.x, Origin.y, 0f);
overlay = go.AddComponent<SpriteRenderer>();
overlay.sprite = sprite;
overlay.sortingOrder = overlaySortingOrder;
lastOverlayKey = null;
}
void RedrawOverlay()
{
// перерисовываем только если занятость поменялась
var key = new System.Text.StringBuilder(Width * Height);
for (int y = 0; y < Height; y++) for (int x = 0; x < Width; x++) key.Append(busy[x, y] ? '1' : '0');
string k = key.ToString();
if (k == lastOverlayKey) return;
lastOverlayKey = k;
int w = overlayTex.width, h = overlayTex.height;
var px = new Color32[w * h];
Color32 line = lineColor, free = freeColor, bad = busyColor;
for (int y = 0; y < h; y++)
{
int cy = y / PixelsPerCell, iy = y % PixelsPerCell;
for (int x = 0; x < w; x++)
{
int cx = x / PixelsPerCell, ix = x % PixelsPerCell;
bool edge = ix == 0 || iy == 0 || x == w - 1 || y == h - 1;
px[y * w + x] = busy[cx, cy] ? bad : (edge ? line : free);
if (busy[cx, cy] && edge) px[y * w + x] = new Color32(bad.r, bad.g, bad.b, (byte)Mathf.Min(255, bad.a + 50));
}
}
overlayTex.SetPixels32(px);
overlayTex.Apply(false);
}
}
// Размер здания на сетке и где его "пятно" относительно точки, в которую ставится префаб.
// size -- BuildingData.footprint, а если там 0 × 0 -- коллайдер префаба (иначе картинка), округлённый до клеток.
// anchorOffset -- центр пятна минус позиция префаба (у многих картинок точка привязки не в центре).
public struct BuildFootprint
{
public Vector2Int size;
public Vector2 anchorOffset;
static readonly Dictionary<BuildingData, BuildFootprint> cache = new Dictionary<BuildingData, BuildFootprint>();
public static BuildFootprint Of(BuildingData data)
{
if (data == null) return new BuildFootprint { size = Vector2Int.one };
if (cache.TryGetValue(data, out var fp)) return fp;
Rect r = LocalRect(data.prefab);
fp.anchorOffset = r.center;
if (data.footprint.x > 0 && data.footprint.y > 0) fp.size = data.footprint;
else fp.size = new Vector2Int(Mathf.Max(1, Mathf.RoundToInt(r.width)), Mathf.Max(1, Mathf.RoundToInt(r.height)));
cache[data] = fp;
return fp;
}
// Прямоугольник префаба относительно его позиции, в единицах мира.
// Считаем по трансформам руками: у выключенного шаблона collider.bounds не работает.
static Rect LocalRect(GameObject prefab)
{
if (prefab == null) return new Rect(-0.5f, -0.5f, 1, 1);
Vector3 root = prefab.transform.position;
var box = prefab.GetComponent<BoxCollider2D>();
if (box == null) box = prefab.GetComponentInChildren<BoxCollider2D>(true);
if (box != null)
{
Vector2 half = box.size * 0.5f;
return Corners(box.transform, box.offset - half, box.offset + half, root);
}
bool any = false;
Rect result = new Rect();
foreach (var sr in prefab.GetComponentsInChildren<SpriteRenderer>(true))
{
if (sr.sprite == null) continue;
var b = sr.sprite.bounds;
Rect one = Corners(sr.transform, b.min, b.max, root);
if (!any) { result = one; any = true; }
else result = Rect.MinMaxRect(Mathf.Min(result.xMin, one.xMin), Mathf.Min(result.yMin, one.yMin),
Mathf.Max(result.xMax, one.xMax), Mathf.Max(result.yMax, one.yMax));
}
return any ? result : new Rect(-0.5f, -0.5f, 1, 1);
}
static Rect Corners(Transform t, Vector2 min, Vector2 max, Vector3 root)
{
Vector3 a = t.TransformPoint(new Vector3(min.x, min.y)) - root;
Vector3 b = t.TransformPoint(new Vector3(max.x, max.y)) - root;
Vector3 c = t.TransformPoint(new Vector3(min.x, max.y)) - root;
Vector3 d = t.TransformPoint(new Vector3(max.x, min.y)) - root;
return Rect.MinMaxRect(Mathf.Min(a.x, b.x, c.x, d.x), Mathf.Min(a.y, b.y, c.y, d.y),
Mathf.Max(a.x, b.x, c.x, d.x), Mathf.Max(a.y, b.y, c.y, d.y));
}
}