3D: герой у Терема -- гуляет по свободным клеткам рядом с Теремом, тап -- фраза и окно героя (перенос из 2D)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Alexander
2026-11-06 16:42:03 +01:00
co-authored by Claude Opus 5.5
parent 6d36c14313
commit 538dbea222
4 changed files with 226 additions and 0 deletions
+28
View File
@@ -828,6 +828,7 @@ public class City3DController : MonoBehaviour, City3DSave.ILive
if (!stateApplied && map != null) ApplySaved();
if (cam == null) return;
UpdateConstructionViews();
UpdateHero();
bool down, held, up;
Vector3 pos;
if (Input.touchCount > 0)
@@ -898,6 +899,12 @@ public class City3DController : MonoBehaviour, City3DSave.ILive
void Tap(Vector3 pos)
{
// Тап по герою -- фраза и окно героя (как в 2D)
if (mode == Mode.View && City3DHero.Instance != null && !City3DHero.Instance.Hidden)
{
foreach (var hit in Physics.RaycastAll(cam.ScreenPointToRay(pos), 500f))
if (hit.collider.GetComponentInParent<City3DHero>() != null) { City3DHero.Instance.OnTapped(); return; }
}
if (!CellUnder(pos, mode == Mode.Place || mode == Mode.Move, out int cx, out int cy)) return;
if (mode == Mode.Place || mode == Mode.Move)
{
@@ -1015,6 +1022,27 @@ public class City3DController : MonoBehaviour, City3DSave.ILive
float guiScale = 1f;
GUIStyle button, label, box;
// ---------------- Герой у Терема (перенос из 2D) ----------------
// Терем достроен -- у его ворот герой; Терема нет -- героя нет. В режиме стройки и переноса прячется
void UpdateHero()
{
if (!stateApplied) return;
string castleId = null;
foreach (var pair in buildingType)
{
if (Types[pair.Value].Name != "Терем") continue;
if (!buildingViews.TryGetValue(pair.Key, out var view) || view == null || BuildTimer.IsConstructing(view.transform)) continue;
castleId = pair.Key;
break;
}
var hero = City3DHero.Instance;
if (castleId == null) { if (hero != null) Destroy(hero.gameObject); return; }
if (hero == null || hero.CastleId != castleId || hero.CastleView == null)
hero = City3DHero.Spawn(transform, map, castleId, buildingViews[castleId], () => townHallLevel);
hero.Hidden = mode == Mode.Place || mode == Mode.Move;
}
// ---------------- Стройка по времени в 3D ----------------
// BuildTimer затемняет только 2D-спрайты, поэтому в 3D здание, которое ещё строится, выглядело готовым
// (Казармы «построены», а нанимать нельзя). Здесь: здание темнее и над ним -- «Стройка 0:45» (05.11)
+186
View File
@@ -0,0 +1,186 @@
using Bogatyri.Settlement.Placement;
using UnityEngine;
// ГЕРОЙ В 3D-ПОСЕЛЕНИИ (перенос из 2D, решение 05.11: всё из 2D-города -- в 3D). Как HeroWalker в 2D-городе:
// - живёт у Терема богатыря: появляется у его ворот, когда Терем достроен, и исчезает, если Терема нет;
// - гуляет по свободным клеткам 3D-карты (земля, открытая зона, без зданий и деревьев) не дальше WalkRadius от Терема:
// шаг -- на соседнюю свободную клетку, поэтому сквозь здания и деревья не проходит; после нескольких шагов стоит 1-3 с;
// - тап по герою -- фраза и окно героя (окно Терема); в режиме стройки и переноса прячется;
// - пока без арта: фигурка из простых форм (тело, голова, шлем, щит, копьё). Арт подставим в BuildFigure.
// Только оживление города: на экономику, бой и сохранение не влияет. Создаёт и убирает City3DController.
public class City3DHero : MonoBehaviour
{
public static City3DHero Instance { get; private set; }
const float Speed = 1.3f; // клеток в секунду
const int WalkRadius = 6; // насколько далеко уходит от Терема
static readonly string[] Phrases =
{
"Город под защитой!",
"Веди, воевода, — не подведу.",
"Силушка богатырская при мне.",
"Дозором обхожу владения.",
"Есть ли где ворог? Только скажи.",
};
SettlementMap map;
System.Func<int> townHallLevel;
public string CastleId { get; private set; }
public GameObject CastleView { get; private set; }
Transform figure;
Vector2Int cell; // клетка, к которой идёт (или на которой стоит)
Vector2Int previous; // откуда пришёл -- чтобы не топтаться туда-обратно
int stepsLeft;
float waitLeft;
// Видимость: в режиме стройки и переноса герой прячется, чтобы не мешать ставить здания
public bool Hidden
{
get => figure != null && !figure.gameObject.activeSelf;
set { if (figure != null && figure.gameObject.activeSelf == value) figure.gameObject.SetActive(!value); }
}
public static City3DHero Spawn(Transform parent, SettlementMap map, string castleId, GameObject castleView, System.Func<int> townHallLevel)
{
if (Instance != null) Destroy(Instance.gameObject);
var go = new GameObject("Hero3D");
go.transform.SetParent(parent, false);
var h = go.AddComponent<City3DHero>();
h.map = map;
h.CastleId = castleId;
h.CastleView = castleView;
h.townHallLevel = townHallLevel;
h.BuildFigure();
h.cell = h.previous = h.Gate();
go.transform.position = CellWorld(h.cell);
h.waitLeft = 1f;
Instance = h;
return h;
}
void OnDestroy()
{
if (Instance == this) Instance = null;
}
// ---------------- Внешний вид (заглушка) ----------------
void BuildFigure()
{
figure = new GameObject("Figure").transform;
figure.SetParent(transform, false);
Part(PrimitiveType.Capsule, new Vector3(0, 0.32f, 0), new Vector3(0.28f, 0.3f, 0.22f), new Color(0.55f, 0.4f, 0.2f)); // кафтан
Part(PrimitiveType.Sphere, new Vector3(0, 0.72f, 0), new Vector3(0.2f, 0.2f, 0.2f), new Color(1f, 0.85f, 0.7f)); // голова
Part(PrimitiveType.Sphere, new Vector3(0, 0.8f, 0), new Vector3(0.22f, 0.14f, 0.22f), new Color(0.75f, 0.75f, 0.8f)); // шлем
Part(PrimitiveType.Cylinder, new Vector3(0, 0.9f, 0), new Vector3(0.03f, 0.06f, 0.03f), new Color(0.75f, 0.75f, 0.8f)); // шишак
Part(PrimitiveType.Cube, new Vector3(0.17f, 0.36f, 0.04f), new Vector3(0.04f, 0.26f, 0.2f), new Color(0.8f, 0.15f, 0.15f)); // щит
Part(PrimitiveType.Cylinder, new Vector3(-0.17f, 0.5f, 0), new Vector3(0.025f, 0.5f, 0.025f), new Color(0.45f, 0.3f, 0.15f)); // копьё
Part(PrimitiveType.Cylinder, new Vector3(0, 0.005f, 0), new Vector3(0.36f, 0.005f, 0.36f), new Color(0.18f, 0.22f, 0.14f)); // тень
// Тап по герою: коллайдер чуть больше фигурки (City3DController ищет City3DHero под пальцем)
var hit = gameObject.AddComponent<BoxCollider>();
hit.center = new Vector3(0, 0.5f, 0);
hit.size = new Vector3(0.7f, 1.1f, 0.7f);
}
void Part(PrimitiveType type, Vector3 pos, Vector3 scale, Color color)
{
var p = GameObject.CreatePrimitive(type);
Destroy(p.GetComponent<Collider>());
p.transform.SetParent(figure, false);
p.transform.localPosition = pos;
p.transform.localScale = scale;
p.GetComponent<Renderer>().sharedMaterial = Map3D.Mat(color);
}
// ---------------- Прогулка по клеткам ----------------
static Vector3 CellWorld(Vector2Int c)
{
float x = c.x + 0.5f, z = c.y + 0.5f;
return new Vector3(x, City3DLayout.HeightAt(x, z), z);
}
bool Walkable(int x, int y)
{
if (map == null || !map.InMap(x, y)) return false;
if (map.KindAt(x, y) != CellKind.Ground || !map.IsOpen(x, y, townHallLevel != null ? townHallLevel() : 1)) return false;
return map.BuildingAt(x, y) == null && map.ObstacleAt(x, y) == null;
}
bool CastleRect(out PlacedRect r)
{
r = default;
return map != null && map.TryGetBuilding(CastleId, out r);
}
bool NearHome(int x, int y)
{
if (!CastleRect(out var r)) return true;
float cx = r.X + r.W / 2f, cy = r.Y + r.H / 2f;
return Mathf.Abs(x + 0.5f - cx) <= WalkRadius + r.W / 2f && Mathf.Abs(y + 0.5f - cy) <= WalkRadius + r.H / 2f;
}
// Ворота: ближайшая свободная клетка вокруг Терема (с нижней стороны -- первой)
Vector2Int Gate()
{
if (!CastleRect(out var r)) return Vector2Int.zero;
for (int ring = 1; ring <= WalkRadius; ring++)
for (int x = r.X - ring; x < r.X + r.W + ring; x++)
foreach (int y in new[] { r.Y - ring, r.Y + r.H - 1 + ring })
if (Walkable(x, y)) return new Vector2Int(x, y);
return new Vector2Int(r.X + r.W / 2, r.Y - 1);
}
static readonly Vector2Int[] Dirs = { Vector2Int.up, Vector2Int.down, Vector2Int.left, Vector2Int.right };
// Следующая клетка: соседняя свободная рядом с Теремом; назад -- только если больше некуда
Vector2Int NextCell()
{
var options = new System.Collections.Generic.List<Vector2Int>();
foreach (var d in Dirs)
{
var c = cell + d;
if (c != previous && Walkable(c.x, c.y) && NearHome(c.x, c.y)) options.Add(c);
}
if (options.Count == 0 && Walkable(previous.x, previous.y)) return previous;
return options.Count > 0 ? options[Random.Range(0, options.Count)] : cell;
}
void Update()
{
if (Hidden) return;
if (!Walkable(cell.x, cell.y) && cell != Gate()) { cell = previous = Gate(); transform.position = CellWorld(cell); } // на клетку поставили здание
if (waitLeft > 0f) { waitLeft -= Time.deltaTime; figure.localPosition = Vector3.zero; return; }
Vector3 goal = CellWorld(cell), pos = transform.position;
Vector3 flat = new Vector3(goal.x - pos.x, 0, goal.z - pos.z);
if (flat.magnitude < 0.03f)
{
if (stepsLeft <= 0) { stepsLeft = Random.Range(3, 7); waitLeft = Random.Range(1f, 3f); return; }
stepsLeft--;
var next = NextCell();
previous = cell;
cell = next;
return;
}
Vector3 step = flat.normalized * Speed * Time.deltaTime;
if (step.magnitude > flat.magnitude) step = flat;
pos += step;
pos.y = City3DLayout.HeightAt(pos.x, pos.z); // по земле, через холмы
transform.position = pos;
transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(flat), Time.deltaTime * 10f); // смотрит, куда идёт
figure.localPosition = new Vector3(0f, Mathf.Abs(Mathf.Sin(Time.time * 10f)) * 0.04f, 0f); // покачивание при ходьбе
}
// ---------------- Тап ----------------
public void OnTapped()
{
UIKit.Toast("Богатырь: " + Phrases[Random.Range(0, Phrases.Length)]);
waitLeft = 2f; // остановился поговорить
if (CastleView != null) BuildingWindow.Open(CastleView);
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8d4c2a7e1f5b4e93b6a1c0d9e2f73a58
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: