268 lines
12 KiB
C#
268 lines
12 KiB
C#
using System.Collections.Generic;
|
||
using RR.Game2D.Core.Managers;
|
||
using UnityEngine;
|
||
using UnityEngine.Rendering;
|
||
using UnityEngine.Rendering.Universal;
|
||
|
||
[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<Collider2D> playerColliders = new HashSet<Collider2D>();
|
||
|
||
private float boundsMin;
|
||
private float boundsMax;
|
||
private Volume[] _volumes;
|
||
|
||
/// <summary>
|
||
/// Валидация в редакторе — обновляем границы, если указан collider, и подставляем тег игрока по умолчанию.
|
||
/// Вызвается в редакторе при изменении полей в инспекторе.
|
||
/// </summary>
|
||
private void OnValidate()
|
||
{
|
||
if (targetCollider != null)
|
||
{
|
||
UpdateBounds();
|
||
}
|
||
|
||
if (string.IsNullOrEmpty(playerTag)) playerTag = "Player";
|
||
}
|
||
|
||
/// <summary>
|
||
/// Инициализация при включении объекта: подставляем collider если не указан, находим игрока и кэшируем Volume для каждого света.
|
||
/// Важно: здесь мы не создаём новых Volume, а только берём существующие компоненты в родительских объектах света.
|
||
/// </summary>
|
||
private void OnEnable()
|
||
{
|
||
if (targetCollider == null)
|
||
targetCollider = GetComponent<Collider2D>();
|
||
|
||
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<Volume>();
|
||
if (volume != null)
|
||
{
|
||
// Включаем volume, чтобы настроенные эффекты (Bloom и т.п.) применялись во время работы.
|
||
volume.enabled = true;
|
||
}
|
||
|
||
_volumes[i] = volume;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Start — дополнительная страховка: подставляем collider и обновляем границы на старте сцены.
|
||
/// </summary>
|
||
private void Start()
|
||
{
|
||
if (targetCollider == null)
|
||
{
|
||
targetCollider = GetComponent<Collider2D>();
|
||
}
|
||
|
||
UpdateBounds();
|
||
|
||
if (playerTransform == null) playerTransform = GameManager.Instance.PlayerObj.transform;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Основной цикл: если игрок находится внутри триггера, вычисляем прогресс по выбранной оси и плавно интерполируем интенсивность света
|
||
/// и, при необходимости, значение bloom в соответствующем Volume.
|
||
/// </summary>
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Триггер: запоминаем коллайдер игрока, когда любой из его коллайдеров входит в область.
|
||
/// Используем HashSet, чтобы корректно работать с объектом, у которого несколько Collider2D (включая CompositeCollider2D).
|
||
/// </summary>
|
||
/// <param name="other">Коллайдер, вошедший в триггер</param>
|
||
private void OnTriggerEnter2D(Collider2D other)
|
||
{
|
||
if (!other.CompareTag(playerTag)) return;
|
||
|
||
// Запоминаем коллайдер игрока
|
||
playerColliders.Add(other);
|
||
|
||
// Сохраняем Transform игрока (на случай, если раньше не найден)
|
||
if (playerTransform == null)
|
||
playerTransform = other.transform;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Триггер выхода: убираем соответствующий коллайдер из набора; если набор пуст — игрок полностью вышел.
|
||
/// </summary>
|
||
/// <param name="other">Коллайдер, вышедший из триггера</param>
|
||
private void OnTriggerExit2D(Collider2D other)
|
||
{
|
||
if (!other.CompareTag(playerTag)) return;
|
||
|
||
// Убираем коллайдер из множества; если множество пустое — игрок полностью вышел
|
||
playerColliders.Remove(other);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Обновляет внутренние границы отслеживаемого Collider2D.
|
||
/// При useYProgress==false возвращает min/max по X, иначе по Y.
|
||
/// </summary>
|
||
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;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Рисует в редакторе границу collider'а и позицию игрока для удобства отладки.
|
||
/// </summary>
|
||
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);
|
||
}
|
||
}
|
||
}
|