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

369 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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));
}
}