2025-09-21 07:32:56 +00:00
using System.Collections ;
using System.Collections.Generic ;
using UnityEngine ;
using UnityEngine.Events ;
using Pooling ;
namespace Minigames.DivingForPictures
{
/// <summary>
/// Spawns and manages mobile obstacles for the diving minigame.
/// Uses object pooling and validates spawn positions to avoid colliding with tiles.
/// </summary>
public class ObstacleSpawner : MonoBehaviour
{
[Header("Obstacle Prefabs")]
[Tooltip("List of possible obstacle prefabs to spawn")]
[SerializeField] private List < GameObject > 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 < GameObject > onObstacleSpawned ;
[Tooltip("Called when an obstacle is returned to pool")]
public UnityEvent < GameObject > onObstacleDestroyed ;
// Private fields
private ObstaclePool _obstaclePool ;
private Camera _mainCamera ;
private float _screenBottom ;
private float _spawnRangeX ;
private Coroutine _spawnCoroutine ;
private readonly List < GameObject > _activeObstacles = new List < GameObject > ( ) ;
private int _obstacleCounter = 0 ; // Counter for unique obstacle naming
2025-09-22 11:40:01 +02:00
private bool _isSurfacing = false ; // Flag to track surfacing state
2025-09-21 07:32:56 +00:00
private void Awake ( )
{
_mainCamera = Camera . main ;
// Validate obstacle prefabs
ValidateObstaclePrefabs ( ) ;
if ( useObjectPooling )
{
InitializeObjectPool ( ) ;
}
}
private void Start ( )
{
CalculateScreenBounds ( ) ;
StartSpawning ( ) ;
}
private void OnDestroy ( )
{
StopSpawning ( ) ;
}
/// <summary>
/// Validates that all prefabs have required components
/// </summary>
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 < FloatingObstacle > ( ) = = null )
{
Debug . LogWarning ( $"Obstacle prefab {obstaclePrefabs[i].name} does not have a FloatingObstacle component. Adding one automatically." ) ;
obstaclePrefabs [ i ] . AddComponent < FloatingObstacle > ( ) ;
}
// 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 ) ;
}
}
}
/// <summary>
/// Sets the layer of a GameObject and all its children
/// </summary>
private void SetLayerRecursively ( GameObject obj , int layer )
{
obj . layer = layer ;
foreach ( Transform child in obj . transform )
{
SetLayerRecursively ( child . gameObject , layer ) ;
}
}
/// <summary>
/// Initialize the object pool system
/// </summary>
private void InitializeObjectPool ( )
{
GameObject poolGO = new GameObject ( "ObstaclePool" ) ;
poolGO . transform . SetParent ( transform ) ;
_obstaclePool = poolGO . AddComponent < ObstaclePool > ( ) ;
// Set up pool configuration
_obstaclePool . maxPerPrefabPoolSize = maxPerPrefabPoolSize ;
_obstaclePool . totalMaxPoolSize = totalMaxPoolSize ;
// Convert GameObject list to FloatingObstacle list
List < FloatingObstacle > prefabObstacles = new List < FloatingObstacle > ( obstaclePrefabs . Count ) ;
foreach ( var prefab in obstaclePrefabs )
{
if ( prefab ! = null )
{
FloatingObstacle obstacleComponent = prefab . GetComponent < FloatingObstacle > ( ) ;
if ( obstacleComponent ! = null )
{
prefabObstacles . Add ( obstacleComponent ) ;
}
else
{
Debug . LogError ( $"Obstacle prefab {prefab.name} is missing a FloatingObstacle component!" ) ;
}
}
}
// Initialize the pool
_obstaclePool . Initialize ( prefabObstacles ) ;
}
/// <summary>
/// Calculate screen bounds in world space dynamically
/// </summary>
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}" ) ;
}
/// <summary>
/// Starts the obstacle spawning coroutine
/// </summary>
public void StartSpawning ( )
{
if ( _spawnCoroutine = = null )
{
_spawnCoroutine = StartCoroutine ( SpawnObstaclesCoroutine ( ) ) ;
Debug . Log ( "[ObstacleSpawner] Started spawning obstacles" ) ;
}
}
/// <summary>
/// Stops the obstacle spawning coroutine
/// </summary>
public void StopSpawning ( )
{
if ( _spawnCoroutine ! = null )
{
StopCoroutine ( _spawnCoroutine ) ;
_spawnCoroutine = null ;
Debug . Log ( "[ObstacleSpawner] Stopped spawning obstacles" ) ;
}
}
/// <summary>
/// Main spawning coroutine that runs continuously
/// </summary>
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 ( ) ;
}
}
/// <summary>
/// Attempts to spawn an obstacle at a valid position
/// </summary>
private void TrySpawnObstacle ( )
{
2025-09-22 11:40:01 +02:00
// Don't spawn new obstacles when surfacing
if ( _isSurfacing )
{
Debug . Log ( "[ObstacleSpawner] Skipping obstacle spawn - currently surfacing" ) ;
return ;
}
2025-09-21 07:32:56 +00:00
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}" ) ;
}
}
/// <summary>
/// Gets a random spawn position below the screen
/// </summary>
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 ) ;
}
/// <summary>
/// Checks if a spawn position is valid (not colliding with tiles)
/// </summary>
private bool IsValidSpawnPosition ( Vector3 position )
{
// Use OverlapCircle to check for collisions with tiles
Collider2D collision = Physics2D . OverlapCircle ( position , spawnCollisionRadius , tileLayerMask ) ;
return collision = = null ;
}
/// <summary>
/// Spawns an obstacle at the specified position
/// </summary>
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 ;
}
2025-09-22 11:40:01 +02:00
// 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 ) ;
2025-09-21 07:32:56 +00:00
Debug . Log ( $"[ObstacleSpawner] Got obstacle {obstacle.name} from pool, active state: {obstacle.activeInHierarchy}" ) ;
2025-09-22 11:40:01 +02:00
// ENHANCED FORCE ACTIVATION - more robust approach
2025-09-21 07:32:56 +00:00
if ( ! obstacle . activeInHierarchy )
{
Debug . LogWarning ( $"[ObstacleSpawner] Pool returned inactive object {obstacle.name}, force activating!" ) ;
2025-09-22 11:40:01 +02:00
// Configure obstacle BEFORE activation
ConfigureObstacle ( obstacle , prefabIndex ) ;
// Force activate the obstacle
2025-09-21 07:32:56 +00:00
obstacle . SetActive ( true ) ;
2025-09-22 11:40:01 +02:00
// 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 ) ;
}
}
2025-09-21 07:32:56 +00:00
Debug . Log ( $"[ObstacleSpawner] After force activation, {obstacle.name} active state: {obstacle.activeInHierarchy}" ) ;
}
2025-09-22 11:40:01 +02:00
else
{
// Still configure if already active
ConfigureObstacle ( obstacle , prefabIndex ) ;
}
2025-09-21 07:32:56 +00:00
}
else
{
Debug . Log ( $"[ObstacleSpawner] Instantiating new obstacle (pooling disabled)" ) ;
obstacle = Instantiate ( prefab , position , prefab . transform . rotation , transform ) ;
2025-09-22 11:40:01 +02:00
// Configure the newly instantiated obstacle
ConfigureObstacle ( obstacle , prefabIndex ) ;
2025-09-21 07:32:56 +00:00
}
// 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}" ) ;
}
/// <summary>
/// Configures an obstacle with randomized properties
/// </summary>
private void ConfigureObstacle ( GameObject obstacle , int prefabIndex )
{
FloatingObstacle obstacleComponent = obstacle . GetComponent < FloatingObstacle > ( ) ;
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 ) ;
}
}
/// <summary>
/// Returns an obstacle to the pool (called by FloatingObstacle)
/// </summary>
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 ) ;
}
}
/// <summary>
/// Public method to change spawn interval at runtime
/// </summary>
public void SetSpawnInterval ( float interval )
{
spawnInterval = interval ;
}
/// <summary>
/// Public method to change spawn range at runtime
/// </summary>
public void SetSpawnRange ( float range )
{
spawnRangeX = range ;
}
/// <summary>
/// Public method to set speed range at runtime
/// </summary>
public void SetSpeedRange ( float min , float max )
{
minMoveSpeed = min ;
maxMoveSpeed = max ;
}
/// <summary>
/// Public method to recalculate screen bounds (useful if camera changes)
/// </summary>
public void RecalculateScreenBounds ( )
{
CalculateScreenBounds ( ) ;
}
2025-09-22 11:40:01 +02:00
/// <summary>
/// Start surfacing mode - reverse direction of existing obstacles and stop spawning new ones
/// </summary>
public void StartSurfacing ( )
{
if ( _isSurfacing ) return ; // Already surfacing
_isSurfacing = true ;
// Reverse direction of all existing obstacles
foreach ( GameObject obstacle in _activeObstacles )
{
if ( obstacle ! = null )
{
FloatingObstacle obstacleComponent = obstacle . GetComponent < FloatingObstacle > ( ) ;
if ( obstacleComponent ! = null )
{
// Call StartSurfacing on the obstacle component itself
obstacleComponent . StartSurfacing ( ) ;
}
}
}
Debug . Log ( $"[ObstacleSpawner] Started surfacing mode. Reversed direction of {_activeObstacles.Count} active obstacles." ) ;
}
2025-09-21 07:32:56 +00:00
/// <summary>
/// Gets the count of currently active obstacles
/// </summary>
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
}
}