Implement Object Pooling for platform items and procedural enemies to optimize memory allocations

Подробный список изменений:
- В компонент PlatformSpawnedContent добавлен указатель SourcePrefab на оригинальный префаб.
- Переписаны методы создания и очистки в PlatformContentSpawner: при движении экрана вниз монеты, пружины и другое оборудование не уничтожаются, а деактивируются и переносятся в скрытый пул Platform Content Pool. При движении вверх объекты активируются из пула по их префабам.
- Реализован аналогичный пулинг в ProceduralEnemySpawner для процедурных врагов с очисткой пула в OnDestroy при выгрузке сцены.
This commit is contained in:
Данил Ярош 2026-07-03 15:35:40 +03:00
parent 839a68aef1
commit 01a6f631ef
3 changed files with 214 additions and 13 deletions

View File

@ -22,12 +22,15 @@ namespace DanroJump.Gameplay
[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;
@ -73,8 +76,52 @@ namespace DanroJump.Gameplay
}
}
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)
{
@ -89,7 +136,7 @@ namespace DanroJump.Gameplay
if (enemy.transform.position.y < worldY)
{
Destroy(enemy);
Recycle(enemy);
aliveEnemies.RemoveAt(index);
}
}
@ -142,8 +189,34 @@ namespace DanroJump.Gameplay
private void Spawn(GameObject prefab, Vector3 position)
{
var instance = container.InstantiatePrefab(prefab, position, Quaternion.identity, transform);
instance.SetActive(true);
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);
}
@ -186,9 +259,17 @@ namespace DanroJump.Gameplay
{
for (var index = aliveEnemies.Count - 1; index >= 0; index--)
{
if (aliveEnemies[index] == null || !aliveEnemies[index].activeInHierarchy)
var enemy = aliveEnemies[index];
if (enemy == null)
{
aliveEnemies.RemoveAt(index);
continue;
}
if (!enemy.activeSelf || !enemy.activeInHierarchy)
{
Recycle(enemy);
aliveEnemies.RemoveAt(index);
}
}
}
@ -202,5 +283,30 @@ namespace DanroJump.Gameplay
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;
}
}
}
}

View File

@ -10,12 +10,15 @@ namespace DanroJump.Gameplay
public sealed class PlatformContentSpawner
{
private const string EnemyRootName = "Enemy Pool";
private const string PoolRootName = "Platform Content Pool";
private readonly DiContainer container;
private readonly Dictionary<PlatformController, List<GameObject>> spawnedContentByPlatform = new();
private readonly List<PlatformSpawnedContent> orphanMarkerBuffer = new();
private readonly List<PlatformSpawnPoint> spawnPointCandidates = new();
private readonly Dictionary<GameObject, List<GameObject>> inactivePool = new();
private Transform enemyRoot;
private Transform poolRoot;
/// <summary>
/// Получает DI-контейнер для создания prefab с injection.
@ -25,6 +28,50 @@ namespace DanroJump.Gameplay
this.container = container;
}
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 RecycleOrDestroy(GameObject content)
{
if (content == null)
{
return;
}
if (content.TryGetComponent(out PlatformSpawnedContent marker) && marker.SourcePrefab != null)
{
content.SetActive(false);
content.transform.SetParent(GetPoolRoot(), false);
var sourcePrefab = marker.SourcePrefab;
if (!inactivePool.TryGetValue(sourcePrefab, out var list))
{
list = new List<GameObject>();
inactivePool.Add(sourcePrefab, list);
}
list.Add(content);
}
else
{
Object.Destroy(content);
}
}
/// <summary>
/// Очищает старый контент платформы и спавнит новый по правилам источника.
/// </summary>
@ -62,7 +109,7 @@ namespace DanroJump.Gameplay
}
/// <summary>
/// Очищает контент всех отслеживаемых платформ.
/// Очищает контент всех отслеживаемых платформ и полностью освобождает пул.
/// </summary>
public void ClearAll()
{
@ -72,6 +119,28 @@ namespace DanroJump.Gameplay
}
spawnedContentByPlatform.Clear();
foreach (var pair in inactivePool)
{
if (pair.Value != null)
{
foreach (var obj in pair.Value)
{
if (obj != null)
{
Object.Destroy(obj);
}
}
pair.Value.Clear();
}
}
inactivePool.Clear();
if (poolRoot != null)
{
Object.Destroy(poolRoot.gameObject);
poolRoot = null;
}
}
/// <summary>
@ -97,14 +166,33 @@ namespace DanroJump.Gameplay
}
/// <summary>
/// Создает конкретный prefab в точке спавна и привязывает оборудование к платформе.
/// Создает конкретный prefab в точке спавна с поддержкой пулинга.
/// </summary>
private void Spawn(GameObject prefab, Transform parent, PlatformController platform)
{
var targetParent = parent != null ? parent : platform.transform;
var isEnemy = EnemyPrefabRules.TryGetEnemy(prefab, out _);
var instanceParent = isEnemy ? GetEnemyRoot(platform) : targetParent;
var instance = container.InstantiatePrefab(prefab, targetParent.position, targetParent.rotation, instanceParent);
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(instanceParent, false);
instance.transform.SetPositionAndRotation(targetParent.position, targetParent.rotation);
instance.SetActive(true);
}
}
if (instance == null)
{
instance = container.InstantiatePrefab(prefab, targetParent.position, targetParent.rotation, instanceParent);
}
if (isEnemy)
{
@ -117,6 +205,7 @@ namespace DanroJump.Gameplay
marker = instance.AddComponent<PlatformSpawnedContent>();
}
marker.SourcePrefab = prefab;
marker.ResetForPlatformSpawn();
if (instance.TryGetComponent(out IPlatformEquipment equipment))
@ -175,9 +264,9 @@ namespace DanroJump.Gameplay
}
/// <summary>
/// Уничтожает объекты из уже выделенного списка без освобождения самого списка.
/// Утилизирует или уничтожает объекты из уже выделенного списка без освобождения самого списка.
/// </summary>
private static void ClearContentList(List<GameObject> spawnedContent)
private void ClearContentList(List<GameObject> spawnedContent)
{
for (var index = spawnedContent.Count - 1; index >= 0; index--)
{
@ -194,13 +283,13 @@ namespace DanroJump.Gameplay
continue;
}
Object.Destroy(content);
RecycleOrDestroy(content);
spawnedContent.RemoveAt(index);
}
}
/// <summary>
/// Fallback-очистка дочерних объектов, если словарь не содержит платформу.
/// Fallback-очистка дочерних объектов с утилизацией в пул.
/// </summary>
private void ClearOrphanSpawnedContent(PlatformController platform)
{
@ -216,7 +305,7 @@ namespace DanroJump.Gameplay
continue;
}
Object.Destroy(marker.gameObject);
RecycleOrDestroy(marker.gameObject);
}
}

View File

@ -12,6 +12,12 @@ namespace DanroJump.Gameplay
/// </summary>
public bool IsDetachedFromPlatform { get; private set; }
/// <summary>
/// Ссылка на оригинальный префаб, из которого был инстанцирован данный объект.
/// Используется для возврата объекта в нужную ветку пула.
/// </summary>
public GameObject SourcePrefab { get; set; }
/// <summary>
/// Помечает объект как больше не принадлежащий платформе.
/// </summary>