Danro/Assets/Scripts/Behaviour/LightDimmer.cs

267 lines
13 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<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 не задан вручную.
GameObject 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)
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++)
{
Light2D light2D = lights[i];
// Безопасность: если света нет, пропускаем
if (!light2D)
continue;
// Получаем заранее закэшированный Volume (может быть null)
Volume vol = _volumes != null && i < _volumes.Length ? _volumes[i] : null;
// Пытаемся извлечь компонент Bloom из профиля, если volume и профиль присутствуют
Bloom bloom = null;
if (applyBloom && vol && vol.profile)
{
// TryGet возвращает true если компонент есть, и мы получаем ссылку на него
vol.profile.TryGet(out bloom);
}
if (smoothing <= 0f)
{
// Мгновенное изменение — полезно для теста или резких переключателей
light2D.intensity = desiredForPosition;
if (applyBloom && bloom) bloom.intensity.value = desiredForBloom;
}
else
{
// Плавное изменение — используем MoveTowards для стабильного изменения без overshoot
light2D.intensity = Mathf.MoveTowards(light2D.intensity, desiredForPosition, smoothing * Time.deltaTime);
if (applyBloom && bloom)
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()
{
Bounds b = targetCollider.bounds;
_boundsMin = b.min.x;
_boundsMax = b.max.x;
if (!useYProgress) return;
_boundsMin = b.min.y;
_boundsMax = b.max.y;
}
/// <summary>
/// Рисует в редакторе границу collider'а и позицию игрока для удобства отладки.
/// </summary>
private void OnDrawGizmos()
{
if (targetCollider == null) return;
Bounds 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) return;
Gizmos.color = Color.cyan;
Vector3 p = _playerTransform.position;
Gizmos.DrawSphere(new Vector3(p.x, p.y, 0f), 0.05f);
}
}
}