using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Events; using Pooling; 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("Spawn Settings")] [Tooltip("Time interval between spawn attempts (in seconds)")] [SerializeField] private float spawnInterval = 2f; [Tooltip("Random variation in spawn timing (+/- seconds)")] [SerializeField] private float spawnIntervalVariation = 0.5f; [Tooltip("Maximum number of spawn position attempts before skipping")] [SerializeField] private int maxSpawnAttempts = 10; [Tooltip("Radius around spawn point to check for tile collisions")] [SerializeField] private float spawnCollisionRadius = 1f; [Header("Spawn Position")] [Tooltip("How far below screen to spawn obstacles")] [SerializeField] private float spawnDistanceBelowScreen = 2f; [Tooltip("Horizontal spawn range (distance from center)")] [SerializeField] private float spawnRangeX = 8f; [Header("Obstacle Properties Randomization")] [Tooltip("Minimum movement speed for spawned obstacles")] [SerializeField] private float minMoveSpeed = 1f; [Tooltip("Maximum movement speed for spawned obstacles")] [SerializeField] private float maxMoveSpeed = 4f; [Header("Object Pooling")] [Tooltip("Whether to use object pooling for obstacles")] [SerializeField] private bool useObjectPooling = true; [Tooltip("Maximum objects per prefab type in pool")] [SerializeField] private int maxPerPrefabPoolSize = 3; [Tooltip("Total maximum pool size across all prefab types")] [SerializeField] private int totalMaxPoolSize = 15; [Header("Layer Settings")] [Tooltip("Layer mask for tile collision detection during spawn position validation")] [SerializeField] private LayerMask tileLayerMask = -1; // Let user configure which layers to avoid [Tooltip("Target layer for spawned obstacles - obstacles will be placed on this layer")] [SerializeField] private int obstacleLayer = 11; // Default to layer 11, but configurable [Header("Events")] [Tooltip("Called when an obstacle is spawned")] public UnityEvent onObstacleSpawned; [Tooltip("Called when an obstacle is returned to pool")] public UnityEvent onObstacleDestroyed; // 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 void Awake() { _mainCamera = Camera.main; // Validate obstacle prefabs ValidateObstaclePrefabs(); if (useObjectPooling) { InitializeObjectPool(); } } private void Start() { CalculateScreenBounds(); StartSpawning(); } private void OnDestroy() { StopSpawning(); } /// /// Validates that all prefabs have required components /// private void ValidateObstaclePrefabs() { 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 != obstacleLayer) { Debug.LogWarning($"Obstacle prefab {obstaclePrefabs[i].name} is not on the configured obstacle layer ({obstacleLayer}). Setting layer automatically."); SetLayerRecursively(obstaclePrefabs[i], 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 = maxPerPrefabPoolSize; _obstaclePool.totalMaxPoolSize = totalMaxPoolSize; // 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 = spawnInterval + Random.Range(-spawnIntervalVariation, spawnIntervalVariation); 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() { 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 < maxSpawnAttempts; 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}/{maxSpawnAttempts})"); } } if (!foundValidPosition) { Debug.LogWarning($"[ObstacleSpawner] SPAWN MISSED: Could not find valid spawn position after {maxSpawnAttempts} 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 Collider2D collision = Physics2D.OverlapCircle(position, spawnCollisionRadius, tileLayerMask); 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 (useObjectPooling && _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; } Debug.Log($"[ObstacleSpawner] Got obstacle {obstacle.name} from pool, active state: {obstacle.activeInHierarchy}"); // FORCE ACTIVATION - bypass pool issues if (!obstacle.activeInHierarchy) { Debug.LogWarning($"[ObstacleSpawner] Pool returned inactive object {obstacle.name}, force activating!"); obstacle.SetActive(true); Debug.Log($"[ObstacleSpawner] After force activation, {obstacle.name} active state: {obstacle.activeInHierarchy}"); } obstacle.transform.position = position; obstacle.transform.rotation = prefab.transform.rotation; obstacle.transform.SetParent(transform); Debug.Log($"[ObstacleSpawner] After positioning, obstacle {obstacle.name} active state: {obstacle.activeInHierarchy}"); } else { Debug.Log($"[ObstacleSpawner] Instantiating new obstacle (pooling disabled)"); obstacle = Instantiate(prefab, position, prefab.transform.rotation, transform); } // 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}"); // Configure the obstacle ConfigureObstacle(obstacle, prefabIndex); Debug.Log($"[ObstacleSpawner] After configuration, obstacle {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 obstacleComponent.MoveSpeed = Random.Range(minMoveSpeed, maxMoveSpeed); // 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 (useObjectPooling && _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) { spawnInterval = interval; } /// /// Public method to change spawn range at runtime /// public void SetSpawnRange(float range) { spawnRangeX = range; } /// /// Public method to set speed range at runtime /// public void SetSpeedRange(float min, float max) { minMoveSpeed = min; maxMoveSpeed = max; } /// /// Public method to recalculate screen bounds (useful if camera changes) /// public void RecalculateScreenBounds() { CalculateScreenBounds(); } /// /// 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 if (_spawnRangeX > 0f) { // 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, spawnCollisionRadius); } } #endif } }