Feature/hero 3d #21

Merged
Alexander merged 3 commits from feature/hero-3d into develop 2026-11-06 19:00:32 +03:00
3 changed files with 81 additions and 13 deletions
Showing only changes of commit a72a8941ff - Show all commits
+64 -10
View File
@@ -408,17 +408,20 @@ public class City3DController : MonoBehaviour, City3DSave.ILive
go.AddComponent<MeshRenderer>().sharedMaterial = Map3D.TexturedMat(groundTexture);
go.AddComponent<MeshCollider>().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<Collider>());
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<Renderer>().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<int>(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<MeshFilter>().sharedMesh = mesh;
go.AddComponent<MeshRenderer>().sharedMaterial = Map3D.TexturedMat(tex); // без коллайдера: тап и стройка -- только на участке
}
void BuildObstacles()
{
int i = 0;
+16 -3
View File
@@ -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)
{
+1
View File
@@ -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. Числа камеры — ТЕСТ. |