using System.Collections.Generic; using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; using RR.Game2D.Core.Managers; namespace RR.Game2D.Behaviour.Lighting { [RequireComponent(typeof(Collider2D))] public class LightDimmer : MonoBehaviour { [Tooltip("Если не указан, будет использован Collider2D на этом же объекте.")] [SerializeField] private Collider2D targetCollider; [Tooltip("Тег игрока (используется для поиска Transform игрока)")] [SerializeField] private string playerTag; [Tooltip("Массив со всеми источниками света, которые нужно изменять")] [SerializeField] private Light2D[] lights; [Tooltip("Использовать прогресс по оси Y вместо оси X")] [SerializeField] private bool useYProgress = false; [Header("Настройки света")] [SerializeField][Min(0)] private float startIntensity; [SerializeField][Min(0)] private float endIntensity; [SerializeField][Min(0)] private float smoothing; [Header("Настройки свечения")] [SerializeField] private bool applyBloom = true; [SerializeField][Min(0)] private float startBloomIntensity = 0f; [SerializeField][Min(0)] private float targetBloomIntensity = 1f; private Transform _playerTransform; // Храним все коллайдеры игрока, которые в данный момент пересекают триггер private readonly HashSet _playerColliders = new HashSet(); private float _boundsMin; private float _boundsMax; private Volume[] _volumes; /// /// Валидация в редакторе — обновляем границы, если указан collider, и подставляем тег игрока по умолчанию. /// Вызвается в редакторе при изменении полей в инспекторе. /// private void OnValidate() { if (targetCollider != null) { UpdateBounds(); } if (string.IsNullOrEmpty(playerTag)) playerTag = "Player"; } /// /// Инициализация при включении объекта: подставляем collider если не указан, находим игрока и кэшируем Volume для каждого света. /// Важно: здесь мы не создаём новых Volume, а только берём существующие компоненты в родительских объектах света. /// private void OnEnable() { if (targetCollider == null) targetCollider = GetComponent(); if (targetCollider != null) { UpdateBounds(); } // Подготавливаем массив volumes той же длины, что и массив lights. // Это кэш для ускорения доступа к Bloom в Update, чтобы не делать GetComponent каждый кадр. _volumes = new Volume[lights.Length]; if (string.IsNullOrEmpty(playerTag)) playerTag = "Player"; // Пытаемся найти игрока на сцене для удобства — это позволит скрипту работать, даже если playerTransform не задан вручную. var playerObj = GameObject.FindGameObjectWithTag(playerTag); if (playerObj != null) _playerTransform = playerObj.transform; // Кэшируем Volume (если есть) для каждого источника света. Volume может находиться на родителе или выше. for (int i = 0; i < lights.Length; i++) { Light2D light2D = lights[i]; // Возможна ситуация, когда элемент массива пустой (null) — защищаемся от этого. if (light2D == null) { _volumes[i] = null; continue; } // Ищем ближайший родительский Volume (он может быть на том же GameObject или выше в иерархии). Volume volume = light2D.GetComponentInParent(); if (volume != null) { // Включаем volume, чтобы настроенные эффекты (Bloom и т.п.) применялись во время работы. volume.enabled = true; } _volumes[i] = volume; } } /// /// Start — дополнительная страховка: подставляем collider и обновляем границы на старте сцены. /// private void Start() { if (targetCollider == null) { targetCollider = GetComponent(); } UpdateBounds(); if (_playerTransform == null) _playerTransform = GameManager.Instance.PlayerObj.transform; } /// /// Основной цикл: если игрок находится внутри триггера, вычисляем прогресс по выбранной оси и плавно интерполируем интенсивность света /// и, при необходимости, значение bloom в соответствующем Volume. /// private void Update() { // Требуем наличие: playerTransform, массив lights и targetCollider if (!_playerTransform || lights == null || lights.Length == 0 || targetCollider == null) return; // Debug.Log($"{name}: Player Transform Detected"); // Проверяем, что игрок действительно внутри триггера, учитываем все коллайдеры (на случай CompositeCollider/мультиколлайдеров) bool playerInside = _playerColliders.Count > 0; if (!playerInside) return; // Debug.Log($"{name}: Player Inside Trigger"); Vector3 playerPos = _playerTransform.position; // Дополнительная проверка: AABB bounds коллайдера (быстрая фильтрация), чтобы избежать лишних вычислений // if (!targetCollider.bounds.Contains(playerPos)) // return; // Debug.Log($"{name}: Player Position: {playerPos}"); // Выбираем координату для прогресса: X или Y float playerCurrent = useYProgress ? playerPos.y : playerPos.x; // Debug.Log($"{name}: Player Current Coordinate: {playerCurrent}"); float t = 0f; // Если boundsMin==boundsMax — делить нельзя, поэтому проверяем приблизительное равенство if (!Mathf.Approximately(_boundsMax, _boundsMin)) { // Нормализуем позицию игрока по отрезку [boundsMin, boundsMax] t = Mathf.InverseLerp(_boundsMin, _boundsMax, playerCurrent); t = Mathf.Clamp01(t); } // Интерполируем целевые значения интенсивности и bloom в зависимости от нормализованной позиции t float desiredForPosition = Mathf.Lerp(startIntensity, endIntensity, t); float desiredForBloom = Mathf.Lerp(startBloomIntensity, targetBloomIntensity, t); // Проходим по всем светам и плавно (или мгновенно, если smoothing<=0) приводим интенсивность к целевой for (int i = 0; i < lights.Length; i++) { var light2D = lights[i]; // Безопасность: если света нет, пропускаем if (!light2D) continue; // Получаем заранее закэшированный Volume (может быть null) var vol = _volumes != null && i < _volumes.Length ? _volumes[i] : null; // Пытаемся извлечь компонент Bloom из профиля, если volume и профиль присутствуют Bloom bloom = null; if (applyBloom && vol != null && vol.profile != null) { // TryGet возвращает true если компонент есть, и мы получаем ссылку на него vol.profile.TryGet(out bloom); } if (smoothing <= 0f) { // Мгновенное изменение — полезно для теста или резких переключателей light2D.intensity = desiredForPosition; if (applyBloom && bloom != null) bloom.intensity.value = desiredForBloom; } else { // Плавное изменение — используем MoveTowards для стабильного изменения без overshoot light2D.intensity = Mathf.MoveTowards(light2D.intensity, desiredForPosition, smoothing * Time.deltaTime); if (applyBloom && bloom != null) bloom.intensity.value = Mathf.MoveTowards(bloom.intensity.value, desiredForBloom, smoothing * Time.deltaTime); } } } /// /// Триггер: запоминаем коллайдер игрока, когда любой из его коллайдеров входит в область. /// Используем HashSet, чтобы корректно работать с объектом, у которого несколько Collider2D (включая CompositeCollider2D). /// /// Коллайдер, вошедший в триггер private void OnTriggerEnter2D(Collider2D other) { if (!other.CompareTag(playerTag)) return; // Запоминаем коллайдер игрока _playerColliders.Add(other); // Сохраняем Transform игрока (на случай, если раньше не найден) if (_playerTransform == null) _playerTransform = other.transform; } /// /// Триггер выхода: убираем соответствующий коллайдер из набора; если набор пуст — игрок полностью вышел. /// /// Коллайдер, вышедший из триггера private void OnTriggerExit2D(Collider2D other) { if (!other.CompareTag(playerTag)) return; // Убираем коллайдер из множества; если множество пустое — игрок полностью вышел _playerColliders.Remove(other); } /// /// Обновляет внутренние границы отслеживаемого Collider2D. /// При useYProgress==false возвращает min/max по X, иначе по Y. /// private void UpdateBounds() { var b = targetCollider.bounds; _boundsMin = b.min.x; _boundsMax = b.max.x; if (useYProgress) { _boundsMin = b.min.y; _boundsMax = b.max.y; } } /// /// Рисует в редакторе границу collider'а и позицию игрока для удобства отладки. /// void OnDrawGizmosSelected() { if (targetCollider == null) return; var b = targetCollider.bounds; Gizmos.color = Color.yellow; Gizmos.DrawLine(new Vector3(b.min.x, b.min.y, 0f), new Vector3(b.max.x, b.min.y, 0f)); Gizmos.DrawLine(new Vector3(b.max.x, b.min.y, 0f), new Vector3(b.max.x, b.max.y, 0f)); Gizmos.DrawLine(new Vector3(b.max.x, b.max.y, 0f), new Vector3(b.min.x, b.max.y, 0f)); Gizmos.DrawLine(new Vector3(b.min.x, b.max.y, 0f), new Vector3(b.min.x, b.min.y, 0f)); if (_playerTransform != null) { Gizmos.color = Color.cyan; Vector3 p = _playerTransform.position; Gizmos.DrawSphere(new Vector3(p.x, p.y, 0f), 0.05f); } } } }