using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Events; using Pooling; using AppleHills.Core.Settings; 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 { [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 Camera _mainCamera; private float _screenBottom; private float _spawnRangeX; private Coroutine _spawnCoroutine; 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 private void Awake() { _mainCamera = 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(); } private void Start() { // Find DivingGameManager and subscribe to its initialization event DivingGameManager gameManager = FindFirstObjectByType(); if (gameManager != null) { gameManager.OnGameInitialized += Initialize; // If game is already initialized, initialize immediately if (gameManager.GetType().GetField("_isGameInitialized", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?.GetValue(gameManager) is bool isInitialized && isInitialized) { Initialize(); } } else { Debug.LogWarning("[ObstacleSpawner] DivingGameManager not found. Initializing immediately."); Initialize(); } } /// /// Initializes the obstacle spawner when triggered by DivingGameManager /// private void Initialize() { CalculateScreenBounds(); StartSpawning(); Debug.Log("[ObstacleSpawner] Initialized"); } private void OnDestroy() { StopSpawning(); } /// /// 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) { Debug.LogWarning($"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) { Debug.LogWarning($"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 = 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; Debug.Log($"[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()); Debug.Log("[ObstacleSpawner] Started spawning obstacles"); } } /// /// Stops the obstacle spawning coroutine /// public void StopSpawning() { if (_spawnCoroutine != null) { StopCoroutine(_spawnCoroutine); _spawnCoroutine = null; Debug.Log("[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) { Debug.Log("[ObstacleSpawner] Skipping obstacle spawn - currently surfacing"); return; } Debug.Log($"[ObstacleSpawner] TrySpawnObstacle called at {Time.time:F2}"); if (obstaclePrefabs == null || obstaclePrefabs.Count == 0) { Debug.LogWarning("[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)) { Debug.Log($"[ObstacleSpawner] Found valid position at {spawnPosition} after {attempts + 1} attempts"); SpawnObstacleAt(spawnPosition); foundValidPosition = true; break; } else { Debug.Log($"[ObstacleSpawner] Position {spawnPosition} invalid (attempt {attempts + 1}/{_settings.ObstacleMaxSpawnAttempts})"); } } if (!foundValidPosition) { Debug.LogWarning($"[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) { Debug.Log($"[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) { Debug.Log($"[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); Debug.Log($"[ObstacleSpawner] Got obstacle {obstacle.name} from pool, active state: {obstacle.activeInHierarchy}"); // ENHANCED FORCE ACTIVATION - more robust approach if (!obstacle.activeInHierarchy) { Debug.LogWarning($"[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); } } Debug.Log($"[ObstacleSpawner] After force activation, {obstacle.name} active state: {obstacle.activeInHierarchy}"); } else { // Still configure if already active ConfigureObstacle(obstacle, prefabIndex); } } else { Debug.Log($"[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}"; Debug.Log($"[ObstacleSpawner] Renamed obstacle from '{oldName}' to '{obstacle.name}', active state: {obstacle.activeInHierarchy}"); // Track active obstacles _activeObstacles.Add(obstacle); // Invoke events onObstacleSpawned?.Invoke(obstacle); Debug.Log($"[ObstacleSpawner] After events, obstacle {obstacle.name} active state: {obstacle.activeInHierarchy}"); Debug.Log($"[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 (since FloatingObstacle has this built-in now) obstacleComponent.SetSpawner(this); } } /// /// Returns an obstacle to the pool (called by FloatingObstacle) /// public void ReturnObstacleToPool(GameObject obstacle, int prefabIndex) { if (obstacle == null) return; Debug.Log($"[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) { Debug.Log($"[ObstacleSpawner] Returning {obstacle.name} to pool"); _obstaclePool.ReturnObstacle(obstacle, prefabIndex); } else { Debug.Log($"[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 Debug.LogWarning("[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 Debug.LogWarning("[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); } } } Debug.Log($"[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(); } } } Debug.Log($"[ObstacleSpawner] Started surfacing mode for {_activeObstacles.Count} active obstacles"); } /// /// Gets the count of currently active obstacles /// public int ActiveObstacleCount => _activeObstacles.Count; #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 } }