Карта мира в 2.5D на примитивах

Карта на земле (X/Z): замок, города, лагеря, ресурсные точки, отряды и метки разведки -- примитивы,
подписи смотрят в камеру; наклонная перспективная камера с тем же поведением (перетаскивание, щипок, тап);
туман -- в текстуре земли; солнце и 3D-рендерер URP для камеры (Map3D). Логика и окна не менялись.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Alexander
2026-11-04 14:55:41 +01:00
co-authored by Claude Opus 5.5
parent a3c4a99a79
commit 9a6e5a2e7b
10 changed files with 268 additions and 135 deletions
+52 -54
View File
@@ -48,7 +48,10 @@ public class WorldMapController : MonoBehaviour
const float CastleSize = 2.2f;
const float CitySize = 1.1f;
const float CampSize = 0.8f;
const float StartZoom = 8f;
const float StartDistance = 18f; // камера 2.5D: расстояние до земли
// Высота объектов над землёй (2.5D на примитивах, 04.11.2026)
const float CastleHeight = 1.4f, CityHeight = 0.9f, CampHeight = 0.5f, PointHeight = 0.45f;
static readonly Color GroundColor = new Color(0.3f, 0.42f, 0.25f);
static readonly Color CastleColor = new Color(0.55f, 0.15f, 0.15f);
@@ -67,10 +70,10 @@ public class WorldMapController : MonoBehaviour
private readonly List<(MapNode node, TextMeshPro label)> garrisonLabels = new List<(MapNode, TextMeshPro)>();
// Лагеря меняют вид: разбит -- серый, с таймером до появления
private readonly List<(MapNode node, SpriteRenderer sprite, TextMeshPro label)> campViews = new List<(MapNode, SpriteRenderer, TextMeshPro)>();
private readonly List<(MapNode node, Renderer sprite, TextMeshPro label)> campViews = new List<(MapNode, Renderer, TextMeshPro)>();
// Ресурсные точки появляются и исчезают -- виды создаются и убираются по ходу игры
private readonly Dictionary<string, (SpriteRenderer sprite, TextMeshPro label)> pointViews = new Dictionary<string, (SpriteRenderer, TextMeshPro)>();
private readonly Dictionary<string, (Renderer sprite, TextMeshPro label)> pointViews = new Dictionary<string, (Renderer, TextMeshPro)>();
private readonly List<string> stalePoints = new List<string>();
const float PointSize = 0.7f;
static readonly Color[] PointColors =
@@ -93,8 +96,8 @@ public class WorldMapController : MonoBehaviour
void Start()
{
mapRoot = new GameObject("Map").transform;
BuildMap();
IntelligenceOverlay.Create(mapRoot, World); // туман и чужие армии (модуль Разведки)
Renderer ground = BuildMap();
IntelligenceOverlay.Create(mapRoot, World, ground); // туман на земле и чужие армии (модуль Разведки)
SetUpCamera();
backButton.onClick.AddListener(SceneNavigator.ReturnToCity);
@@ -211,11 +214,13 @@ public class WorldMapController : MonoBehaviour
// ---------------- КАРТА ----------------
void BuildMap()
// Карта лежит на земле (2.5D): замок -- стена с башней, свой город -- кубик на белой площадке,
// соседи -- кубики, лагеря -- цилиндры, ресурсные точки -- ромбы. Подписи висят над объектами.
Renderer BuildMap()
{
float width = WorldMapLayout.HalfWidth / 1000f * 2f;
float height = WorldMapLayout.HalfHeight / 1000f * 2f;
CreateSprite("Ground", PrimitiveSprites.Square, GroundColor, Vector3.zero, new Vector3(width, height, 1f), -10);
Renderer ground = CreateGround(width, height);
foreach (var node in World.Nodes)
{
@@ -224,32 +229,50 @@ public class WorldMapController : MonoBehaviour
switch (node.type)
{
case MapNodeType.BossCastle:
CreateSprite(node.id, PrimitiveSprites.Square, CastleColor, pos, Vector3.one * CastleSize, 0);
CreateSprite(node.id + "_Keep", PrimitiveSprites.Square, new Color(0.3f, 0.08f, 0.08f), pos, Vector3.one * CastleSize * 0.5f, 1);
CreateLabel(node.displayName, pos + new Vector3(0, CastleSize / 2f + 0.4f, 0), 4f);
garrisonLabels.Add((node, CreateLabel("", pos - new Vector3(0, CastleSize / 2f + 0.35f, 0), 3f)));
Map3D.Block(mapRoot, node.id, PrimitiveType.Cube, CastleColor, pos, Vector2.one * CastleSize, CastleHeight * 0.6f);
Map3D.Block(mapRoot, node.id + "_Keep", PrimitiveType.Cube, new Color(0.3f, 0.08f, 0.08f), pos, Vector2.one * CastleSize * 0.5f, CastleHeight * 1.8f);
Map3D.Label(mapRoot, node.displayName, pos + Vector3.up * (CastleHeight * 1.8f + 1.1f), 4f);
garrisonLabels.Add((node, Map3D.Label(mapRoot, "", pos + Vector3.up * (CastleHeight * 1.8f + 0.45f), 3f)));
break;
case MapNodeType.PlayerCity:
// Белая рамка -- чтобы свой город сразу бросался в глаза
CreateSprite(node.id + "_Frame", PrimitiveSprites.Square, Color.white, pos, Vector3.one * (CitySize + 0.25f), 0);
CreateSprite(node.id, PrimitiveSprites.Square, PlayerCityColor, pos, Vector3.one * CitySize, 1);
CreateLabel(node.displayName, pos - new Vector3(0, CitySize / 2f + 0.35f, 0), 3.2f);
// Белая площадка -- чтобы свой город сразу бросался в глаза
Map3D.Block(mapRoot, node.id + "_Frame", PrimitiveType.Cube, Color.white, pos, Vector2.one * (CitySize + 0.35f), 0.12f);
Map3D.Block(mapRoot, node.id, PrimitiveType.Cube, PlayerCityColor, pos + Vector3.up * 0.12f, Vector2.one * CitySize, CityHeight);
Map3D.Label(mapRoot, node.displayName, pos + Vector3.up * (CityHeight + 0.75f), 3.2f);
break;
case MapNodeType.NeighborCity:
CreateSprite(node.id, PrimitiveSprites.Square, NeighborCityColor, pos, Vector3.one * CitySize, 0);
garrisonLabels.Add((node, CreateLabel("", pos - new Vector3(0, CitySize / 2f + 0.5f, 0), 2.6f)));
Map3D.Block(mapRoot, node.id, PrimitiveType.Cube, NeighborCityColor, pos, Vector2.one * CitySize, CityHeight);
garrisonLabels.Add((node, Map3D.Label(mapRoot, "", pos + Vector3.up * (CityHeight + 0.85f), 2.6f)));
break;
case MapNodeType.Camp:
// Круг, а не квадрат: лагерь не путается с городом
var sprite = CreateSprite(node.id, PrimitiveSprites.Circle, CampColor, pos, Vector3.one * CampSize, 0);
var label = CreateLabel("", pos - new Vector3(0, CampSize / 2f + 0.5f, 0), 2.3f);
// Цилиндр, а не куб: лагерь не путается с городом
var sprite = Map3D.Block(mapRoot, node.id, PrimitiveType.Cylinder, CampColor, pos, Vector2.one * CampSize, CampHeight);
var label = Map3D.Label(mapRoot, "", pos + Vector3.up * (CampHeight + 0.8f), 2.3f);
campViews.Add((node, sprite, label));
break;
}
}
return ground;
}
// Земля -- плоскость с текстурой: её раскрашивает туман (IntelligenceOverlay). UV (0,0) -- левый нижний угол карты
Renderer CreateGround(float width, float height)
{
var mesh = new Mesh { name = "WorldMap_Ground" };
float hx = width / 2f, hz = height / 2f;
mesh.vertices = new[] { new Vector3(-hx, 0, -hz), new Vector3(-hx, 0, hz), new Vector3(hx, 0, hz), new Vector3(hx, 0, -hz) };
mesh.uv = new[] { new Vector2(0, 0), new Vector2(0, 1), new Vector2(1, 1), new Vector2(1, 0) };
mesh.triangles = new[] { 0, 1, 2, 0, 2, 3 };
mesh.RecalculateNormals();
var go = new GameObject("Ground");
go.transform.SetParent(mapRoot, false);
go.AddComponent<MeshFilter>().sharedMesh = mesh;
var r = go.AddComponent<MeshRenderer>();
r.sharedMaterial = new Material(Map3D.Mat(GroundColor)) { color = Color.white };
return r;
}
void SetUpCamera()
@@ -263,7 +286,8 @@ public class WorldMapController : MonoBehaviour
}
mapCamera.mapHalfSize = new Vector2(WorldMapLayout.HalfWidth / 1000f, WorldMapLayout.HalfHeight / 1000f);
mapCamera.GetComponent<Camera>().orthographicSize = StartZoom;
mapCamera.distance = StartDistance;
Map3D.EnsureSun(); // в 2D-сцене света не было
CenterOnMyCity();
}
@@ -273,33 +297,6 @@ public class WorldMapController : MonoBehaviour
if (home != null) mapCamera.CenterOn(ToWorld(home));
}
SpriteRenderer CreateSprite(string name, Sprite sprite, Color color, Vector3 position, Vector3 scale, int order)
{
var go = new GameObject(name);
go.transform.SetParent(mapRoot, false);
go.transform.position = position;
go.transform.localScale = scale;
var sr = go.AddComponent<SpriteRenderer>();
sr.sprite = sprite;
sr.color = color;
sr.sortingOrder = order;
return sr;
}
TextMeshPro CreateLabel(string text, Vector3 position, float fontSize)
{
var go = new GameObject("Label");
go.transform.SetParent(mapRoot, false);
go.transform.position = position;
var tmp = go.AddComponent<TextMeshPro>();
tmp.text = text;
tmp.fontSize = fontSize;
tmp.alignment = TextAlignmentOptions.Center;
tmp.rectTransform.sizeDelta = new Vector2(6f, 1.2f);
tmp.sortingOrder = 10;
return tmp;
}
void RefreshCamps()
{
if (Time.unscaledTime < nextCampRefresh) return;
@@ -315,7 +312,7 @@ public class WorldMapController : MonoBehaviour
foreach (var (node, sprite, label) in campViews)
{
long left = World.CampRespawnInMs(node.id, now);
sprite.color = left > 0 ? CampClearedColor : CampColor;
sprite.sharedMaterial = Map3D.Mat(left > 0 ? CampClearedColor : CampColor);
label.text = left > 0 // разбил его сам игрок -- это он знает
? $"{node.displayName}\n<color=#BBBBBB>разбит, вернётся через {FormatMinutes(left)}</color>"
: $"{node.displayName}, ур. {node.level}\n{GarrisonText(node)}";
@@ -332,9 +329,9 @@ public class WorldMapController : MonoBehaviour
{
MapNode node = World.GetNode(p.id);
Vector3 pos = ToWorld(node);
var sprite = CreateSprite(p.id, PrimitiveSprites.Square, PointColors[p.kind], pos, Vector3.one * PointSize, 0);
sprite.transform.rotation = Quaternion.Euler(0, 0, 45); // ромб -- не путается с городом и лагерем
var label = CreateLabel("", pos - new Vector3(0, PointSize / 2f + 0.5f, 0), 2.2f);
var sprite = Map3D.Block(mapRoot, p.id, PrimitiveType.Cube, PointColors[p.kind], pos, Vector2.one * PointSize, PointHeight);
sprite.transform.rotation = Quaternion.Euler(0, 45, 0); // ромб -- не путается с городом и лагерем
var label = Map3D.Label(mapRoot, "", pos + Vector3.up * (PointHeight + 0.75f), 2.2f);
view = (sprite, label);
pointViews[p.id] = view;
}
@@ -384,9 +381,10 @@ public class WorldMapController : MonoBehaviour
return sec >= 60 ? $"{(sec + 59) / 60} мин" : $"{sec} сек";
}
// Координаты карты (x, y) -> мир на земле (x, 0, y). 1000 милли-единиц = 1 клетка
public static Vector3 ToWorld(MapNode node)
{
return new Vector3(node.x / 1000f, node.y / 1000f, 0f);
return new Vector3(node.x / 1000f, 0f, node.y / 1000f);
}
// ---------------- ВЫБОР ЦЕЛИ ----------------
@@ -419,7 +417,7 @@ public class WorldMapController : MonoBehaviour
: node.type == MapNodeType.ResourcePoint ? PointSize : CitySize;
float halfSize = size / 2f + 0.2f;
Vector3 pos = ToWorld(node);
if (Mathf.Abs(world.x - pos.x) <= halfSize && Mathf.Abs(world.y - pos.y) <= halfSize) return node;
if (Mathf.Abs(world.x - pos.x) <= halfSize && Mathf.Abs(world.z - pos.z) <= halfSize) return node;
}
return null;
}