From 01a6f631ef55e1e3196e25e289f48a97e7551257 Mon Sep 17 00:00:00 2001 From: Danil Iarosh Date: Fri, 3 Jul 2026 15:35:40 +0300 Subject: [PATCH] Implement Object Pooling for platform items and procedural enemies to optimize memory allocations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Подробный список изменений: - В компонент PlatformSpawnedContent добавлен указатель SourcePrefab на оригинальный префаб. - Переписаны методы создания и очистки в PlatformContentSpawner: при движении экрана вниз монеты, пружины и другое оборудование не уничтожаются, а деактивируются и переносятся в скрытый пул Platform Content Pool. При движении вверх объекты активируются из пула по их префабам. - Реализован аналогичный пулинг в ProceduralEnemySpawner для процедурных врагов с очисткой пула в OnDestroy при выгрузке сцены. --- .../Enemies/ProceduralEnemySpawner.cs | 116 +++++++++++++++++- .../Spawning/PlatformContentSpawner.cs | 105 ++++++++++++++-- .../Spawning/PlatformSpawnedContent.cs | 6 + 3 files changed, 214 insertions(+), 13 deletions(-) diff --git a/Assets/Scripts/Runtime/Gameplay/Enemies/ProceduralEnemySpawner.cs b/Assets/Scripts/Runtime/Gameplay/Enemies/ProceduralEnemySpawner.cs index 0198162..9bf4bdb 100644 --- a/Assets/Scripts/Runtime/Gameplay/Enemies/ProceduralEnemySpawner.cs +++ b/Assets/Scripts/Runtime/Gameplay/Enemies/ProceduralEnemySpawner.cs @@ -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 aliveEnemies = new(); + private readonly System.Collections.Generic.Dictionary> 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(); + inactivePool.Add(sourcePrefab, list); + } + list.Add(enemy); + } + else + { + Destroy(enemy); + } + } + /// - /// Удаляет процедурных врагов ниже линии рецикла платформ. + /// Удаляет процедурных врагов ниже линии рецикла платформ с утилизацией в пул. /// 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(); + } + + 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; + } + } } } diff --git a/Assets/Scripts/Runtime/Gameplay/Spawning/PlatformContentSpawner.cs b/Assets/Scripts/Runtime/Gameplay/Spawning/PlatformContentSpawner.cs index 3b92f7b..bfe50ee 100644 --- a/Assets/Scripts/Runtime/Gameplay/Spawning/PlatformContentSpawner.cs +++ b/Assets/Scripts/Runtime/Gameplay/Spawning/PlatformContentSpawner.cs @@ -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> spawnedContentByPlatform = new(); private readonly List orphanMarkerBuffer = new(); private readonly List spawnPointCandidates = new(); + private readonly Dictionary> inactivePool = new(); private Transform enemyRoot; + private Transform poolRoot; /// /// Получает 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(); + inactivePool.Add(sourcePrefab, list); + } + list.Add(content); + } + else + { + Object.Destroy(content); + } + } + /// /// Очищает старый контент платформы и спавнит новый по правилам источника. /// @@ -62,7 +109,7 @@ namespace DanroJump.Gameplay } /// - /// Очищает контент всех отслеживаемых платформ. + /// Очищает контент всех отслеживаемых платформ и полностью освобождает пул. /// 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; + } } /// @@ -97,14 +166,33 @@ namespace DanroJump.Gameplay } /// - /// Создает конкретный prefab в точке спавна и привязывает оборудование к платформе. + /// Создает конкретный prefab в точке спавна с поддержкой пулинга. /// 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(); } + marker.SourcePrefab = prefab; marker.ResetForPlatformSpawn(); if (instance.TryGetComponent(out IPlatformEquipment equipment)) @@ -175,9 +264,9 @@ namespace DanroJump.Gameplay } /// - /// Уничтожает объекты из уже выделенного списка без освобождения самого списка. + /// Утилизирует или уничтожает объекты из уже выделенного списка без освобождения самого списка. /// - private static void ClearContentList(List spawnedContent) + private void ClearContentList(List 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); } } /// - /// Fallback-очистка дочерних объектов, если словарь не содержит платформу. + /// Fallback-очистка дочерних объектов с утилизацией в пул. /// private void ClearOrphanSpawnedContent(PlatformController platform) { @@ -216,7 +305,7 @@ namespace DanroJump.Gameplay continue; } - Object.Destroy(marker.gameObject); + RecycleOrDestroy(marker.gameObject); } } diff --git a/Assets/Scripts/Runtime/Gameplay/Spawning/PlatformSpawnedContent.cs b/Assets/Scripts/Runtime/Gameplay/Spawning/PlatformSpawnedContent.cs index e8a0c52..789a7fb 100644 --- a/Assets/Scripts/Runtime/Gameplay/Spawning/PlatformSpawnedContent.cs +++ b/Assets/Scripts/Runtime/Gameplay/Spawning/PlatformSpawnedContent.cs @@ -12,6 +12,12 @@ namespace DanroJump.Gameplay /// public bool IsDetachedFromPlatform { get; private set; } + /// + /// Ссылка на оригинальный префаб, из которого был инстанцирован данный объект. + /// Используется для возврата объекта в нужную ветку пула. + /// + public GameObject SourcePrefab { get; set; } + /// /// Помечает объект как больше не принадлежащий платформе. ///