Danro-Jump/Assets/Scripts/Runtime/Gameplay/Enemies/ProceduralEnemySpawner.cs
Danil Iarosh 01a6f631ef Implement Object Pooling for platform items and procedural enemies to optimize memory allocations
Подробный список изменений:
- В компонент PlatformSpawnedContent добавлен указатель SourcePrefab на оригинальный префаб.
- Переписаны методы создания и очистки в PlatformContentSpawner: при движении экрана вниз монеты, пружины и другое оборудование не уничтожаются, а деактивируются и переносятся в скрытый пул Platform Content Pool. При движении вверх объекты активируются из пула по их префабам.
- Реализован аналогичный пулинг в ProceduralEnemySpawner для процедурных врагов с очисткой пула в OnDestroy при выгрузке сцены.
2026-07-03 15:35:40 +03:00

313 lines
11 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 UnityEngine;
using Zenject;
namespace DanroJump.Gameplay
{
/// <summary>
/// Создает летающих и универсальных врагов в диапазонах высоты, которые подготовил PlatformSpawner.
/// Перед созданием проверяет, что область врага не пересекает платформенные коллайдеры.
/// </summary>
public sealed class ProceduralEnemySpawner : MonoBehaviour
{
[Header("Spawn cadence")]
[SerializeField] [Min(0f)] private float initialSpawnY = 8f;
[SerializeField] [Min(0.1f)] private float verticalStep = 12f;
[SerializeField] [Min(0)] private int maxAliveEnemies = 8;
[SerializeField] [Min(1)] private int spawnAttempts = 12;
[Header("Spawn area")]
[SerializeField] [Min(0f)] private float horizontalPadding = 0.85f;
[SerializeField] [Min(0f)] private float verticalJitter = 1.5f;
[SerializeField] private Vector2 fallbackEnemyHalfExtents = new(0.55f, 0.55f);
[SerializeField] private Vector2 platformClearance = new(0.15f, 0.15f);
[SerializeField] private LayerMask blockingMask = ~0;
private const string PoolRootName = "Procedural Enemy Pool";
private readonly Collider2D[] overlapBuffer = new Collider2D[16];
private readonly System.Collections.Generic.List<GameObject> aliveEnemies = new();
private readonly System.Collections.Generic.Dictionary<GameObject, System.Collections.Generic.List<GameObject>> inactivePool = new();
private DiContainer container;
private Camera gameplayCamera;
private IGameplayDifficultyProgression difficultyProgression;
private System.Random random;
private Transform poolRoot;
private float nextSpawnY;
private bool isInitialized;
/// <summary>
/// Инициализирует спавнер из PlatformSpawner, чтобы seed и генерация были синхронизированы.
/// </summary>
public void Initialize(
DiContainer injectedContainer,
Camera injectedGameplayCamera,
IGameplayDifficultyProgression injectedDifficultyProgression,
int seed)
{
container = injectedContainer;
gameplayCamera = injectedGameplayCamera;
difficultyProgression = injectedDifficultyProgression;
random = new System.Random(seed);
nextSpawnY = initialSpawnY;
isInitialized = true;
}
/// <summary>
/// Создает врагов в новом вертикальном диапазоне, который только что сгенерировал PlatformSpawner.
/// </summary>
public void SpawnForGeneratedRange(float fromY, float toY)
{
CleanupDestroyedEnemies();
if (!CanTrySpawn())
{
return;
}
var minY = Mathf.Min(fromY, toY);
var maxY = Mathf.Max(fromY, toY);
while (nextSpawnY < minY)
{
nextSpawnY += verticalStep;
}
while (nextSpawnY <= maxY && CanTrySpawn())
{
TrySpawnEnemyAtNextStep(minY, maxY);
nextSpawnY += verticalStep;
}
}
private Transform GetPoolRoot()
{
if (poolRoot != null)
{
return poolRoot;
}
var rootObject = GameObject.Find(PoolRootName);
if (rootObject == null)
{
rootObject = new GameObject(PoolRootName);
rootObject.SetActive(false);
}
poolRoot = rootObject.transform;
return poolRoot;
}
private void Recycle(GameObject enemy)
{
if (enemy == null)
{
return;
}
if (enemy.TryGetComponent(out PlatformSpawnedContent marker) && marker.SourcePrefab != null)
{
enemy.SetActive(false);
enemy.transform.SetParent(GetPoolRoot(), false);
var sourcePrefab = marker.SourcePrefab;
if (!inactivePool.TryGetValue(sourcePrefab, out var list))
{
list = new System.Collections.Generic.List<GameObject>();
inactivePool.Add(sourcePrefab, list);
}
list.Add(enemy);
}
else
{
Destroy(enemy);
}
}
/// <summary>
/// Удаляет процедурных врагов ниже линии рецикла платформ с утилизацией в пул.
/// </summary>
public void CleanupBelow(float worldY)
{
for (var index = aliveEnemies.Count - 1; index >= 0; index--)
{
var enemy = aliveEnemies[index];
if (enemy == null)
{
aliveEnemies.RemoveAt(index);
continue;
}
if (enemy.transform.position.y < worldY)
{
Recycle(enemy);
aliveEnemies.RemoveAt(index);
}
}
}
private bool CanTrySpawn()
{
return isInitialized &&
gameplayCamera != null &&
container != null &&
difficultyProgression != null &&
(maxAliveEnemies <= 0 || aliveEnemies.Count < maxAliveEnemies);
}
private void TrySpawnEnemyAtNextStep(float minGeneratedY, float maxGeneratedY)
{
if (!difficultyProgression.TryGetProceduralEnemyPrefab(nextSpawnY, random, out var prefab))
{
return;
}
var halfExtents = GetEnemyHalfExtents(prefab);
for (var attempt = 0; attempt < spawnAttempts; attempt++)
{
var position = CreateSpawnPosition(minGeneratedY, maxGeneratedY, halfExtents);
if (IntersectsBlockingCollider(position, halfExtents + platformClearance))
{
continue;
}
Spawn(prefab, position);
return;
}
}
private Vector3 CreateSpawnPosition(float minGeneratedY, float maxGeneratedY, Vector2 halfExtents)
{
var cameraHalfWidth = gameplayCamera.orthographicSize * gameplayCamera.aspect;
var minX = gameplayCamera.transform.position.x - cameraHalfWidth + horizontalPadding + halfExtents.x;
var maxX = gameplayCamera.transform.position.x + cameraHalfWidth - horizontalPadding - halfExtents.x;
var minY = minGeneratedY + halfExtents.y;
var maxY = Mathf.Max(minY, maxGeneratedY - halfExtents.y);
var jitteredY = nextSpawnY + RandomRange(-verticalJitter, verticalJitter);
return new Vector3(
RandomRange(minX, maxX),
Mathf.Clamp(jitteredY, minY, maxY),
0f);
}
private void Spawn(GameObject prefab, Vector3 position)
{
GameObject instance = null;
if (inactivePool.TryGetValue(prefab, out var list) && list.Count > 0)
{
var lastIndex = list.Count - 1;
instance = list[lastIndex];
list.RemoveAt(lastIndex);
if (instance != null)
{
instance.transform.SetParent(transform, false);
instance.transform.position = position;
instance.SetActive(true);
}
}
if (instance == null)
{
instance = container.InstantiatePrefab(prefab, position, Quaternion.identity, transform);
}
if (!instance.TryGetComponent(out PlatformSpawnedContent marker))
{
marker = instance.AddComponent<PlatformSpawnedContent>();
}
marker.SourcePrefab = prefab;
marker.ResetForPlatformSpawn();
aliveEnemies.Add(instance);
}
private bool IntersectsBlockingCollider(Vector3 position, Vector2 halfExtents)
{
var contactFilter = new ContactFilter2D
{
useLayerMask = true,
layerMask = blockingMask,
useTriggers = false
};
var count = Physics2D.OverlapBox(position, halfExtents * 2f, 0f, contactFilter, overlapBuffer);
for (var index = 0; index < count; index++)
{
var hit = overlapBuffer[index];
if (hit != null && !hit.GetComponentInParent<EnemyBase>())
{
return true;
}
}
return false;
}
private Vector2 GetEnemyHalfExtents(GameObject prefab)
{
var collider = prefab != null ? prefab.GetComponentInChildren<Collider2D>(true) : null;
if (collider == null)
{
return fallbackEnemyHalfExtents;
}
return new Vector2(
Mathf.Max(fallbackEnemyHalfExtents.x, collider.bounds.extents.x),
Mathf.Max(fallbackEnemyHalfExtents.y, collider.bounds.extents.y));
}
private void CleanupDestroyedEnemies()
{
for (var index = aliveEnemies.Count - 1; index >= 0; index--)
{
var enemy = aliveEnemies[index];
if (enemy == null)
{
aliveEnemies.RemoveAt(index);
continue;
}
if (!enemy.activeSelf || !enemy.activeInHierarchy)
{
Recycle(enemy);
aliveEnemies.RemoveAt(index);
}
}
}
private float RandomRange(float min, float max)
{
if (max <= min)
{
return min;
}
return Mathf.Lerp(min, max, (float)(random?.NextDouble() ?? 0d));
}
private void OnDestroy()
{
foreach (var pair in inactivePool)
{
if (pair.Value != null)
{
foreach (var obj in pair.Value)
{
if (obj != null)
{
Destroy(obj);
}
}
pair.Value.Clear();
}
}
inactivePool.Clear();
if (poolRoot != null)
{
Destroy(poolRoot.gameObject);
poolRoot = null;
}
}
}
}