Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
- Исправлено поведение игрока при падении с высоты и разворотах (устранен сильный отскок от углов) - Центрирован PolygonCollider2D по оси X на префабе character_defeat_4.prefab (m_Offset.x установлен в 0) - Упрощена форма PolygonCollider2D до симметричного выпуклого шестиугольника - Сохранен бэкап старого префаба игрока в Player_Old.prefab
369 lines
15 KiB
C#
369 lines
15 KiB
C#
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using System;
|
||
using UnityEngine;
|
||
using UnityEngine.VFX;
|
||
using RR.Common.Utility;
|
||
|
||
namespace RR.Game2D.Behaviour
|
||
{
|
||
/// <summary>
|
||
/// Пул объектов визуальных эффектов для переиспользования VFX и оптимизации производительности.
|
||
/// </summary>
|
||
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<GameObject> _pool;
|
||
private readonly Dictionary<GameObject, int> _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<GameObject>();
|
||
|
||
// Предварительно создаем VFX объекты для пула
|
||
for (int i = 0; i < poolSize; i++)
|
||
{
|
||
GameObject vfxGameObject = new GameObject($"VFX_{i}");
|
||
vfxGameObject.AddComponent<VisualEffect>();
|
||
|
||
// Устанавливаем 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;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Применяет Sorting Layer к рендереру VFX объекта
|
||
/// </summary>
|
||
private void ApplySortingLayer(GameObject vfxGameObject)
|
||
{
|
||
Renderer vfxRenderer = vfxGameObject.GetComponent<Renderer>();
|
||
if (vfxRenderer != null)
|
||
{
|
||
vfxRenderer.sortingLayerID = defaultSortingLayer.id;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Получить VFX объект из пула (без автоматического запуска)
|
||
/// </summary>
|
||
/// <param name="vfxAsset">Ассет визуального эффекта для применения</param>
|
||
/// <returns>GameObject с компонентом VisualEffect или null, если не удалось получить</returns>
|
||
private GameObject Get(VisualEffectAsset vfxAsset)
|
||
{
|
||
GameObject vfxInstance;
|
||
|
||
// Проверяем, есть ли свободные объекты в пуле
|
||
if (_pool.Count > 0)
|
||
{
|
||
vfxInstance = _pool.Dequeue();
|
||
|
||
VisualEffect currentVFX = vfxInstance.GetComponent<VisualEffect>();
|
||
// Если текущий ассет не совпадает с запрашиваемым, заменяем его и реинициализируем
|
||
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<VisualEffect>();
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Воспроизвести VFX из пула и автоматически вернуть его позже.
|
||
/// </summary>
|
||
/// <param name="vfxAsset">Ассет визуального эффекта</param>
|
||
/// <param name="position">Позиция для эффекта</param>
|
||
/// <param name="lifetime">Время жизни эффекта в секундах (если null — будет использовано defaultLifetime)</param>
|
||
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<GameObject, VisualEffect> 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<VisualEffect>();
|
||
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<VisualEffect>();
|
||
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<Renderer>();
|
||
if (renderer != null)
|
||
renderer.SetPropertyBlock(null);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Вернуть VFX объект обратно в пул для повторного использования
|
||
/// </summary>
|
||
/// <param name="vfxInstance">VFX объект для возврата в пул</param>
|
||
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<VisualEffect>();
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
}
|