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);
}
}
}
}