using System.Collections.Generic; using UnityEngine; namespace _Project.Scripts.LevelGenerating { public class ShellResult { public Transform Root; public List Views = new List(); public List AllDoors = new List(); } public class ShellSpawner : MonoBehaviour { [SerializeField] private GameObject planePrefab; [SerializeField] private float planeBaseSize = 10f; [SerializeField] private float wallHeight = 3f; [SerializeField] private float colliderThickness = 0.2f; [Header("Двери и проёмы")] [Tooltip("Префаб двойной двери (с компонентом DoorController)")] [SerializeField] private GameObject doorPrefab; [Tooltip("Высота дверного проёма в метрах. От doorHeight до wallHeight будет построена перемычка с текстурой комнаты.")] [SerializeField] private float doorHeight = 2.2f; [Tooltip("Толщина перемычки над дверью в метрах (0 = плоская, >0 = объёмная с нижней гранью над головой)")] [Min(0f)] [SerializeField] private float lintelThickness = 0.3f; [Tooltip("Дополнительный поворот префаба двери (по умолчанию 180 по Y)")] [SerializeField] private Vector3 doorRotationOffset = new Vector3(0f, 180f, 0f); [Tooltip("Локальное смещение двери относительно проёма (X - вдоль стены, Y - высота, Z - вперёд/назад)")] [SerializeField] private Vector3 doorPositionOffset = Vector3.zero; public ShellResult Spawn(FloorData floor) { if (planePrefab == null) { Debug.LogError("ShellSpawner: не назначен planePrefab"); return null; } var result = new ShellResult { Root = new GameObject($"Floor_{floor.Index}_Shell").transform }; int groundLayer = LayerMask.NameToLayer("Ground"); int ceilingLayer = LayerMask.NameToLayer("Ceiling"); // 1. Объект на каждую комнату + пол и потолок for (int i = 0; i < floor.Rooms.Count; i++) { var room = floor.Rooms[i]; var roomGo = new GameObject($"Room_{i}_{room.rt}"); roomGo.transform.SetParent(result.Root, false); var view = roomGo.AddComponent(); view.Data = room; view.Index = i; view.Height = wallHeight; result.Views.Add(view); var scale = new Vector3(view.SizeMeters.x / planeBaseSize, 1f, view.SizeMeters.z / planeBaseSize); Vector3 c = view.CenterMeters; // пол: нормаль вверх var floorGo = Make(roomGo.transform, "Floor", new Vector3(c.x, 0f, c.z), Quaternion.identity, scale, true); if (groundLayer != -1) floorGo.layer = groundLayer; view.FloorRenderers.AddRange(floorGo.GetComponentsInChildren()); // потолок: перевёрнут, нормаль вниз var ceilingGo = Make(roomGo.transform, "Ceiling", new Vector3(c.x, wallHeight, c.z), Quaternion.Euler(180f, 0f, 0f), scale, false); if (ceilingLayer != -1) ceilingGo.layer = ceilingLayer; view.CeilingRenderers.AddRange(ceilingGo.GetComponentsInChildren()); } // 2. Сплошные стены раскладываются по комнатам-владельцам foreach (var run in floor.Walls) { if (run.RoomIndex < 0 || run.RoomIndex >= result.Views.Count) { Debug.LogWarning($"Стена ссылается на несуществующую комнату {run.RoomIndex}"); continue; } SpawnWallPiece(result.Views[run.RoomIndex], run.Start, run.Length, run.Facing, 0f, wallHeight, 0f, "Wall"); } // 3. Дверные проёмы: объёмная перемычка над дверью + сама двойная дверь foreach (var dw in floor.Doorways) { if (dw.RoomIndex < 0 || dw.RoomIndex >= result.Views.Count) continue; SpawnDoorway(result.Views[dw.RoomIndex], dw, result); } return result; } private void SpawnDoorway(RoomView view, Doorway dw, ShellResult result) { // 1. Объёмная перемычка над дверью (от doorHeight до wallHeight) float topWallHeight = wallHeight - doorHeight; if (topWallHeight > 0.01f) { SpawnLintel(view, dw, doorHeight, topWallHeight); } // 2. Спавн префаба двойной двери на каждом проёме комнаты if (!dw.SpawnDoor || doorPrefab == null) return; float len = dw.Length * LevelGrid.Module; float sx = dw.Start.x * LevelGrid.Module; float sz = dw.Start.y * LevelGrid.Module; Vector3 basePos; Quaternion baseRot; switch (dw.Facing) { case WallFacing.ZP: // южная стена, нормаль в +Z basePos = new Vector3(sx + len / 2f, 0f, sz); baseRot = Quaternion.Euler(0f, 0f, 0f); break; case WallFacing.ZN: // северная стена, нормаль в -Z basePos = new Vector3(sx + len / 2f, 0f, sz); baseRot = Quaternion.Euler(0f, 180f, 0f); break; case WallFacing.XP: // западная стена, нормаль в +X basePos = new Vector3(sx, 0f, sz + len / 2f); baseRot = Quaternion.Euler(0f, 90f, 0f); break; default: // XN: восточная стена, нормаль в -X basePos = new Vector3(sx, 0f, sz + len / 2f); baseRot = Quaternion.Euler(0f, -90f, 0f); break; } Quaternion finalRot = baseRot * Quaternion.Euler(doorRotationOffset); Vector3 finalPos = basePos + finalRot * doorPositionOffset; var doorGo = Instantiate(doorPrefab, finalPos, finalRot, view.transform); doorGo.name = $"DoubleDoor_{dw.Facing}"; var controller = doorGo.GetComponent(); if (controller == null) controller = doorGo.AddComponent(); view.Doors.Add(controller); result.AllDoors.Add(controller); } // Строит перемычку над дверью с учётом толщины lintelThickness: // лицевую вертикальную стенку (сдвинутую на halfThick внутрь комнаты) и нижнюю горизонтальную грань private void SpawnLintel(RoomView view, Doorway dw, float bottomY, float pieceHeight) { float halfThick = lintelThickness * 0.5f; // Лицевая вертикальная сторона перемычки SpawnWallPiece(view, dw.Start, dw.Length, dw.Facing, bottomY, pieceHeight, halfThick, "Wall_AboveDoor"); // Если толщина > 0, закрываем нижний торец перемычки (на высоте doorHeight, нормалью вниз) if (halfThick > 0.001f) { float len = dw.Length * LevelGrid.Module; float sx = dw.Start.x * LevelGrid.Module; float sz = dw.Start.y * LevelGrid.Module; Vector3 capPos; Vector3 capScale; switch (dw.Facing) { case WallFacing.ZP: // от линии стены в +Z на halfThick capPos = new Vector3(sx + len / 2f, bottomY, sz + halfThick / 2f); capScale = new Vector3(len / planeBaseSize, 1f, halfThick / planeBaseSize); break; case WallFacing.ZN: // от линии стены в -Z на halfThick capPos = new Vector3(sx + len / 2f, bottomY, sz - halfThick / 2f); capScale = new Vector3(len / planeBaseSize, 1f, halfThick / planeBaseSize); break; case WallFacing.XP: // от линии стены в +X на halfThick capPos = new Vector3(sx + halfThick / 2f, bottomY, sz + len / 2f); capScale = new Vector3(halfThick / planeBaseSize, 1f, len / planeBaseSize); break; default: // XN: от линии стены в -X на halfThick capPos = new Vector3(sx - halfThick / 2f, bottomY, sz + len / 2f); capScale = new Vector3(halfThick / planeBaseSize, 1f, len / planeBaseSize); break; } // Поворот 180 по X — нормаль смотрит вниз (в проём над дверью) var capGo = Make(view.transform, "Wall_AboveDoor_Bottom", capPos, Quaternion.Euler(180f, 0f, 0f), capScale, false); int wallLayer = LayerMask.NameToLayer("Wall"); if (wallLayer != -1) capGo.layer = wallLayer; view.WallRenderers.AddRange(capGo.GetComponentsInChildren()); } } private void SpawnWallPiece(RoomView view, Vector2Int startCell, int lengthModules, WallFacing facing, float bottomY, float pieceHeight, float forwardOffset, string pieceName) { float len = lengthModules * LevelGrid.Module; float y = bottomY + pieceHeight / 2f; float sx = startCell.x * LevelGrid.Module; float sz = startCell.y * LevelGrid.Module; Vector3 pos; Quaternion rot; Vector3 scale; switch (facing) { case WallFacing.ZP: pos = new Vector3(sx + len / 2f, y, sz + forwardOffset); rot = Quaternion.Euler(90f, 0f, 0f); scale = new Vector3(len / planeBaseSize, 1f, pieceHeight / planeBaseSize); break; case WallFacing.ZN: pos = new Vector3(sx + len / 2f, y, sz - forwardOffset); rot = Quaternion.Euler(-90f, 0f, 0f); scale = new Vector3(len / planeBaseSize, 1f, pieceHeight / planeBaseSize); break; case WallFacing.XP: pos = new Vector3(sx + forwardOffset, y, sz + len / 2f); rot = Quaternion.Euler(0f, 0f, -90f); scale = new Vector3(pieceHeight / planeBaseSize, 1f, len / planeBaseSize); break; default: // XN pos = new Vector3(sx - forwardOffset, y, sz + len / 2f); rot = Quaternion.Euler(0f, 0f, 90f); scale = new Vector3(pieceHeight / planeBaseSize, 1f, len / planeBaseSize); break; } var go = Make(view.transform, pieceName, pos, rot, scale, true); int wallLayer = LayerMask.NameToLayer("Wall"); if (wallLayer != -1) go.layer = wallLayer; view.WallRenderers.AddRange(go.GetComponentsInChildren()); } private GameObject Make(Transform parent, string name, Vector3 pos, Quaternion rot, Vector3 scale, bool withCollider) { var go = Instantiate(planePrefab, pos, rot, parent); go.name = name; go.transform.localScale = scale; foreach (var c in go.GetComponents()) Destroy(c); if (withCollider) { var box = go.AddComponent(); box.size = new Vector3(planeBaseSize, colliderThickness, planeBaseSize); box.center = new Vector3(0f, -colliderThickness / 2f, 0f); } return go; } } }