using System.Collections.Generic; using UnityEngine; using UnityEngine.Serialization; using Zenject; namespace DanroJump.Gameplay { /// /// Генерирует и переиспользует платформы выше камеры с учетом текущего этапа сложности. /// public sealed class PlatformSpawner : MonoBehaviour { [Min(1)] [SerializeField] private int initialPlatformCount = 34; [Min(0f)] [SerializeField] private float horizontalPadding = 0.85f; [FormerlySerializedAs("recycleDistanceBelowCamera")] [SerializeField] [Min(0f)] [Tooltip("How far below the camera a platform must fall before it is recycled. Increase this to make platforms disappear lower.")] private float recycleDistanceBelowCameraBottom = 12f; [SerializeField] private bool useHardwareRandomSeed = true; [SerializeField] private int randomSeed = 1207; [SerializeField] private ProceduralEnemySpawner proceduralEnemySpawner; private readonly List platforms = new(); private readonly Dictionary prefabByPlatform = new(); private readonly Dictionary platformHalfWidthByPrefab = new(); private DiContainer container; private Camera gameplayCamera; private IGameplayDifficultyProgression difficultyProgression; private PlatformContentSpawner contentSpawner; private JumpPlayerController playerController; private System.Random random; private int currentRandomSeed; private float highestPlatformY; private int nextSpawnIndex; /// /// Seed, который реально использует генератор платформ в текущем запуске. /// public int CurrentRandomSeed => currentRandomSeed; /// /// Самая высокая Y-координата, до которой уже сгенерированы платформы. /// public float CurrentGenerationY => highestPlatformY; /// /// Имя этапа, по которому сейчас генерируются платформы. /// public string CurrentGenerationStage => GetStageName(highestPlatformY); /// /// Прогресс генерации внутри текущего этапа. /// public float CurrentGenerationStageProgress => ProgressionForDiagnostics != null ? ProgressionForDiagnostics.GetStageProgress(highestPlatformY) : 0f; /// /// Текущее распределение платформ по ширине уровня. /// public float CurrentPlatformDistribution => ProgressionForDiagnostics != null ? ProgressionForDiagnostics.GetHorizontalDistribution(highestPlatformY) : 0f; private IGameplayDifficultyProgression ProgressionForDiagnostics => difficultyProgression; /// /// Получает зависимости через SceneContext: контейнер, игрока, камеру, контент-спавнер и прогрессию. /// [Inject] public void Construct( DiContainer injectedContainer, JumpPlayerController playerController, Camera injectedGameplayCamera, PlatformContentSpawner injectedContentSpawner, IGameplayDifficultyProgression injectedDifficultyProgression) { container = injectedContainer; this.playerController = playerController; gameplayCamera = injectedGameplayCamera; contentSpawner = injectedContentSpawner; difficultyProgression = injectedDifficultyProgression; } /// /// Инициализирует генератор случайных чисел и выводит выбранный seed в консоль. /// private void Awake() { currentRandomSeed = useHardwareRandomSeed ? CreateHardwareRandomSeed() : randomSeed; random = new System.Random(currentRandomSeed); Debug.Log($"{nameof(PlatformSpawner)} random seed: {currentRandomSeed} ({(useHardwareRandomSeed ? "hardware" : "manual")}).", this); } /// /// Проверяет runtime-зависимости и создает стартовый набор платформ. /// private void Start() { if (!ValidateRuntimeState()) { return; } ResolveProceduralEnemySpawner(); proceduralEnemySpawner?.Initialize(container, gameplayCamera, difficultyProgression, currentRandomSeed); GenerateInitialPlatforms(); } /// /// Каждый кадр переносит платформы, которые ушли ниже камеры, наверх уровня. /// private void Update() { RecyclePlatformsBelowCamera(); } private void OnDestroy() { if (contentSpawner != null) { // Перед уничтожением сцены очищаем дочерний контент платформ, чтобы не оставлять связи на prefab. foreach (var platform in platforms) { contentSpawner.ClearSpawnedContent(platform); } } prefabByPlatform.Clear(); proceduralEnemySpawner?.CleanupBelow(float.PositiveInfinity); } /// /// Создает стартовую площадку и начальную вертикальную цепочку платформ. /// private void GenerateInitialPlatforms() { highestPlatformY = difficultyProgression.InitialSpawnY; nextSpawnIndex = 0; SpawnPlatform(GetPlatformPrefab(highestPlatformY), new Vector2(GetCameraCenterX(), highestPlatformY), true); for (var index = 1; index < initialPlatformCount; index++) { var previousGenerationY = highestPlatformY; highestPlatformY += RandomVerticalSpacing(); var selectedPlatformPrefab = GetPlatformPrefab(highestPlatformY); SpawnPlatform(selectedPlatformPrefab, new Vector2(RandomHorizontalPosition(highestPlatformY, selectedPlatformPrefab), highestPlatformY), false); proceduralEnemySpawner?.SpawnForGeneratedRange(previousGenerationY, highestPlatformY); } } /// /// Находит платформы ниже камеры и переносит их выше текущей генерации. /// private void RecyclePlatformsBelowCamera() { if (gameplayCamera == null || platforms.Count == 0) { return; } var bottomLimit = gameplayCamera.transform.position.y - recycleDistanceBelowCameraBottom; proceduralEnemySpawner?.CleanupBelow(bottomLimit); // Вместо постоянного Instantiate/Destroy переиспользуем платформы ниже видимой зоны. for (var index = 0; index < platforms.Count; index++) { var platform = platforms[index]; if (platform == null || platform.transform.position.y >= bottomLimit) { continue; } var previousGenerationY = highestPlatformY; highestPlatformY += RandomVerticalSpacing(); var selectedPlatformPrefab = GetPlatformPrefab(highestPlatformY); var position = new Vector2(RandomHorizontalPosition(highestPlatformY, selectedPlatformPrefab), highestPlatformY); platforms[index] = RecyclePlatform(platform, selectedPlatformPrefab, position); proceduralEnemySpawner?.SpawnForGeneratedRange(previousGenerationY, highestPlatformY); } } /// /// Создает новую платформу и инициализирует ее контекстом спавна. /// private void SpawnPlatform(GameObject selectedPlatformPrefab, Vector2 position, bool isStartPlatform) { var platform = CreatePlatform(selectedPlatformPrefab, position, isStartPlatform); if (platform == null) { return; } platforms.Add(platform); InitializePlatform(platform, CreateContext(platform, isStartPlatform)); } /// /// Переиспользует платформу или заменяет ее, если активный этап выбрал другой prefab. /// private PlatformController RecyclePlatform(PlatformController platform, GameObject selectedPlatformPrefab, Vector2 position) { if (ShouldReplacePlatform(platform, selectedPlatformPrefab)) { // Если новый этап требует другой prefab, безопаснее заменить объект, чем мутировать старый. return ReplacePlatform(platform, selectedPlatformPrefab, position); } contentSpawner.ClearSpawnedContent(platform); platform.gameObject.SetActive(true); platform.transform.position = new Vector3(position.x, position.y, platform.transform.position.z); RecyclePlatform(platform, CreateContext(platform, false)); return platform; } /// /// Создает экземпляр платформы через DI-контейнер, чтобы дочерние компоненты получили injection. /// private PlatformController CreatePlatform(GameObject prefab, Vector2 position, bool isStartPlatform) { if (prefab == null) { Debug.LogError($"{nameof(PlatformSpawner)} cannot spawn a platform because no platform prefab is configured.", this); return null; } var platformObject = container.InstantiatePrefab(prefab, new Vector3(position.x, position.y, 0f), Quaternion.identity, transform); platformObject.name = isStartPlatform ? "Start Platform" : "Jump Platform"; platformObject.SetActive(true); if (!platformObject.TryGetComponent(out PlatformController platform)) { Debug.LogError($"{nameof(PlatformSpawner)} spawned platform without {nameof(PlatformController)}.", platformObject); Destroy(platformObject); return null; } prefabByPlatform[platform] = prefab; return platform; } /// /// Удаляет старый экземпляр платформы и создает новый через DI-контейнер. /// private PlatformController ReplacePlatform(PlatformController platform, GameObject selectedPlatformPrefab, Vector2 position) { contentSpawner.ClearSpawnedContent(platform); prefabByPlatform.Remove(platform); Destroy(platform.gameObject); var replacement = CreatePlatform(selectedPlatformPrefab, position, false); if (replacement == null) { return null; } InitializePlatform(replacement, CreateContext(replacement, false)); return replacement; } /// /// Проверяет, нужно ли заменить платформу из-за смены prefab активного этапа. /// private bool ShouldReplacePlatform(PlatformController platform, GameObject selectedPlatformPrefab) { if (platform == null || selectedPlatformPrefab == null) { return false; } return prefabByPlatform.TryGetValue(platform, out var currentPrefab) && currentPrefab != selectedPlatformPrefab; } /// /// Выполняет первичную инициализацию платформы и спавнит ее содержимое. /// private void InitializePlatform(PlatformController platform, PlatformSpawnContext context) { platform.Initialize(context); contentSpawner.SpawnContent(platform, GetContentSource(context.WorldY), random); } /// /// Передает платформе новый контекст recycle и пересоздает контент. /// private void RecyclePlatform(PlatformController platform, PlatformSpawnContext context) { platform.Recycle(context); contentSpawner.SpawnContent(platform, GetContentSource(context.WorldY), random); } /// /// Создает контекст, который платформа и ее содержимое используют при инициализации. /// private PlatformSpawnContext CreateContext(PlatformController platform, bool isStartPlatform) { return new PlatformSpawnContext(container, playerController, gameplayCamera, nextSpawnIndex++, isStartPlatform, platform.transform.position.y); } /// /// Проверяет, что SceneContext передал все зависимости, необходимые для генерации. /// private bool ValidateRuntimeState() { var isValid = true; if (container == null) { Debug.LogError($"{nameof(PlatformSpawner)} was not injected. Check SceneContext and {nameof(GameplayInstaller)} setup.", this); isValid = false; } if (playerController == null) { Debug.LogError($"{nameof(PlatformSpawner)} has no player reference.", this); isValid = false; } if (gameplayCamera == null) { Debug.LogError($"{nameof(PlatformSpawner)} has no gameplay camera reference.", this); isValid = false; } if (contentSpawner == null) { Debug.LogError($"{nameof(PlatformSpawner)} has no platform content spawner.", this); isValid = false; } if (difficultyProgression == null) { Debug.LogError($"{nameof(PlatformSpawner)} has no difficulty progression reference.", this); isValid = false; } else if (!difficultyProgression.HasStages) { Debug.LogError($"{nameof(PlatformSpawner)} has no difficulty stages configured.", this); isValid = false; } return isValid; } /// /// Находит procedural enemy spawner рядом с генератором платформ, если ссылка не задана в инспекторе. /// private void ResolveProceduralEnemySpawner() { if (proceduralEnemySpawner == null) { proceduralEnemySpawner = GetComponentInChildren(true); } } /// /// Выбирает X-позицию платформы так, чтобы ее края оставались внутри границ камеры. /// private float RandomHorizontalPosition(float worldY, GameObject platformPrefab) { var halfWidth = gameplayCamera != null ? gameplayCamera.orthographicSize * gameplayCamera.aspect : 3.75f; var platformHalfWidth = GetPlatformHalfWidth(platformPrefab); var usableHalfWidth = Mathf.Max(0f, halfWidth - horizontalPadding - platformHalfWidth); var centerX = GetCameraCenterX(); if (usableHalfWidth <= 0f) { return centerX; } var distribution = difficultyProgression != null ? difficultyProgression.GetHorizontalDistribution(worldY) : 0f; var signed = RandomRange(-1f, 1f); var sign = signed < 0f ? -1f : 1f; var magnitude = Mathf.Abs(signed); // distribution -1 сжимает позиции к центру, +1 вытягивает их к краям. var normalizedMagnitude = distribution < 0f ? magnitude * (distribution + 1f) : Mathf.Lerp(magnitude, 1f, distribution); return centerX + sign * normalizedMagnitude * usableHalfWidth; } private float GetCameraCenterX() { return gameplayCamera != null ? gameplayCamera.transform.position.x : 0f; } private float GetPlatformHalfWidth(GameObject platformPrefab) { if (platformPrefab == null) { return 0f; } if (platformHalfWidthByPrefab.TryGetValue(platformPrefab, out var cachedHalfWidth)) { return cachedHalfWidth; } var rootX = platformPrefab.transform.position.x; var minX = float.PositiveInfinity; var maxX = float.NegativeInfinity; var renderers = platformPrefab.GetComponentsInChildren(true); for (var index = 0; index < renderers.Length; index++) { EncapsulateBounds(renderers[index].bounds, ref minX, ref maxX); } var colliders = platformPrefab.GetComponentsInChildren(true); for (var index = 0; index < colliders.Length; index++) { EncapsulateBounds(colliders[index].bounds, ref minX, ref maxX); } var halfWidth = float.IsInfinity(minX) || float.IsInfinity(maxX) ? 0f : Mathf.Max(Mathf.Abs(minX - rootX), Mathf.Abs(maxX - rootX)); platformHalfWidthByPrefab[platformPrefab] = halfWidth; return halfWidth; } private static void EncapsulateBounds(Bounds bounds, ref float minX, ref float maxX) { if (bounds.size.x <= 0f) { return; } minX = Mathf.Min(minX, bounds.min.x); maxX = Mathf.Max(maxX, bounds.max.x); } private float RandomRange(float min, float max) { return Mathf.Lerp(min, max, (float)random.NextDouble()); } /// /// Генерирует seed из времени и характеристик текущего устройства. /// private static int CreateHardwareRandomSeed() { var unixTime = System.DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); var seedSource = string.Concat( unixTime, "|", SystemInfo.processorType, "|", SystemInfo.processorCount, "|", SystemInfo.graphicsDeviceName); return StableHash(seedSource); } /// /// Стабильно превращает строку источников entropy в положительный int seed. /// private static int StableHash(string value) { unchecked { const int offsetBasis = -2128831035; const int prime = 16777619; var hash = offsetBasis; for (var index = 0; index < value.Length; index++) { hash ^= value[index]; hash *= prime; } return hash == int.MinValue ? int.MaxValue : Mathf.Abs(hash); } } /// /// Возвращает вертикальный шаг до следующей платформы из текущего этапа. /// private float RandomVerticalSpacing() { if (difficultyProgression != null) { return difficultyProgression.GetVerticalSpacing(highestPlatformY); } return 0f; } /// /// Выбирает prefab платформы для указанной высоты. /// private GameObject GetPlatformPrefab(float worldY) { return difficultyProgression != null ? difficultyProgression.GetPlatformPrefab(worldY, random) : null; } /// /// Создает источник контента для указанной высоты генерации. /// private IPlatformContentSource GetContentSource(float worldY) { return difficultyProgression != null ? difficultyProgression.CreateContentSource(worldY) : null; } /// /// Возвращает имя этапа для diagnostics overlay. /// private string GetStageName(float worldY) { return ProgressionForDiagnostics != null ? ProgressionForDiagnostics.GetStageName(worldY) : string.Empty; } } }