3D: общий HUD, здания игры в 3D (окна, рабочие, улучшения, стройка по таймеру, сохранение), окна зданий с прокруткой

This commit is contained in:
2026-11-05 14:18:22 +03:00
parent 235ff124ee
commit 88f460a4a6
16 changed files with 16356 additions and 37 deletions
+177 -29
View File
@@ -16,9 +16,17 @@ using UnityEngine;
// - зоны по уровню Ратуши (кнопки «Ратуша -/+» -- ТОЛЬКО ДЛЯ ТЕСТА), камера плавно показывает новую зону.
// Правила места -- в модуле Поселения (Bogatyri.Settlement.Placement, тесты PlacementTests), здесь только показ и ввод.
//
// Общий HUD (05.11, Валдемар): в сцене стоит префаб HUD_Canvas -- тот же, что в 2D-городе. Нажатия по HUD
// в сцену не проходят; кнопка "СТРОЙКА" HUD открывает список зданий (BuildModeController.ExternalToggle);
// временные кнопки этой сцены сдвинуты, чтобы не налезать на HUD.
//
// Здания игры (05.11, Валдемар, City3DGameplay): у каждого 3D-здания те же компоненты, что у 2D-префабов --
// тап открывает окно здания (рабочие, добыча, улучшение по таймеру), стройка -- с ценой, лимитом, строителями и
// таймером (BuildTimer), Ратуша -- настоящая (TownHall, её уровень открывает зоны). Город сохраняется (City3DSave).
//
// Пока НЕ подключено (этапы II-III): цена и таймер стройки, Экономика и награда за расчистку, сохранение,
// дороги, настоящие модели. Кнопки -- временные (IMGUI); окна согласуются с Володей.
public class City3DController : MonoBehaviour
public class City3DController : MonoBehaviour, City3DSave.ILive
{
// Тип здания для прототипа: максимальный габарит (клеток) с первого уровня, высота куба, цвет
struct BuildingType
@@ -79,6 +87,112 @@ public class City3DController : MonoBehaviour
// ---------------- Построение сцены ----------------
bool buildMenuOpen; // список "что построить" открывается кнопкой "СТРОЙКА" общего HUD
// HUD закрывает верх (ресурсы) и низ (кнопки) экрана -- временные кнопки сцены ставим между ними (в единицах 720)
const float HudTop = 150f, HudBottom = 150f, HudLeft = 160f;
void ToggleBuildMenu()
{
if (mode == Mode.Place || mode == Mode.Move) { CancelGhost(); buildMenuOpen = false; return; }
buildMenuOpen = !buildMenuOpen;
if (UIPanelManager.Instance != null) UIPanelManager.Instance.CloseAll();
}
bool stateApplied; // сохранённый город уже поставлен (в первом кадре, после загрузки сохранения в Start)
void OnEnable()
{
BuildModeController.ExternalToggle = ToggleBuildMenu;
BuildModeController.ExternalMove = MoveFromWindow;
CitySync.ExternalCity = true;
City3DSave.Live = this;
EventBus.Subscribe<TownHallLevelChangedEvent>(OnTownHallLevel);
}
void OnDisable()
{
if (BuildModeController.ExternalToggle == (System.Action)ToggleBuildMenu) BuildModeController.ExternalToggle = null;
if (BuildModeController.ExternalMove == (System.Action<GameObject>)MoveFromWindow) BuildModeController.ExternalMove = null;
CitySync.ExternalCity = false;
// Сохраняемся до ухода из сцены (SceneNavigator, выход из игры) -- здесь здания могут быть уже убраны
if (City3DSave.Live == (City3DSave.ILive)this) City3DSave.Live = null;
EventBus.Unsubscribe<TownHallLevelChangedEvent>(OnTownHallLevel);
}
// ---------------- Сохранение (City3DSave) ----------------
public City3DSave.Data CaptureState()
{
var d = new City3DSave.Data { nextId = nextBuildingId };
foreach (var b in map.Buildings)
{
if (!buildingViews.TryGetValue(b.Id, out var view) || view == null) continue;
var e = new City3DSave.Entry { id = b.Id, type = b.Type, x = b.X, y = b.Y, rot = b.Rotation, level = City3DGameplay.LevelOf(view) };
City3DSave.WriteTimer(e, view);
d.buildings.Add(e);
}
d.cleared.AddRange(map.Cleared);
return d;
}
// Загрузили другое сохранение или сбросили -- город уже стоит, перестраиваем сцену целиком
public void OnSaveLoaded()
{
if (stateApplied) UnityEngine.SceneManagement.SceneManager.LoadScene(gameObject.scene.name);
}
void ApplySaved()
{
stateApplied = true;
var d = City3DSave.Cached;
int c = City3DLayout.Size / 2;
bool hasTownHall = false;
if (d != null)
{
nextBuildingId = Mathf.Max(1, d.nextId);
map.RestoreCleared(d.cleared);
foreach (var id in d.cleared)
if (obstacleViews.TryGetValue(id, out var ov)) { Destroy(ov); obstacleViews.Remove(id); }
// Ратуша первой: её уровень открывает зоны
var list = new List<City3DSave.Entry>(d.buildings);
list.Sort((a, b) => (b.type == "Ратуша").CompareTo(a.type == "Ратуша"));
foreach (var e in list)
{
int type = System.Array.FindIndex(Types, t => t.Name == e.type);
if (type < 0) continue;
var t = Types[type];
var rect = new PlacedRect { Id = e.id, Type = t.Name, X = e.x, Y = e.y, SizeW = t.W, SizeH = t.H, Rotation = e.rot };
if (!map.Restore(rect)) continue;
buildingType[e.id] = type;
buildingViews[e.id] = CreateBuildingView(e.id, type);
City3DGameplay.Attach(buildingViews[e.id], t.Name, e.level);
City3DSave.ReadTimer(e, buildingViews[e.id]);
UpdateBuildingView(e.id);
if (type == 0) { hasTownHall = true; townHallLevel = Mathf.Clamp(e.level, 1, 5); }
}
}
if (!hasTownHall) PlaceNew(0, c - 2, c - 2, 0); // новый город: Ратуша в центре
UpdateGrass();
RefreshGround();
if (CitySync.Instance != null) CitySync.Instance.SyncNow();
}
// Кнопка "Перенести" в окне здания
void MoveFromWindow(GameObject view)
{
foreach (var pair in buildingViews)
if (pair.Value == view)
{
if (mode == Mode.Place || mode == Mode.Move) CancelGhost();
buildMenuOpen = false;
StartMoving(pair.Key);
return;
}
}
void OnTownHallLevel(TownHallLevelChangedEvent e) => ApplyTownHallLevel(e.Level);
void Start()
{
cam = Camera.main;
@@ -89,6 +203,7 @@ public class City3DController : MonoBehaviour
cameraRig.Apply();
Map3D.SetUpLook(cam, 30f, 85f); // солнце, небо, дымка вдали
City3DGameplay.PutHudInFront(cam); // общий HUD -- поверх холмов и деревьев
layout = City3DLayout.Create();
map = layout.Map;
@@ -96,9 +211,8 @@ public class City3DController : MonoBehaviour
BuildObstacles();
BuildGrass();
// Ратуша стоит в центре с самого начала
int c = City3DLayout.Size / 2;
PlaceNew(0, c - 2, c - 2, 0);
// Ратуша и остальные здания -- из сохранения (или новая Ратуша в центре): в первом кадре, когда сохранение
// уже загружено (оно грузится в Start), см. ApplySaved
RefreshGround();
}
@@ -259,6 +373,7 @@ public class City3DController : MonoBehaviour
nextBuildingId++;
buildingType[id] = type;
buildingViews[id] = CreateBuildingView(id, type);
City3DGameplay.Attach(buildingViews[id], t.Name, 1); // здание игры: окно, рабочие, уровни (без цены и таймера)
UpdateBuildingView(id);
UpdateGrass();
return true;
@@ -464,8 +579,15 @@ public class City3DController : MonoBehaviour
{
int type = placingType;
int x = ghostX, y = ghostY, rot = ghostRot;
string problem = City3DGameplay.CanBuild(Types[type].Name); // уровень Ратуши, лимит, строители, ресурсы
if (problem != null) { Say(problem); return; }
string id = "b_" + nextBuildingId;
CancelGhost();
if (PlaceNew(type, x, y, rot)) Say(Types[type].Name + " построено (прототип: без цены и таймера)");
if (PlaceNew(type, x, y, rot))
{
City3DGameplay.StartConstruction(buildingViews[id]); // цена и стройка по времени (BuildTimer)
Say(Types[type].Name + ": стройка началась");
}
}
else if (mode == Mode.Move)
{
@@ -508,6 +630,7 @@ public class City3DController : MonoBehaviour
void Update()
{
if (!stateApplied && map != null) ApplySaved();
if (cam == null) return;
bool down, held, up;
Vector3 pos;
@@ -530,7 +653,7 @@ public class City3DController : MonoBehaviour
if (down)
{
pointerOverGui = OverGui(pos);
pointerOverGui = OverGui(pos) || UIBlocker.IsPointerOverUI(); // и кнопки/окна общего HUD
if (pointerOverGui) return;
pointerDown = pos;
pointerMoved = false;
@@ -585,6 +708,14 @@ public class City3DController : MonoBehaviour
}
string building = map.BuildingAt(cx, cy);
if (mode == Mode.Edit && building != null) { StartMoving(building); return; }
// Тап по зданию -- окно здания игры (как в 2D): рабочие, добыча, улучшение, перенос
if (building != null && buildingViews.TryGetValue(building, out var tapped) && tapped != null)
{
selectedBuilding = selectedObstacle = null;
buildMenuOpen = false;
BuildingWindow.Open(tapped);
return;
}
selectedBuilding = building;
selectedObstacle = building == null ? map.ObstacleAt(cx, cy) : null;
}
@@ -623,10 +754,24 @@ public class City3DController : MonoBehaviour
if (ghost != null) { var l = ghost.transform.Find("Label"); if (l != null) l.rotation = cam.transform.rotation; }
}
// Кнопки "Ратуша −/+" -- только для теста: меняют уровень настоящей Ратуши (TownHall)
void ChangeTownHall(int delta)
{
var th = TownHall.Instance;
int level = Mathf.Clamp((th != null ? th.currentLevel : townHallLevel) + delta, 1, th != null ? th.maxLevel : 5);
if (th != null)
{
th.currentLevel = level;
EventBus.Publish(new TownHallLevelChangedEvent { Level = level }); // зоны обновит OnTownHallLevel
}
else ApplyTownHallLevel(level);
}
// Уровень Ратуши изменился (улучшение в окне Ратуши или тест): открыть зону
void ApplyTownHallLevel(int level)
{
int before = townHallLevel;
townHallLevel = Mathf.Clamp(townHallLevel + delta, 1, 5);
townHallLevel = Mathf.Clamp(level, 1, 5);
if (townHallLevel == before) return;
RefreshGround();
if (ghost != null) UpdateGhost();
@@ -685,13 +830,16 @@ public class City3DController : MonoBehaviour
}
if (Event.current.type == EventType.Layout) guiRects.Clear();
// Верх: режим, Ратуша (тест), редактирование
Box(new Rect(10, 10, 330, 44), $"3D-поселение (прототип) • Ратуша {townHallLevel} ур.");
if (Button(new Rect(350, 10, 60, 44), "−")) ChangeTownHall(-1);
if (Button(new Rect(415, 10, 60, 44), "+")) ChangeTownHall(+1);
// Окно HUD открыто -- временные кнопки сцены не показываем (и не ловим нажатия)
if (UIPanelManager.Instance != null && UIPanelManager.Instance.CurrentId != "") return;
// Под верхней панелью HUD, справа: Ратуша (тест), редактирование
Box(new Rect(w - 480, HudTop, 290, 44), $"3D (прототип) • Ратуша {townHallLevel} ур.");
if (Button(new Rect(w - 185, HudTop, 55, 44), "−")) ChangeTownHall(-1);
if (Button(new Rect(w - 125, HudTop, 55, 44), "+")) ChangeTownHall(+1);
if (mode == Mode.View || mode == Mode.Edit)
{
if (Button(new Rect(w - 250, 10, 240, 44), mode == Mode.Edit ? "Готово" : "Редактировать"))
if (Button(new Rect(w - 250, HudTop + 50, 240, 44), mode == Mode.Edit ? "Готово" : "Редактировать"))
{
mode = mode == Mode.Edit ? Mode.View : Mode.Edit;
selectedBuilding = selectedObstacle = null;
@@ -700,13 +848,13 @@ public class City3DController : MonoBehaviour
}
}
// Слева: что построить
if (mode == Mode.View || mode == Mode.Edit)
// Слева, правее кнопок HUD: что построить -- открывается кнопкой "СТРОЙКА" общего HUD
if (buildMenuOpen && (mode == Mode.View || mode == Mode.Edit))
{
float y = 64;
float y = HudTop;
for (int i = 1; i < Types.Length; i++)
{
if (Button(new Rect(10, y, 200, 40), $"{Types[i].Name} {Types[i].W}×{Types[i].H}")) StartPlacing(i);
if (Button(new Rect(HudLeft, y, 200, 40), $"{Types[i].Name} {Types[i].W}×{Types[i].H}")) { buildMenuOpen = false; StartPlacing(i); }
y += 44;
}
}
@@ -717,39 +865,39 @@ public class City3DController : MonoBehaviour
string status = ghostError == PlaceError.None
? (ghostFoundation > 0 ? $"Можно ставить (фундамент {ghostFoundation / 1000f:0.0} м)" : "Можно ставить")
: SettlementMap.ErrorText(ghostError);
Box(new Rect(w / 2 - 300, h - 120, 600, 44), (mode == Mode.Move ? "Перенос: " : Types[GhostType].Name + ": ") + status);
if (Button(new Rect(w / 2 - 300, h - 70, 190, 60), "Повернуть 90°")) { ghostRot = (ghostRot + 90) % 360; UpdateGhost(); }
if (Button(new Rect(w / 2 - 95, h - 70, 190, 60), mode == Mode.Move ? "Подтвердить" : "Поставить")) ConfirmGhost();
if (Button(new Rect(w / 2 + 110, h - 70, 190, 60), "Отмена")) CancelGhost();
Box(new Rect(w / 2 - 300, h - HudBottom - 120, 600, 44), (mode == Mode.Move ? "Перенос: " : Types[GhostType].Name + ": ") + status);
if (Button(new Rect(w / 2 - 300, h - HudBottom - 70, 190, 60), "Повернуть 90°")) { ghostRot = (ghostRot + 90) % 360; UpdateGhost(); }
if (Button(new Rect(w / 2 - 95, h - HudBottom - 70, 190, 60), mode == Mode.Move ? "Подтвердить" : "Поставить")) ConfirmGhost();
if (Button(new Rect(w / 2 + 110, h - HudBottom - 70, 190, 60), "Отмена")) CancelGhost();
}
// Меню здания: перенос
if (mode == Mode.View && selectedBuilding != null && map.TryGetBuilding(selectedBuilding, out var b))
{
Box(new Rect(w / 2 - 250, h - 120, 500, 44), $"{b.Type} • {b.W}×{b.H} клеток • поворот {b.Rotation}°");
if (Button(new Rect(w / 2 - 250, h - 70, 245, 60), "Переместить")) { string id = selectedBuilding; selectedBuilding = null; StartMoving(id); }
if (Button(new Rect(w / 2 + 5, h - 70, 245, 60), "Закрыть")) selectedBuilding = null;
Box(new Rect(w / 2 - 250, h - HudBottom - 120, 500, 44), $"{b.Type} • {b.W}×{b.H} клеток • поворот {b.Rotation}°");
if (Button(new Rect(w / 2 - 250, h - HudBottom - 70, 245, 60), "Переместить")) { string id = selectedBuilding; selectedBuilding = null; StartMoving(id); }
if (Button(new Rect(w / 2 + 5, h - HudBottom - 70, 245, 60), "Закрыть")) selectedBuilding = null;
}
// Меню дерева / валуна: расчистка без цены и ожидания
if (mode == Mode.View && selectedObstacle != null)
{
bool tree = selectedObstacle.StartsWith("tree");
Box(new Rect(w / 2 - 250, h - 120, 500, 44), tree ? "Дерево" : "Валун");
if (Button(new Rect(w / 2 - 250, h - 70, 245, 60), "Расчистить"))
Box(new Rect(w / 2 - 250, h - HudBottom - 120, 500, 44), tree ? "Дерево" : "Валун");
if (Button(new Rect(w / 2 - 250, h - HudBottom - 70, 245, 60), "Расчистить"))
{
if (map.ClearObstacle(selectedObstacle) && obstacleViews.TryGetValue(selectedObstacle, out var view))
{
Destroy(view);
Say((tree ? "Дерево" : "Валун") + " расчищено. Награда " + (tree ? "деревом" : "камнем") +
" -- после подключения сохранений (этап III)");
City3DGameplay.RewardClearing(tree); // +10 дерева или камня, один раз (решение 04.11)
Say((tree ? "Дерево" : "Валун") + " расчищено: +10 " + (tree ? "дерева" : "камня"));
}
selectedObstacle = null;
}
if (Button(new Rect(w / 2 + 5, h - 70, 245, 60), "Закрыть")) selectedObstacle = null;
if (Button(new Rect(w / 2 + 5, h - HudBottom - 70, 245, 60), "Закрыть")) selectedObstacle = null;
}
if (Time.unscaledTime < messageUntil)
GUI.Label(new Rect(w / 2 - 350, 64, 700, 60), message, label);
GUI.Label(new Rect(w / 2 - 350, HudTop, 700, 60), message, label);
}
}