using System.Collections.Generic; using UnityEngine; using Zenject; namespace DanroJump.Gameplay { /// /// Создает collectibles, equipment и enemies на точках спавна платформы. /// 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. /// public PlatformContentSpawner(DiContainer container) { 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.scene.isLoaded) { Object.Destroy(content); 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); } } /// /// Очищает старый контент платформы и спавнит новый по правилам источника. /// public void SpawnContent( PlatformController platform, IPlatformContentSource catalog, System.Random random) { ClearSpawnedContent(platform); if (platform == null || catalog == null) { return; } SpawnIntoPoints(platform, catalog, PlatformContentKind.Collectible, platform.CollectibleSpawnPoints, random); SpawnIntoPoints(platform, catalog, PlatformContentKind.Equipment, platform.EquipmentSpawnPoints, random); SpawnIntoPoints(platform, catalog, PlatformContentKind.Enemy, platform.EnemySpawnPoints, random); } /// /// Удаляет весь контент, созданный для указанной платформы. /// public void ClearSpawnedContent(PlatformController platform) { if (platform == null) { return; } if (!ClearTrackedContent(platform)) { ClearOrphanSpawnedContent(platform); } } /// /// Очищает контент всех отслеживаемых платформ и полностью освобождает пул. /// public void ClearAll() { foreach (var pair in spawnedContentByPlatform) { ClearContentList(pair.Value); } 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; } } /// /// Спавнит одну категорию контента в одну выбранную точку платформы. /// private void SpawnIntoPoints( PlatformController platform, IPlatformContentSource catalog, PlatformContentKind kind, IReadOnlyList spawnPoints, System.Random random) { if (spawnPoints == null || !catalog.ShouldSpawn(kind, random) || !catalog.TryGetPrefab(kind, random, out var prefab)) { return; } var selectedSpawnPoint = SelectSpawnPoint(spawnPoints, random); if (selectedSpawnPoint != null) { Spawn(prefab, selectedSpawnPoint.Anchor, platform); } } /// /// Создает конкретный 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; 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) { instance.transform.SetPositionAndRotation(targetParent.position, targetParent.rotation); } // Маркер нужен, чтобы при аварийной очистке найти даже неотслеженный дочерний контент. if (!instance.TryGetComponent(out PlatformSpawnedContent marker)) { marker = instance.AddComponent(); } marker.SourcePrefab = prefab; marker.ResetForPlatformSpawn(); if (instance.TryGetComponent(out IPlatformEquipment equipment)) { equipment.Install(platform); } if (!spawnedContentByPlatform.TryGetValue(platform, out var spawnedContent)) { spawnedContent = new List(); spawnedContentByPlatform.Add(platform, spawnedContent); } spawnedContent.Add(instance); } /// /// Возвращает отдельный контейнер для врагов, чтобы платформы не управляли их визуалами и коллайдерами как своими дочерними объектами. /// private Transform GetEnemyRoot(PlatformController platform) { if (enemyRoot != null) { return enemyRoot; } var rootObject = GameObject.Find(EnemyRootName); if (rootObject == null) { rootObject = new GameObject(EnemyRootName); } enemyRoot = rootObject.transform; if (platform != null && platform.transform.parent != null && enemyRoot.parent == null) { enemyRoot.SetParent(platform.transform.parent, false); } return enemyRoot; } /// /// Удаляет контент, который был записан в словарь отслеживания. /// private bool ClearTrackedContent(PlatformController platform) { if (!spawnedContentByPlatform.TryGetValue(platform, out var spawnedContent)) { return false; } ClearContentList(spawnedContent); spawnedContent.Clear(); spawnedContentByPlatform.Remove(platform); return true; } /// /// Утилизирует или уничтожает объекты из уже выделенного списка без освобождения самого списка. /// private void ClearContentList(List spawnedContent) { for (var index = spawnedContent.Count - 1; index >= 0; index--) { var content = spawnedContent[index]; if (content == null) { spawnedContent.RemoveAt(index); continue; } if (content.TryGetComponent(out PlatformSpawnedContent marker) && marker.IsDetachedFromPlatform) { spawnedContent.RemoveAt(index); continue; } RecycleOrDestroy(content); spawnedContent.RemoveAt(index); } } /// /// Fallback-очистка дочерних объектов с утилизацией в пул. /// private void ClearOrphanSpawnedContent(PlatformController platform) { platform.GetComponentsInChildren(true, orphanMarkerBuffer); for (var index = orphanMarkerBuffer.Count - 1; index >= 0; index--) { var marker = orphanMarkerBuffer[index]; if (marker != null) { if (marker.IsDetachedFromPlatform) { continue; } RecycleOrDestroy(marker.gameObject); } } orphanMarkerBuffer.Clear(); } /// /// Проверяет, нужно ли пропустить optional-точку спавна по ее весу. /// private static bool ShouldSkipOptionalPoint(PlatformSpawnPoint spawnPoint, System.Random random) { if (!spawnPoint.Optional) { return false; } if (spawnPoint.Weight <= 0f) { return true; } if (spawnPoint.Weight >= 1f) { return false; } var roll = random != null ? random.NextDouble() : 0d; return roll > spawnPoint.Weight; } /// /// Выбирает одну подходящую точку по весам, чтобы плотность контента не зависела от числа маркеров на prefab. /// private PlatformSpawnPoint SelectSpawnPoint( IReadOnlyList spawnPoints, System.Random random) { spawnPointCandidates.Clear(); var totalWeight = 0f; for (var index = 0; index < spawnPoints.Count; index++) { var spawnPoint = spawnPoints[index]; if (spawnPoint == null || ShouldSkipOptionalPoint(spawnPoint, random)) { continue; } spawnPointCandidates.Add(spawnPoint); totalWeight += Mathf.Max(0f, spawnPoint.Weight); } if (totalWeight <= 0f) { spawnPointCandidates.Clear(); return null; } var roll = (float)((random != null ? random.NextDouble() : 0d) * totalWeight); for (var index = 0; index < spawnPointCandidates.Count; index++) { var spawnPoint = spawnPointCandidates[index]; roll -= Mathf.Max(0f, spawnPoint.Weight); if (roll <= 0f) { spawnPointCandidates.Clear(); return spawnPoint; } } spawnPointCandidates.Clear(); return null; } } }