using System.Collections; using System.Collections.Generic; using System; using UnityEngine; using UnityEngine.VFX; using RR.Common.Utility; namespace RR.Game2D.Behaviour { /// /// Пул объектов визуальных эффектов для переиспользования VFX и оптимизации производительности. /// public class VFXPool : MonoBehaviour { public static VFXPool Instance { get; private set; } [SerializeField] private int poolSize = 32; [SerializeField] private float defaultLifetime = 2f; // время жизни эффекта по умолчанию (сек) [SerializeField] private SortingLayerPicker defaultSortingLayer; private Queue _pool; private readonly Dictionary _spawnVersions = new(); private static readonly string[] Vector4OverrideNames = { "Color", "TintColor", "BaseColor", "_Color", "_BaseColor" }; private static readonly string[] TextureOverrideNames = { "MainTexture", "mainTexture", "_MainTex", "_BaseMap", "_BaseColorMap" }; private static readonly string[] GradientOverrideNames = { "ColorOverLife", "colorOverLife", "Color", "Gradient" }; private static readonly string[] FloatOverrideNames = { "ParticleCount", "particleCount", "Count", "count" }; private static readonly string[] AnimationCurveOverrideNames = { "SizeOverLife", "sizeOverLife", "Size", "size" }; private static readonly string[] ScaleOverrideNames = { "Scale", "scale", "BaseScale", "baseScale" }; private void Awake() { // ---- Singleton ---- if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; // ------------------- // Инициализируем очередь для хранения свободных VFX объектов _pool = new Queue(); // Предварительно создаем VFX объекты для пула for (int i = 0; i < poolSize; i++) { GameObject vfxGameObject = new GameObject($"VFX_{i}"); vfxGameObject.AddComponent(); // Устанавливаем Sorting Layer для рендерера VFX ApplySortingLayer(vfxGameObject); // Созданные объекты сразу не активны в пуле vfxGameObject.SetActive(false); vfxGameObject.transform.SetParent(transform); _pool.Enqueue(vfxGameObject); _spawnVersions[vfxGameObject] = 0; } } private void OnDestroy() { // Если уничтожается текущий активный экземпляр, сбрасываем ссылку if (Instance == this) { Instance = null; } } /// /// Применяет Sorting Layer к рендереру VFX объекта /// private void ApplySortingLayer(GameObject vfxGameObject) { Renderer vfxRenderer = vfxGameObject.GetComponent(); if (vfxRenderer != null) { vfxRenderer.sortingLayerID = defaultSortingLayer.id; } } /// /// Получить VFX объект из пула (без автоматического запуска) /// /// Ассет визуального эффекта для применения /// GameObject с компонентом VisualEffect или null, если не удалось получить private GameObject Get(VisualEffectAsset vfxAsset) { GameObject vfxInstance; // Проверяем, есть ли свободные объекты в пуле if (_pool.Count > 0) { vfxInstance = _pool.Dequeue(); VisualEffect currentVFX = vfxInstance.GetComponent(); // Если текущий ассет не совпадает с запрашиваемым, заменяем его и реинициализируем if (currentVFX.visualEffectAsset != vfxAsset) { currentVFX.Stop(); currentVFX.visualEffectAsset = vfxAsset; currentVFX.Reinit(); } } else { // Пул пуст, создаем новый VFX объект vfxInstance = new GameObject($"VFX_{_pool.Count}"); vfxInstance.SetActive(false); VisualEffect vfx = vfxInstance.AddComponent(); vfx.visualEffectAsset = vfxAsset; vfx.Reinit(); // Устанавливаем Sorting Layer для рендерера VFX ApplySortingLayer(vfxInstance); vfxInstance.transform.SetParent(transform); _spawnVersions[vfxInstance] = 0; } // Сбрасываем поворот if (vfxInstance) { vfxInstance.transform.rotation = Quaternion.identity; } return vfxInstance; } /// /// Воспроизвести VFX из пула и автоматически вернуть его позже. /// /// Ассет визуального эффекта /// Позиция для эффекта /// Время жизни эффекта в секундах (если null — будет использовано defaultLifetime) public GameObject Spawn(VisualEffectAsset vfxAsset, Vector3 position, float? lifetime = null) { return Spawn(vfxAsset, position, lifetime, null); } public GameObject Spawn( VisualEffectAsset vfxAsset, Vector3 position, float? lifetime, Action configureBeforePlay) { float life = lifetime ?? defaultLifetime; GameObject vfxInstance = Get(vfxAsset); if (vfxInstance == null) return null; int spawnVersion = BeginSpawn(vfxInstance); if (vfxInstance.activeSelf) vfxInstance.SetActive(false); vfxInstance.transform.position = position; vfxInstance.transform.rotation = Quaternion.identity; vfxInstance.transform.localScale = Vector3.one; VisualEffect visualEffect = vfxInstance.GetComponent(); ResetRuntimeOverrides(vfxInstance, visualEffect); configureBeforePlay?.Invoke(vfxInstance, visualEffect); // Активируем только после назначения ассета, позиции и рантайм-параметров, // иначе первый burst на свежем объекте пула может не отрисоваться. vfxInstance.SetActive(true); visualEffect.Reinit(); visualEffect.Play(); // Запускаем корутину, которая вернёт объект в пул после окончания времени жизни StartCoroutine(ReturnAfterCoroutine(vfxInstance, life, spawnVersion)); return vfxInstance; } public GameObject Spawn(VisualEffectAsset vfxAsset, Transform newParentTransform, float? lifetime = null) { GameObject vfxInstance = Spawn(vfxAsset, newParentTransform.position, lifetime); vfxInstance.transform.SetParent(newParentTransform); return vfxInstance; } private int BeginSpawn(GameObject vfxInstance) { _spawnVersions.TryGetValue(vfxInstance, out int version); version++; _spawnVersions[vfxInstance] = version; return version; } private bool IsCurrentSpawn(GameObject vfxInstance, int spawnVersion) { return vfxInstance != null && _spawnVersions.TryGetValue(vfxInstance, out int currentVersion) && currentVersion == spawnVersion; } private void InvalidateSpawn(GameObject vfxInstance) { if (vfxInstance == null) return; _spawnVersions.TryGetValue(vfxInstance, out int version); _spawnVersions[vfxInstance] = version + 1; } private IEnumerator ReturnAfterCoroutine(GameObject vfxInstance, float seconds, int spawnVersion) { // Возвращаем эффект даже на экранах победы/поражения, где игра может быть поставлена на паузу. yield return new WaitForSecondsRealtime(seconds); if (!IsCurrentSpawn(vfxInstance, spawnVersion)) yield break; // Перед возвратом явно останавливаем эффект VisualEffect visualEffect = vfxInstance.GetComponent(); if (visualEffect) visualEffect.Stop(); // Возвращаем в пул Return(vfxInstance, spawnVersion); } public void AnimateScaleOverLifetime(GameObject vfxInstance, float baseScale, AnimationCurve sizeOverLife, float lifetime) { if (vfxInstance == null || sizeOverLife == null || lifetime <= 0f) return; _spawnVersions.TryGetValue(vfxInstance, out int spawnVersion); StartCoroutine(AnimateScaleOverLifetimeCoroutine(vfxInstance, baseScale, sizeOverLife, lifetime, spawnVersion)); } private IEnumerator AnimateScaleOverLifetimeCoroutine( GameObject vfxInstance, float baseScale, AnimationCurve sizeOverLife, float lifetime, int spawnVersion) { float elapsed = 0f; baseScale = Mathf.Max(0.01f, baseScale); while (IsCurrentSpawn(vfxInstance, spawnVersion) && vfxInstance.activeInHierarchy && elapsed < lifetime) { float normalizedTime = Mathf.Clamp01(elapsed / lifetime); float curveScale = Mathf.Max(0f, sizeOverLife.Evaluate(normalizedTime)); vfxInstance.transform.localScale = Vector3.one * baseScale * curveScale; elapsed += Time.unscaledDeltaTime; yield return null; } } private static void ResetRuntimeOverrides(GameObject vfxInstance, VisualEffect visualEffect) { if (visualEffect != null) { foreach (string propertyName in Vector4OverrideNames) { if (visualEffect.HasVector4(propertyName)) visualEffect.ResetOverride(propertyName); } foreach (string propertyName in TextureOverrideNames) { if (visualEffect.HasTexture(propertyName)) visualEffect.ResetOverride(propertyName); } foreach (string propertyName in GradientOverrideNames) { if (visualEffect.HasGradient(propertyName)) visualEffect.ResetOverride(propertyName); } foreach (string propertyName in FloatOverrideNames) { if (visualEffect.HasFloat(propertyName)) visualEffect.ResetOverride(propertyName); } foreach (string propertyName in AnimationCurveOverrideNames) { if (visualEffect.HasAnimationCurve(propertyName)) visualEffect.ResetOverride(propertyName); } foreach (string propertyName in ScaleOverrideNames) { if (visualEffect.HasVector3(propertyName)) visualEffect.ResetOverride(propertyName); else if (visualEffect.HasFloat(propertyName)) visualEffect.ResetOverride(propertyName); } } Renderer renderer = vfxInstance.GetComponent(); if (renderer != null) renderer.SetPropertyBlock(null); } /// /// Вернуть VFX объект обратно в пул для повторного использования /// /// VFX объект для возврата в пул private void Return(GameObject vfxInstance, int spawnVersion) { if (vfxInstance == null) return; if (!IsCurrentSpawn(vfxInstance, spawnVersion)) return; // Явно устанавливаем родителем пул, чтобы избежать захламления сцены vfxInstance.transform.SetParent(this.transform); VisualEffect visualEffect = vfxInstance.GetComponent(); if (visualEffect) visualEffect.Stop(); ResetRuntimeOverrides(vfxInstance, visualEffect); vfxInstance.transform.localScale = Vector3.one; vfxInstance.transform.rotation = Quaternion.identity; InvalidateSpawn(vfxInstance); // Если пул не переполнен, деактивируем и кладём в очередь if (_pool.Count < poolSize) { vfxInstance.SetActive(false); _pool.Enqueue(vfxInstance); } else { // Пул переполнен, уничтожаем объект Debug.Log("VFX Pool is full, destroying returned VFX instance."); Destroy(vfxInstance); } } } }