using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Events; using Pooling; using AppleHills.Core.Settings; using AppleHills.Core.Interfaces; using Core; namespace Minigames.DivingForPictures { /// /// Spawns and manages mobile obstacles for the diving minigame. /// Uses object pooling and validates spawn positions to avoid colliding with tiles. /// public class ObstacleSpawner : MonoBehaviour, IPausable { [Header("Obstacle Prefabs")] [Tooltip("List of possible obstacle prefabs to spawn")] [SerializeField] private List obstaclePrefabs; [Header("Events")] [Tooltip("Invoked when a new obstacle is spawned")] public UnityEvent onObstacleSpawned; [Tooltip("Invoked when an obstacle is destroyed or returned to pool")] public UnityEvent onObstacleDestroyed; // Settings references private IDivingMinigameSettings _settings; private DivingDeveloperSettings _devSettings; // Private fields private ObstaclePool _obstaclePool; private UnityEngine.Camera _mainCamera; private float _screenBottom; private float _spawnRangeX; private Coroutine _spawnCoroutine; private Coroutine _moveCoroutine; private Coroutine _despawnCoroutine; private readonly List _activeObstacles = new List(); private int _obstacleCounter = 0; // Counter for unique obstacle naming private bool _isSurfacing = false; // Flag to track surfacing state private float _velocityFactor = 1.0f; // Current velocity factor from the game manager // Pause state private bool _isPaused = false; // IPausable implementation public bool IsPaused => _isPaused; private void Awake() { _mainCamera = UnityEngine.Camera.main; // Get settings from GameManager _settings = GameManager.GetSettingsObject(); _devSettings = GameManager.GetDeveloperSettings(); if (_settings == null) { Debug.LogError("[ObstacleSpawner] Failed to load diving minigame settings!"); } if (_devSettings == null) { Debug.LogError("[ObstacleSpawner] Failed to load diving developer settings!"); } // Validate obstacle prefabs ValidateObstaclePrefabs(); if (_devSettings?.ObstacleUseObjectPooling ?? false) { InitializeObjectPool(); } // Initialize events if null if (onObstacleSpawned == null) onObstacleSpawned = new UnityEvent(); if (onObstacleDestroyed == null) onObstacleDestroyed = new UnityEvent(); } void OnEnable() { // Register with the DivingGameManager for pause/resume events DivingGameManager.Instance.RegisterPausableComponent(this); } private void Start() { DivingGameManager.Instance.OnGameInitialized += Initialize; // If game is already initialized, initialize immediately if (DivingGameManager.Instance.GetType().GetField("_isGameInitialized", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?.GetValue(DivingGameManager.Instance) is bool isInitialized && isInitialized) { Initialize(); } } private void OnDestroy() { DivingGameManager.Instance.UnregisterPausableComponent(this); // Clean up any active coroutines StopAllCoroutines(); } /// /// Initializes the obstacle spawner when triggered by DivingGameManager /// private void Initialize() { // Calculate screen bounds CalculateScreenBounds(); // Start coroutines if not paused StartSpawnCoroutine(); StartMoveCoroutine(); StartDespawnCoroutine(); Logging.Debug("[ObstacleSpawner] Initialized"); } /// /// Pauses the spawner and all associated processes /// public void Pause() { if (_isPaused) return; // Already paused _isPaused = true; // Stop spawning coroutine if (_spawnCoroutine != null) { StopCoroutine(_spawnCoroutine); _spawnCoroutine = null; } // Pause all active obstacles foreach (var obstacle in _activeObstacles.ToArray()) { if (obstacle != null) { FloatingObstacle obstacleComponent = obstacle.GetComponent(); if (obstacleComponent != null) { obstacleComponent.Pause(); } } } Logging.Debug($"[ObstacleSpawner] Paused with {_activeObstacles.Count} active obstacles"); } /// /// Resumes the spawner and all associated processes /// public void DoResume() { if (!_isPaused) return; // Already running _isPaused = false; // Restart spawning coroutine if not in surfacing mode if (!_isSurfacing) { StartSpawnCoroutine(); } // Resume all active obstacles foreach (var obstacle in _activeObstacles.ToArray()) { if (obstacle != null) { FloatingObstacle obstacleComponent = obstacle.GetComponent(); if (obstacleComponent != null) { obstacleComponent.DoResume(); } } } Logging.Debug($"[ObstacleSpawner] Resumed with {_activeObstacles.Count} active obstacles"); } /// /// Starts the obstacle spawning coroutine if not already running /// private void StartSpawnCoroutine() { if (_spawnCoroutine == null && !_isPaused && !_isSurfacing) { _spawnCoroutine = StartCoroutine(SpawnObstacleRoutine()); } } /// /// Starts the obstacle movement coroutine if not already running /// private void StartMoveCoroutine() { if (_moveCoroutine == null && !_isPaused) { _moveCoroutine = StartCoroutine(MoveObstaclesRoutine()); } } /// /// Starts the obstacle despawning coroutine if not already running /// private void StartDespawnCoroutine() { if (_despawnCoroutine == null && !_isPaused) { _despawnCoroutine = StartCoroutine(DespawnObstaclesRoutine()); } } /// /// Validates that all prefabs have required components /// private void ValidateObstaclePrefabs() { if (_devSettings == null) return; for (int i = 0; i < obstaclePrefabs.Count; i++) { if (obstaclePrefabs[i] == null) continue; // Check if the prefab has a FloatingObstacle component if (obstaclePrefabs[i].GetComponent() == null) { Logging.Warning($"Obstacle prefab {obstaclePrefabs[i].name} does not have a FloatingObstacle component. Adding one automatically."); obstaclePrefabs[i].AddComponent(); } // Ensure the prefab is on the correct layer (using configurable obstacleLayer) if (obstaclePrefabs[i].layer != _devSettings.ObstacleLayer) { Logging.Warning($"Obstacle prefab {obstaclePrefabs[i].name} is not on the configured obstacle layer ({_devSettings.ObstacleLayer}). Setting layer automatically."); SetLayerRecursively(obstaclePrefabs[i], _devSettings.ObstacleLayer); } } } /// /// Sets the layer of a GameObject and all its children /// private void SetLayerRecursively(GameObject obj, int layer) { obj.layer = layer; foreach (Transform child in obj.transform) { SetLayerRecursively(child.gameObject, layer); } } /// /// Initialize the object pool system /// private void InitializeObjectPool() { GameObject poolGO = new GameObject("ObstaclePool"); poolGO.transform.SetParent(transform); _obstaclePool = poolGO.AddComponent(); // Set up pool configuration _obstaclePool.maxPerPrefabPoolSize = _devSettings.ObstacleMaxPerPrefabPoolSize; _obstaclePool.totalMaxPoolSize = _devSettings.ObstacleTotalMaxPoolSize; // Convert GameObject list to FloatingObstacle list List prefabObstacles = new List(obstaclePrefabs.Count); foreach (var prefab in obstaclePrefabs) { if (prefab != null) { FloatingObstacle obstacleComponent = prefab.GetComponent(); if (obstacleComponent != null) { prefabObstacles.Add(obstacleComponent); } else { Debug.LogError($"Obstacle prefab {prefab.name} is missing a FloatingObstacle component!"); } } } // Initialize the pool _obstaclePool.Initialize(prefabObstacles); } /// /// Calculate screen bounds in world space dynamically /// private void CalculateScreenBounds() { if (_mainCamera == null) { _mainCamera = UnityEngine.Camera.main; if (_mainCamera == null) { Debug.LogError("[ObstacleSpawner] No main camera found!"); return; } } // Calculate screen bottom (Y spawn position will be 2 units below this) Vector3 bottomWorldPoint = _mainCamera.ViewportToWorldPoint(new Vector3(0.5f, 0f, _mainCamera.nearClipPlane)); _screenBottom = bottomWorldPoint.y; // Calculate screen width in world units Vector3 leftWorldPoint = _mainCamera.ViewportToWorldPoint(new Vector3(0f, 0.5f, _mainCamera.nearClipPlane)); Vector3 rightWorldPoint = _mainCamera.ViewportToWorldPoint(new Vector3(1f, 0.5f, _mainCamera.nearClipPlane)); float screenWidth = rightWorldPoint.x - leftWorldPoint.x; // Calculate spawn range based on 80% of screen width (40% on each side from center) _spawnRangeX = (screenWidth * 0.8f) / 2f; Logging.Debug($"[ObstacleSpawner] Screen calculated - Width: {screenWidth:F2}, Bottom: {_screenBottom:F2}, Spawn Range X: ±{_spawnRangeX:F2}"); } /// /// Starts the obstacle spawning coroutine /// public void StartSpawning() { if (_spawnCoroutine == null) { _spawnCoroutine = StartCoroutine(SpawnObstaclesCoroutine()); Logging.Debug("[ObstacleSpawner] Started spawning obstacles"); } } /// /// Stops the obstacle spawning coroutine /// public void StopSpawning() { if (_spawnCoroutine != null) { StopCoroutine(_spawnCoroutine); _spawnCoroutine = null; Logging.Debug("[ObstacleSpawner] Stopped spawning obstacles"); } } /// /// Main spawning coroutine that runs continuously /// private IEnumerator SpawnObstaclesCoroutine() { while (true) { // Calculate next spawn time with variation float nextSpawnTime = _settings.ObstacleSpawnInterval + Random.Range(-_settings.ObstacleSpawnIntervalVariation, _settings.ObstacleSpawnIntervalVariation); nextSpawnTime = Mathf.Max(0.1f, nextSpawnTime); // Ensure minimum interval yield return new WaitForSeconds(nextSpawnTime); // Attempt to spawn an obstacle TrySpawnObstacle(); } } /// /// Attempts to spawn an obstacle at a valid position /// private void TrySpawnObstacle() { // Don't spawn new obstacles when surfacing if (_isSurfacing) { Logging.Debug("[ObstacleSpawner] Skipping obstacle spawn - currently surfacing"); return; } Logging.Debug($"[ObstacleSpawner] TrySpawnObstacle called at {Time.time:F2}"); if (obstaclePrefabs == null || obstaclePrefabs.Count == 0) { Logging.Warning("[ObstacleSpawner] No obstacle prefabs available for spawning!"); return; } Vector3 spawnPosition; bool foundValidPosition = false; // Try to find a valid spawn position for (int attempts = 0; attempts < _settings.ObstacleMaxSpawnAttempts; attempts++) { spawnPosition = GetRandomSpawnPosition(); if (IsValidSpawnPosition(spawnPosition)) { Logging.Debug($"[ObstacleSpawner] Found valid position at {spawnPosition} after {attempts + 1} attempts"); SpawnObstacleAt(spawnPosition); foundValidPosition = true; break; } else { Logging.Debug($"[ObstacleSpawner] Position {spawnPosition} invalid (attempt {attempts + 1}/{_settings.ObstacleMaxSpawnAttempts})"); } } if (!foundValidPosition) { Logging.Warning($"[ObstacleSpawner] SPAWN MISSED: Could not find valid spawn position after {_settings.ObstacleMaxSpawnAttempts} attempts at {Time.time:F2}"); } } /// /// Gets a random spawn position below the screen /// private Vector3 GetRandomSpawnPosition() { // Use dynamically calculated spawn range (80% of screen width) float randomX = Random.Range(-_spawnRangeX, _spawnRangeX); // Spawn 2 units below screen bottom float spawnY = _screenBottom - 2f; return new Vector3(randomX, spawnY, 0f); } /// /// Checks if a spawn position is valid (not colliding with tiles) /// private bool IsValidSpawnPosition(Vector3 position) { // Use OverlapCircle to check for collisions with tiles using just the layer // Convert the single layer to a layer mask inline (1 << layerNumber) Collider2D collision = Physics2D.OverlapCircle(position, _settings.ObstacleSpawnCollisionRadius, 1 << _devSettings.TrenchTileLayer); return collision == null; } /// /// Spawns an obstacle at the specified position /// private void SpawnObstacleAt(Vector3 position) { Logging.Debug($"[ObstacleSpawner] SpawnObstacleAt called for position {position}"); // Select random prefab int prefabIndex = Random.Range(0, obstaclePrefabs.Count); GameObject prefab = obstaclePrefabs[prefabIndex]; if (prefab == null) { Debug.LogError($"[ObstacleSpawner] SPAWN FAILED: Obstacle prefab at index {prefabIndex} is null!"); return; } GameObject obstacle; // Spawn using pool or instantiate directly if (_devSettings.ObstacleUseObjectPooling && _obstaclePool != null) { Logging.Debug($"[ObstacleSpawner] Requesting obstacle from pool (prefab index {prefabIndex})"); obstacle = _obstaclePool.GetObstacle(prefabIndex); if (obstacle == null) { Debug.LogError($"[ObstacleSpawner] SPAWN FAILED: Failed to get obstacle from pool for prefab index {prefabIndex}!"); return; } // Important: Set position/parent/rotation BEFORE activation to avoid visual glitches obstacle.transform.position = position; obstacle.transform.rotation = prefab.transform.rotation; obstacle.transform.SetParent(transform); Logging.Debug($"[ObstacleSpawner] Got obstacle {obstacle.name} from pool, active state: {obstacle.activeInHierarchy}"); // ENHANCED FORCE ACTIVATION - more robust approach if (!obstacle.activeInHierarchy) { Logging.Warning($"[ObstacleSpawner] Pool returned inactive object {obstacle.name}, force activating!"); // Configure obstacle BEFORE activation ConfigureObstacle(obstacle, prefabIndex); // Force activate the obstacle obstacle.SetActive(true); // Double-check activation status if (!obstacle.activeInHierarchy) { Debug.LogError($"[ObstacleSpawner] CRITICAL ERROR: Failed to activate {obstacle.name} after multiple attempts!"); // Last resort: try to instantiate a new one instead GameObject newObstacle = Instantiate(prefab, position, prefab.transform.rotation, transform); if (newObstacle != null) { obstacle = newObstacle; ConfigureObstacle(obstacle, prefabIndex); } } Logging.Debug($"[ObstacleSpawner] After force activation, {obstacle.name} active state: {obstacle.activeInHierarchy}"); } else { // Still configure if already active ConfigureObstacle(obstacle, prefabIndex); } } else { Logging.Debug($"[ObstacleSpawner] Instantiating new obstacle (pooling disabled)"); obstacle = Instantiate(prefab, position, prefab.transform.rotation, transform); // Configure the newly instantiated obstacle ConfigureObstacle(obstacle, prefabIndex); } // Assign unique name with counter _obstacleCounter++; string oldName = obstacle.name; obstacle.name = $"Obstacle{_obstacleCounter:D3}"; Logging.Debug($"[ObstacleSpawner] Renamed obstacle from '{oldName}' to '{obstacle.name}', active state: {obstacle.activeInHierarchy}"); // Track active obstacles _activeObstacles.Add(obstacle); // Invoke events onObstacleSpawned?.Invoke(obstacle); Logging.Debug($"[ObstacleSpawner] After events, obstacle {obstacle.name} active state: {obstacle.activeInHierarchy}"); Logging.Debug($"[ObstacleSpawner] Successfully spawned obstacle {obstacle.name} at {position}. Active count: {_activeObstacles.Count}, Final active state: {obstacle.activeInHierarchy}"); } /// /// Configures an obstacle with randomized properties /// private void ConfigureObstacle(GameObject obstacle, int prefabIndex) { FloatingObstacle obstacleComponent = obstacle.GetComponent(); if (obstacleComponent != null) { // Set prefab index obstacleComponent.PrefabIndex = prefabIndex; // Randomize properties using settings obstacleComponent.MoveSpeed = Random.Range( _settings.ObstacleMinMoveSpeed, _settings.ObstacleMaxMoveSpeed); // Set spawner reference obstacleComponent.SetSpawner(this); // If spawner is already paused, pause the obstacle immediately if (_isPaused) { obstacleComponent.Pause(); } } } /// /// Returns an obstacle to the pool (called by FloatingObstacle) /// public void ReturnObstacleToPool(GameObject obstacle, int prefabIndex) { if (obstacle == null) return; Logging.Debug($"[ObstacleSpawner] ReturnObstacleToPool called for {obstacle.name}, active state: {obstacle.activeInHierarchy}"); // Remove from active list _activeObstacles.Remove(obstacle); // Invoke events onObstacleDestroyed?.Invoke(obstacle); // Return to pool or destroy if (_devSettings.ObstacleUseObjectPooling && _obstaclePool != null) { Logging.Debug($"[ObstacleSpawner] Returning {obstacle.name} to pool"); _obstaclePool.ReturnObstacle(obstacle, prefabIndex); } else { Logging.Debug($"[ObstacleSpawner] Destroying {obstacle.name} (pooling disabled)"); Destroy(obstacle); } } /// /// Public method to change spawn interval at runtime /// public void SetSpawnInterval(float interval) { // This method can no longer directly modify the settings // Consider implementing a runtime settings override system if needed Logging.Warning("[ObstacleSpawner] SetSpawnInterval no longer modifies settings directly. Settings are now centralized."); } /// /// Public method to set speed range at runtime /// public void SetSpeedRange(float min, float max) { // This method can no longer directly modify the settings // Consider implementing a runtime settings override system if needed Logging.Warning("[ObstacleSpawner] SetSpeedRange no longer modifies settings directly. Settings are now centralized."); } /// /// Public method to recalculate screen bounds (useful if camera changes) /// public void RecalculateScreenBounds() { CalculateScreenBounds(); } /// /// Called when the velocity factor changes from the DivingGameManager /// public void OnVelocityFactorChanged(float velocityFactor) { _velocityFactor = velocityFactor; // Update all active obstacles with the new velocity factor foreach (GameObject obstacle in _activeObstacles) { if (obstacle != null) { FloatingObstacle obstacleComponent = obstacle.GetComponent(); if (obstacleComponent != null) { obstacleComponent.OnVelocityFactorChanged(velocityFactor); } } } Logging.Debug($"[ObstacleSpawner] Velocity factor updated to {_velocityFactor:F2}, propagated to {_activeObstacles.Count} active obstacles"); } /// /// Start surfacing mode - reverse direction of existing obstacles and stop spawning new ones /// public void StartSurfacing() { if (_isSurfacing) return; // Already surfacing _isSurfacing = true; // Notify obstacles about surfacing state (for direction-based logic) foreach (GameObject obstacle in _activeObstacles) { if (obstacle != null) { FloatingObstacle obstacleComponent = obstacle.GetComponent(); if (obstacleComponent != null) { // Call StartSurfacing on the obstacle component itself obstacleComponent.StartSurfacing(); } } } Logging.Debug($"[ObstacleSpawner] Started surfacing mode for {_activeObstacles.Count} active obstacles"); } /// /// Gets the count of currently active obstacles /// public int ActiveObstacleCount => _activeObstacles.Count; /// /// Coroutine that handles obstacle spawning at regular intervals /// private IEnumerator SpawnObstacleRoutine() { Logging.Debug("[ObstacleSpawner] Started spawning coroutine"); while (enabled && gameObject.activeInHierarchy && !_isPaused && !_isSurfacing) { // Calculate next spawn time with variation float nextSpawnTime = _settings.ObstacleSpawnInterval + Random.Range(-_settings.ObstacleSpawnIntervalVariation, _settings.ObstacleSpawnIntervalVariation); nextSpawnTime = Mathf.Max(0.1f, nextSpawnTime); // Ensure minimum interval // Attempt to spawn an obstacle TrySpawnObstacle(); yield return new WaitForSeconds(nextSpawnTime); } // Clear coroutine reference when stopped _spawnCoroutine = null; Logging.Debug("[ObstacleSpawner] Spawning coroutine ended"); } /// /// Coroutine that handles checking obstacle positions /// Unlike the previous implementation, we don't need to move obstacles manually /// since the FloatingObstacle handles its own movement via coroutines /// private IEnumerator MoveObstaclesRoutine() { Logging.Debug("[ObstacleSpawner] Started obstacle monitoring coroutine"); // This coroutine now just monitors obstacles, not moves them while (enabled && gameObject.activeInHierarchy && !_isPaused) { // Clean up any null references in the active obstacles list _activeObstacles.RemoveAll(obstacle => obstacle == null); yield return new WaitForSeconds(0.5f); } // Clear coroutine reference when stopped _moveCoroutine = null; Logging.Debug("[ObstacleSpawner] Obstacle monitoring coroutine ended"); } /// /// Coroutine that checks for obstacles that are off-screen and should be despawned /// private IEnumerator DespawnObstaclesRoutine() { const float checkInterval = 0.5f; // Check every half second Logging.Debug("[ObstacleSpawner] Started despawn coroutine with interval: " + checkInterval); while (enabled && gameObject.activeInHierarchy && !_isPaused) { // Calculate screen bounds for despawning float despawnBuffer = 2f; // Extra buffer beyond screen edges if (_mainCamera == null) { _mainCamera = UnityEngine.Camera.main; if (_mainCamera == null) { yield return new WaitForSeconds(checkInterval); continue; } } Vector3 topWorldPoint = _mainCamera.ViewportToWorldPoint(new Vector3(0.5f, 1f, _mainCamera.transform.position.z)); float _screenTop = topWorldPoint.y; Vector3 bottomWorldPoint = _mainCamera.ViewportToWorldPoint(new Vector3(0.5f, 0f, _mainCamera.transform.position.z)); float _screenBottom = bottomWorldPoint.y; float topEdge = _screenTop + despawnBuffer; float bottomEdge = _screenBottom - despawnBuffer; // Find obstacles that need to be despawned List obstaclesToRemove = new List(); foreach (var obstacle in _activeObstacles) { if (obstacle == null) { obstaclesToRemove.Add(obstacle); continue; } // Check if obstacle is out of screen bounds float obstacleY = obstacle.transform.position.y; bool shouldDespawn; if (_isSurfacing) { // When surfacing, despawn obstacles below the bottom edge shouldDespawn = obstacleY < bottomEdge; } else { // When descending, despawn obstacles above the top edge shouldDespawn = obstacleY > topEdge; } if (shouldDespawn) { obstaclesToRemove.Add(obstacle); } } // Remove and despawn obstacles foreach (var obstacle in obstaclesToRemove) { FloatingObstacle obstacleComponent = obstacle?.GetComponent(); if (obstacleComponent != null) { // Instead of calling a non-existent DespawnObstacle method, // use FloatingObstacle's ForceReturnToPool method obstacleComponent.ForceReturnToPool(); } else { // Fallback if component not found ReturnObstacleToPool(obstacle, -1); } } yield return new WaitForSeconds(checkInterval); } // Clear coroutine reference when stopped _despawnCoroutine = null; Logging.Debug("[ObstacleSpawner] Despawn coroutine ended"); } #if UNITY_EDITOR private void OnDrawGizmosSelected() { // Only draw if screen bounds have been calculated and settings are available if (_spawnRangeX > 0f && _settings != null) { // Draw spawn area using dynamic calculations Gizmos.color = Color.yellow; Vector3 center = new Vector3(0f, _screenBottom - 2f, 0f); Vector3 size = new Vector3(_spawnRangeX * 2f, 1f, 1f); Gizmos.DrawWireCube(center, size); // Draw collision radius at spawn point Gizmos.color = Color.red; Gizmos.DrawWireSphere(center, _settings.ObstacleSpawnCollisionRadius); } } #endif } }