From a72a8941ffa71a48802325da98246bbab81e2c00 Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 6 Nov 2026 16:57:29 +0100 Subject: [PATCH] =?UTF-8?q?3D:=20=D0=BC=D0=B5=D1=81=D1=82=D0=BD=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D1=8C=20=D0=B7=D0=B0=20=D0=BA=D1=80=D0=B0=D0=B5?= =?UTF-8?q?=D0=BC=20=D1=83=D1=87=D0=B0=D1=81=D1=82=D0=BA=D0=B0=20--=20?= =?UTF-8?q?=D1=85=D0=BE=D0=BB=D0=BC=D1=8B,=20=D1=80=D0=B5=D0=BA=D0=B0=20?= =?UTF-8?q?=D0=B8=20=D0=B3=D0=BE=D1=80=D1=8B=20=D0=BF=D1=80=D0=BE=D0=B4?= =?UTF-8?q?=D0=BE=D0=BB=D0=B6=D0=B0=D1=8E=D1=82=D1=81=D1=8F=20=D0=B1=D0=B5?= =?UTF-8?q?=D0=B7=20=D1=88=D0=B2=D0=B0=20=D0=B8=20=D0=BF=D0=BB=D0=B0=D0=B2?= =?UTF-8?q?=D0=BD=D0=BE=20=D1=83=D1=85=D0=BE=D0=B4=D1=8F=D1=82=20=D0=B2=20?= =?UTF-8?q?=D1=80=D0=B0=D0=B2=D0=BD=D0=B8=D0=BD=D1=83;=20=D0=BA=D1=80?= =?UTF-8?q?=D0=B0=D0=B9=20=D0=B1=D0=BE=D0=BB=D1=8C=D1=88=D0=B5=20=D0=BD?= =?UTF-8?q?=D0=B5=20=D0=BE=D0=B1=D1=80=D0=B5=D0=B7=D0=B0=D0=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/City3D/City3DController.cs | 74 ++++++++++++++++++++--- Assets/Scripts/City3D/City3DLayout.cs | 19 +++++- Docs/Modules/Settlement.md | 1 + 3 files changed, 81 insertions(+), 13 deletions(-) diff --git a/Assets/Scripts/City3D/City3DController.cs b/Assets/Scripts/City3D/City3DController.cs index 87e5da9..8ed3d29 100644 --- a/Assets/Scripts/City3D/City3DController.cs +++ b/Assets/Scripts/City3D/City3DController.cs @@ -408,17 +408,20 @@ public class City3DController : MonoBehaviour, City3DSave.ILive go.AddComponent().sharedMaterial = Map3D.TexturedMat(groundTexture); go.AddComponent().sharedMesh = mesh; - // Лес и луга за краем участка: камера не видит пустого неба - Map3D.Block(transform, "OuterLand", PrimitiveType.Cube, new Color(0.24f, 0.4f, 0.22f), new Vector3(n / 2f, -0.7f, n / 2f), - new Vector2(n + 200f, n + 200f), 0.1f); + // Местность за краем участка -- продолжение холмов, реки и гор (City3DLayout.SkirtHeight), дальше -- равнина + BuildSkirt(); + Map3D.Block(transform, "OuterLand", PrimitiveType.Cube, new Color(0.3f, 0.5f, 0.26f), + new Vector3(n / 2f, City3DLayout.SkirtFloor - 0.05f, n / 2f), new Vector2(n + 2 * City3DLayout.SkirtCells + 300f, n + 2 * City3DLayout.SkirtCells + 300f), 0.1f); var forest = new System.Random(7); - for (int i = 0; i < 140; i++) + for (int i = 0; i < 260; i++) { - // кольцо деревьев вокруг участка - float angle = (float)forest.NextDouble() * Mathf.PI * 2f, r = n * 0.72f + (float)forest.NextDouble() * 14f; - var p = new Vector3(n / 2f + Mathf.Cos(angle) * r, -0.6f, n / 2f + Mathf.Sin(angle) * r); - if (p.x > 0 && p.x < n && p.z > 0 && p.z < n) continue; // внутри участка -- свои деревья - Map3D.Tree(transform, "ForestTree", p, 1.4f + (float)forest.NextDouble() * 0.8f, forest.Next(6)); + // лес вокруг участка: гуще у края, реже дальше; не в реке и не на горах + float angle = (float)forest.NextDouble() * Mathf.PI * 2f; + float r = n * 0.6f + Mathf.Pow((float)forest.NextDouble(), 1.6f) * (City3DLayout.SkirtCells + 4f); + float x = n / 2f + Mathf.Cos(angle) * r, z = n / 2f + Mathf.Sin(angle) * r; + if (x > -0.5f && x < n + 0.5f && z > -0.5f && z < n + 0.5f) continue; // внутри участка -- свои деревья + if (City3DLayout.IsRiver(x, z) || City3DLayout.IsMountain(x, z)) continue; + Map3D.Tree(transform, "ForestTree", new Vector3(x, City3DLayout.SkirtHeight(x, z), z), 1.4f + (float)forest.NextDouble() * 0.8f, forest.Next(6)); } // Вода -- плоскость над руслом @@ -427,10 +430,61 @@ public class City3DController : MonoBehaviour, City3DSave.ILive Destroy(water.GetComponent()); water.transform.SetParent(transform, false); water.transform.position = new Vector3(37f, -0.25f, n / 2f); - water.transform.localScale = new Vector3(9f, 0.05f, n); + water.transform.localScale = new Vector3(9f, 0.05f, n + 2 * (City3DLayout.SkirtCells - 22)); // река течёт и за краем участка water.GetComponent().sharedMaterial = Mat(new Color(0.3f, 0.58f, 0.85f)); } + // Местность вокруг участка: сетка 1 клетка (на стыке -- те же вершины, что у земли участка, без щелей), + // без самого участка. Цвет -- тот же, что у земли (трава пятнами, берег, вода, горы со снегом), 1 пиксель на клетку + void BuildSkirt() + { + int n = City3DLayout.Size, s = City3DLayout.SkirtCells, w = n + 2 * s; + var verts = new Vector3[(w + 1) * (w + 1)]; + var uvs = new Vector2[verts.Length]; + for (int z = 0; z <= w; z++) + for (int x = 0; x <= w; x++) + { + float wx = x - s, wz = z - s; + int i = z * (w + 1) + x; + verts[i] = new Vector3(wx, City3DLayout.SkirtHeight(wx, wz), wz); + uvs[i] = new Vector2(x / (float)w, z / (float)w); + } + var tris = new List(w * w * 6); + for (int z = 0; z < w; z++) + for (int x = 0; x < w; x++) + { + if (x >= s && x < s + n && z >= s && z < s + n) continue; // участок -- своя земля + int i = z * (w + 1) + x; + tris.Add(i); tris.Add(i + w + 1); tris.Add(i + 1); + tris.Add(i + 1); tris.Add(i + w + 1); tris.Add(i + w + 2); + } + var mesh = new Mesh { name = "City3D_Skirt", indexFormat = UnityEngine.Rendering.IndexFormat.UInt32 }; + mesh.vertices = verts; + mesh.uv = uvs; + mesh.SetTriangles(tris, 0); + mesh.RecalculateNormals(); + + var tex = new Texture2D(w, w, TextureFormat.RGBA32, false) { filterMode = FilterMode.Bilinear, wrapMode = TextureWrapMode.Clamp }; + var px = new Color32[w * w]; + for (int z = 0; z < w; z++) + for (int x = 0; x < w; x++) + { + float wx = x - s + 0.5f, wz = z - s + 0.5f, h = City3DLayout.SkirtHeight(wx, wz); + Color c = City3DLayout.IsRiver(wx, wz) ? new Color(0.32f, 0.5f, 0.72f) + : City3DLayout.IsMountain(wx, wz) && h > 1.5f ? Color.Lerp(new Color(0.48f, 0.45f, 0.42f), new Color(0.85f, 0.86f, 0.88f), Mathf.Clamp01((h - 3.2f) / 1.5f)) + : Mathf.Abs(wx - City3DLayout.RiverX(wz)) < 2.2f ? new Color(0.78f, 0.72f, 0.52f) + : Color.Lerp(new Color(0.33f, 0.56f, 0.27f), new Color(0.45f, 0.66f, 0.32f), Mathf.PerlinNoise(wx * 0.25f, wz * 0.25f)); + px[z * w + x] = c; + } + tex.SetPixels32(px); + tex.Apply(false); + + var go = new GameObject("Skirt"); + go.transform.SetParent(transform, false); + go.AddComponent().sharedMesh = mesh; + go.AddComponent().sharedMaterial = Map3D.TexturedMat(tex); // без коллайдера: тап и стройка -- только на участке + } + void BuildObstacles() { int i = 0; diff --git a/Assets/Scripts/City3D/City3DLayout.cs b/Assets/Scripts/City3D/City3DLayout.cs index f5979ac..1ffb485 100644 --- a/Assets/Scripts/City3D/City3DLayout.cs +++ b/Assets/Scripts/City3D/City3DLayout.cs @@ -39,9 +39,22 @@ public sealed class City3DLayout return hills; } - static float RiverX(float z) => 37f + 3f * Mathf.Sin(z * 0.17f); - static bool IsRiver(float x, float z) => Mathf.Abs(x - RiverX(z)) < 1.3f; - static bool IsMountain(float x, float z) => x < 9f && z > 34f || x < 4f && z > 24f; + public static float RiverX(float z) => 37f + 3f * Mathf.Sin(z * 0.17f); + public static bool IsRiver(float x, float z) => Mathf.Abs(x - RiverX(z)) < 1.3f; + public static bool IsMountain(float x, float z) => x < 9f && z > 34f || x < 4f && z > 24f; + + // Местность за краем участка (только вид, стройки нет): те же холмы, река и горы продолжаются без шва, + // к дальнему краю (SkirtCells) плавно опускаются до общей равнины SkirtFloor (06.11: при отдалении край участка + // выглядел обрезанным -- холмы и река обрывались уступом) + public const int SkirtCells = 60; + public const float SkirtFloor = -0.7f; + + public static float SkirtHeight(float x, float z) + { + float outside = Mathf.Max(0f, Mathf.Max(Mathf.Max(-x, x - Size), Mathf.Max(-z, z - Size))); + float fade = 1f - Mathf.SmoothStep(0f, 1f, Mathf.InverseLerp(SkirtCells - 20f, SkirtCells, outside)); + return Mathf.Lerp(SkirtFloor, HeightAt(x, z), fade); + } public static City3DLayout Create(PlacementRules rules = null) { diff --git a/Docs/Modules/Settlement.md b/Docs/Modules/Settlement.md index 90567c3..5d42225 100644 --- a/Docs/Modules/Settlement.md +++ b/Docs/Modules/Settlement.md @@ -119,6 +119,7 @@ | Дата | Кто | Что | | --- | --- | --- | +| 06.11.2026 | Claude (Саша) | 3D: местность за краем участка — 60 клеток вокруг (`City3DController.BuildSkirt`, `City3DLayout.SkirtHeight`): те же холмы, река и горы продолжаются без шва (на стыке те же вершины) и к дальнему краю плавно опускаются до равнины; река течёт дальше, вокруг лес. Только вид: без коллайдера, стройки и тапов нет. Раньше при отдалении участок обрывался уступом. | | 06.11.2026 | Claude (Саша) | Герой у Терема в 3D (перенос из 2D): `City3D/City3DHero.cs` — фигурка из простых форм появляется у ворот достроенного Терема и гуляет по свободным клеткам 3D-карты (земля, открытая зона, без зданий и деревьев) не дальше 6 клеток от Терема, шагами на соседнюю клетку — сквозь здания не проходит; тап — фраза и окно героя; в режиме стройки и переноса прячется. Создаёт и убирает `City3DController.UpdateHero`. На экономику и сохранение не влияет. | | 06.11.2026 | Claude (Саша) | Открытие зоны Ратушей: камера показывает только край новой зоны — старую зону и 2 клетки новой за её границей (`ZoneEdgeCells`), а не всю зону; ближе, чем была, не подъезжает (`City3DCamera.FocusOn(..., onlyZoomOut)`). Раньше при ускорении улучшения Ратуши камера улетала слишком далеко. | | 06.11.2026 | Claude (Валдемар) | Этап 1 перехода на 3D (2D-город больше не развиваем). Сверка: все 12 зданий 2D (8 `Data_*.asset` + Склад, Амбар, Госпиталь, Терем) есть в 3D с теми же ценами и уровнем Ратуши; ни одна кнопка не ведёт в MVP1. Камера (`City3DCamera`): центр кадра не уходит за открытые зоны; при самом дальнем отдалении открытая зона видна целиком между HUD сверху и снизу — дальше всего = max(подобранное в DEV, сколько нужно для зоны), считается по проекции камеры (`FitDistance`); «+ / −» и колесо приближают плавно; перетаскивание считается на высоте земли под пальцем (на холмах карта идёт точно за пальцем); подъезд к призраку — `MoveTo`, без смены приближения. Ввод: после щипка отпущенный палец больше не считается тапом (не открывается окно здания). `SceneListFixer` больше не добавляет и не включает MVP1 в Build Settings. Числа камеры — ТЕСТ. |