Danil Iarosh 53afa5498e
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
Fix player bouncing off corners and align collider
- Исправлено поведение игрока при падении с высоты и разворотах (устранен сильный отскок от углов)
- Центрирован PolygonCollider2D по оси X на префабе character_defeat_4.prefab (m_Offset.x установлен в 0)
- Упрощена форма PolygonCollider2D до симметричного выпуклого шестиугольника
- Сохранен бэкап старого префаба игрока в Player_Old.prefab
2026-07-16 15:39:43 +03:00

369 lines
15 KiB
C#
Raw 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;
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);
}
}
}
}