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; private readonly List platforms = new(); private readonly Dictionary prefabByPlatform = new(); private DiContainer container; private Camera gameplayCamera; private GameplayDifficultyProgression difficultyProgression; private PlatformContentSpawner contentSpawner; private JumpPlayerController playerController; private System.Random random; private int currentRandomSeed; private float highestPlatformY; private int nextSpawnIndex; public int CurrentRandomSeed => currentRandomSeed; 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 GameplayDifficultyProgression ProgressionForDiagnostics => difficultyProgression != null ? difficultyProgression : FindFirstObjectByType(); [Inject] public void Construct( DiContainer injectedContainer, JumpPlayerController playerController, Camera injectedGameplayCamera, PlatformContentSpawner injectedContentSpawner, GameplayDifficultyProgression injectedDifficultyProgression) { container = injectedContainer; this.playerController = playerController; gameplayCamera = injectedGameplayCamera; contentSpawner = injectedContentSpawner; difficultyProgression = injectedDifficultyProgression; } private void Awake() { currentRandomSeed = useHardwareRandomSeed ? CreateHardwareRandomSeed() : randomSeed; random = new System.Random(currentRandomSeed); Debug.Log($"{nameof(PlatformSpawner)} random seed: {currentRandomSeed} ({(useHardwareRandomSeed ? "hardware" : "manual")}).", this); } private void Start() { if (!ValidateRuntimeState()) { return; } GenerateInitialPlatforms(); } private void Update() { RecyclePlatformsBelowCamera(); } private void OnDestroy() { if (contentSpawner == null) { return; } foreach (var platform in platforms) { contentSpawner.ClearSpawnedContent(platform); } prefabByPlatform.Clear(); } private void GenerateInitialPlatforms() { highestPlatformY = difficultyProgression.InitialSpawnY; nextSpawnIndex = 0; SpawnPlatform(new Vector2(0f, highestPlatformY), true); for (var index = 1; index < initialPlatformCount; index++) { highestPlatformY += RandomVerticalSpacing(); SpawnPlatform(new Vector2(RandomHorizontalPosition(highestPlatformY), highestPlatformY), false); } } private void RecyclePlatformsBelowCamera() { if (gameplayCamera == null || platforms.Count == 0) { return; } var bottomLimit = gameplayCamera.transform.position.y - recycleDistanceBelowCameraBottom; for (var index = 0; index < platforms.Count; index++) { var platform = platforms[index]; if (platform == null || platform.transform.position.y >= bottomLimit) { continue; } highestPlatformY += RandomVerticalSpacing(); platforms[index] = RecyclePlatform(platform, new Vector2(RandomHorizontalPosition(highestPlatformY), highestPlatformY)); } } private void SpawnPlatform(Vector2 position, bool isStartPlatform) { var selectedPlatformPrefab = GetPlatformPrefab(position.y); var platform = CreatePlatform(selectedPlatformPrefab, position, isStartPlatform); if (platform == null) { return; } platforms.Add(platform); InitializePlatform(platform, CreateContext(platform, isStartPlatform)); } private PlatformController RecyclePlatform(PlatformController platform, Vector2 position) { var selectedPlatformPrefab = GetPlatformPrefab(position.y); if (ShouldReplacePlatform(platform, selectedPlatformPrefab)) { 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; } 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; } private PlatformController ReplacePlatform(PlatformController platform, GameObject selectedPlatformPrefab, Vector2 position) { contentSpawner.ClearSpawnedContent(platform); prefabByPlatform.Remove(platform); platform.gameObject.SetActive(false); Destroy(platform.gameObject); var replacement = CreatePlatform(selectedPlatformPrefab, position, false); if (replacement == null) { return null; } InitializePlatform(replacement, CreateContext(replacement, false)); return replacement; } 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); } 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); } 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; } private float RandomHorizontalPosition(float worldY) { var halfWidth = gameplayCamera != null ? gameplayCamera.orthographicSize * gameplayCamera.aspect : 3.75f; var usableHalfWidth = Mathf.Max(0f, halfWidth - horizontalPadding); if (usableHalfWidth <= 0f) { return 0f; } var distribution = difficultyProgression != null ? difficultyProgression.GetHorizontalDistribution(worldY) : 0f; var signed = RandomRange(-1f, 1f); var sign = signed < 0f ? -1f : 1f; var magnitude = Mathf.Abs(signed); var normalizedMagnitude = distribution < 0f ? magnitude * (distribution + 1f) : Mathf.Lerp(magnitude, 1f, distribution); return sign * normalizedMagnitude * usableHalfWidth; } private float RandomRange(float min, float max) { return Mathf.Lerp(min, max, (float)random.NextDouble()); } private static int CreateHardwareRandomSeed() { var unixTime = System.DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); var seedSource = string.Concat( unixTime, "|", SystemInfo.processorType, "|", SystemInfo.processorCount, "|", SystemInfo.graphicsDeviceName); return StableHash(seedSource); } 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; } 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; } private string GetStageName(float worldY) { return ProgressionForDiagnostics != null ? ProgressionForDiagnostics.GetStageName(worldY) : string.Empty; } } }