From 99b7d38b6e783ea883357b4439d4f7a95368645e Mon Sep 17 00:00:00 2001 From: Alexander Date: Tue, 27 Oct 2026 17:02:04 +0100 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=BA=D0=BD=D0=B0=20=D0=B3=D0=BE=D1=80?= =?UTF-8?q?=D0=BE=D0=B4=D0=B0:=20=D1=81=D0=BE=D0=B4=D0=B5=D1=80=D0=B6?= =?UTF-8?q?=D0=B8=D0=BC=D0=BE=D0=B5=20=D0=B2=20=D0=B1=D0=B5=D0=B7=D0=BE?= =?UTF-8?q?=D0=BF=D0=B0=D1=81=D0=BD=D0=BE=D0=B9=20=D0=B7=D0=BE=D0=BD=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit На iPhone левый край окон («Войска» и др.) уходил под вырез камеры. BasePanel переносит Content в контейнер SafeArea; фон окна остаётся как был. SafeArea.fitInsideParent: зона пересчитывается в координаты родителя, если он не на весь экран. Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/Meta/UI/BasePanel.cs | 12 ++++++++ Assets/Scripts/UI/SafeArea.cs | 48 ++++++++++++++++++++++++++--- Docs/Modules/README.md | 1 + 3 files changed, 56 insertions(+), 5 deletions(-) diff --git a/Assets/Scripts/Meta/UI/BasePanel.cs b/Assets/Scripts/Meta/UI/BasePanel.cs index 5be6552..db37c33 100644 --- a/Assets/Scripts/Meta/UI/BasePanel.cs +++ b/Assets/Scripts/Meta/UI/BasePanel.cs @@ -64,11 +64,23 @@ public abstract class BasePanel : MonoBehaviour t = UIKit.Stretch(UIKit.Make("Content", transform), 24, 130, 24, 24); } content = (RectTransform)t; + WrapInSafeArea(content); } return content; } } + // Фон окна -- на весь экран, а содержимое -- в безопасной зоне: на iPhone левый край уходил под вырез камеры. + // Content переезжает в растянутый контейнер с SafeArea и сохраняет свои отступы. + void WrapInSafeArea(RectTransform rt) + { + if (rt.parent != transform) return; // уже обёрнут + var safe = UIKit.Stretch(UIKit.Make("SafeArea", transform)); + safe.SetSiblingIndex(rt.GetSiblingIndex()); + safe.gameObject.AddComponent().fitInsideParent = true; // окно -- не на весь экран + rt.SetParent(safe, false); + } + void Rebuild() { float pos = (scroll != null && !resetScroll) ? scroll.verticalNormalizedPosition : 1f; diff --git a/Assets/Scripts/UI/SafeArea.cs b/Assets/Scripts/UI/SafeArea.cs index b3c712f..ec6e933 100644 --- a/Assets/Scripts/UI/SafeArea.cs +++ b/Assets/Scripts/UI/SafeArea.cs @@ -2,13 +2,19 @@ using UnityEngine; // Подгоняет RectTransform под безопасную зону экрана (Screen.safeArea): вырез камеры, скруглённые углы, // полоска "домой" на iPhone. Следит за поворотом и сменой разрешения (и в Device Simulator). -// Вешать на растянутый на весь Canvas объект; всё, что внутри, не залезет под вырез. +// Вешать на растянутый объект; всё, что внутри, не залезет под вырез. +// - fitInsideParent = false: объект растянут на весь Canvas (HUD); +// - fitInsideParent = true: родитель не на весь экран (окно между верхней и нижней панелью) -- +// зона пересчитывается в координаты родителя, отступ появляется только там, где родитель заходит под вырез. [RequireComponent(typeof(RectTransform))] public class SafeArea : MonoBehaviour { + public bool fitInsideParent; + private RectTransform rect; private Rect appliedArea; private Vector2Int appliedScreen; + private Rect appliedParent; void Awake() { @@ -18,7 +24,8 @@ public class SafeArea : MonoBehaviour void Update() { - if (Screen.safeArea != appliedArea || Screen.width != appliedScreen.x || Screen.height != appliedScreen.y) + if (Screen.safeArea != appliedArea || Screen.width != appliedScreen.x || Screen.height != appliedScreen.y + || (fitInsideParent && ParentScreenRect() != appliedParent)) Apply(); } @@ -29,10 +36,41 @@ public class SafeArea : MonoBehaviour appliedScreen = new Vector2Int(Screen.width, Screen.height); if (Screen.width <= 0 || Screen.height <= 0) return; - // Якоря в долях экрана: Canvas в режиме Screen Space Overlay покрывает экран целиком - rect.anchorMin = new Vector2(area.xMin / Screen.width, area.yMin / Screen.height); - rect.anchorMax = new Vector2(area.xMax / Screen.width, area.yMax / Screen.height); + if (!fitInsideParent) + { + // Якоря в долях экрана: Canvas в режиме Screen Space Overlay покрывает экран целиком + rect.anchorMin = new Vector2(area.xMin / Screen.width, area.yMin / Screen.height); + rect.anchorMax = new Vector2(area.xMax / Screen.width, area.yMax / Screen.height); + } + else + { + Rect parent = ParentScreenRect(); + appliedParent = parent; + if (parent.width <= 0 || parent.height <= 0) return; + + // Пересечение родителя с безопасной зоной -- в долях родителя + float xMin = Mathf.Max(parent.xMin, area.xMin), xMax = Mathf.Min(parent.xMax, area.xMax); + float yMin = Mathf.Max(parent.yMin, area.yMin), yMax = Mathf.Min(parent.yMax, area.yMax); + if (xMax <= xMin || yMax <= yMin) return; + rect.anchorMin = new Vector2((xMin - parent.xMin) / parent.width, (yMin - parent.yMin) / parent.height); + rect.anchorMax = new Vector2((xMax - parent.xMin) / parent.width, (yMax - parent.yMin) / parent.height); + } rect.offsetMin = Vector2.zero; rect.offsetMax = Vector2.zero; } + + // Прямоугольник родителя в пикселях экрана + Rect ParentScreenRect() + { + var parent = transform.parent as RectTransform; + if (parent == null) return new Rect(0, 0, Screen.width, Screen.height); + + var corners = new Vector3[4]; + parent.GetWorldCorners(corners); + Canvas canvas = parent.GetComponentInParent(); + Camera cam = canvas != null && canvas.renderMode != RenderMode.ScreenSpaceOverlay ? canvas.worldCamera : null; + Vector2 min = RectTransformUtility.WorldToScreenPoint(cam, corners[0]); + Vector2 max = RectTransformUtility.WorldToScreenPoint(cam, corners[2]); + return Rect.MinMaxRect(min.x, min.y, max.x, max.y); + } } diff --git a/Docs/Modules/README.md b/Docs/Modules/README.md index 80a9485..328e87b 100644 --- a/Docs/Modules/README.md +++ b/Docs/Modules/README.md @@ -61,6 +61,7 @@ EditMode-тесты: `Assets/Scripts/Modules/Tests/ModuleTests.cs`. Запуск | Дата | Кто | Что | | --- | --- | --- | +| 27.10.2026 | Claude | Слой приложения: содержимое окон города (`BasePanel`) — в безопасной зоне, левый край больше не уходит под вырез камеры. `SafeArea.fitInsideParent` — для объектов не на весь экран. | | 27.10.2026 | Claude | Гарнизон города в модуле Армии. Обновлены Army, Core, World. | | 27.10.2026 | Claude | Лагеря разбойников (PvE) на карте мира. Обновлены World, Battle, Save, Core. | | 27.10.2026 | Claude | Боевая система с типами войск (контры, стены, осадные, потери по типам), состав гарнизонов на карте, ожидаемый исход в окне атаки. Обновлены Battle, World, Army, Core. |