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
}
}