373 lines
16 KiB
C#
373 lines
16 KiB
C#
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;
|
||
|
||
// Здание, которое сейчас переносят: его клетки считаются свободными (режим стройки, перенос)
|
||
public static GameObject IgnoredBuilding;
|
||
[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; // здание сносится в этом кадре
|
||
if (IgnoredBuilding != null && id.gameObject == IgnoredBuilding) 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));
|
||
}
|
||
}
|