2025-09-21 07:32:56 +00:00
using System.Collections ;
using System.Collections.Generic ;
using UnityEngine ;
using UnityEngine.Events ;
using Pooling ;
2025-09-24 12:21:21 +02:00
using AppleHills.Core.Settings ;
2025-09-21 07:32:56 +00:00
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("Events")]
2025-09-24 12:21:21 +02:00
[Tooltip("Invoked when a new obstacle is spawned")]
2025-09-21 07:32:56 +00:00
public UnityEvent < GameObject > onObstacleSpawned ;
2025-09-24 12:21:21 +02:00
[Tooltip("Invoked when an obstacle is destroyed or returned to pool")]
2025-09-21 07:32:56 +00:00
public UnityEvent < GameObject > onObstacleDestroyed ;
2025-09-24 12:21:21 +02:00
// Settings references
private IDivingMinigameSettings _settings ;
private DivingDeveloperSettings _devSettings ;
2025-09-21 07:32:56 +00:00
// 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 12:16:32 +00:00
private bool _isSurfacing = false ; // Flag to track surfacing state
private float _velocityFactor = 1.0f ; // Current velocity factor from the game manager
2025-09-21 07:32:56 +00:00
private void Awake ( )
{
_mainCamera = Camera . main ;
2025-09-24 12:21:21 +02:00
// Get settings from GameManager
_settings = GameManager . GetSettingsObject < IDivingMinigameSettings > ( ) ;
_devSettings = GameManager . GetDeveloperSettings < DivingDeveloperSettings > ( ) ;
if ( _settings = = null )
{
Debug . LogError ( "[ObstacleSpawner] Failed to load diving minigame settings!" ) ;
}
if ( _devSettings = = null )
{
Debug . LogError ( "[ObstacleSpawner] Failed to load diving developer settings!" ) ;
}
2025-09-21 07:32:56 +00:00
// Validate obstacle prefabs
ValidateObstaclePrefabs ( ) ;
2025-09-24 12:21:21 +02:00
if ( _devSettings ? . ObstacleUseObjectPooling ? ? false )
2025-09-21 07:32:56 +00:00
{
InitializeObjectPool ( ) ;
}
2025-09-24 12:21:21 +02:00
// Initialize events if null
if ( onObstacleSpawned = = null )
onObstacleSpawned = new UnityEvent < GameObject > ( ) ;
if ( onObstacleDestroyed = = null )
onObstacleDestroyed = new UnityEvent < GameObject > ( ) ;
2025-09-21 07:32:56 +00:00
}
private void Start ( )
{
CalculateScreenBounds ( ) ;
StartSpawning ( ) ;
}
private void OnDestroy ( )
{
StopSpawning ( ) ;
}
/// <summary>
/// Validates that all prefabs have required components
/// </summary>
private void ValidateObstaclePrefabs ( )
{
2025-09-24 12:21:21 +02:00
if ( _devSettings = = null ) return ;
2025-09-21 07:32:56 +00:00
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)
2025-09-24 12:21:21 +02:00
if ( obstaclePrefabs [ i ] . layer ! = _devSettings . ObstacleLayer )
2025-09-21 07:32:56 +00:00
{
2025-09-24 12:21:21 +02:00
Debug . LogWarning ( $"Obstacle prefab {obstaclePrefabs[i].name} is not on the configured obstacle layer ({_devSettings.ObstacleLayer}). Setting layer automatically." ) ;
SetLayerRecursively ( obstaclePrefabs [ i ] , _devSettings . ObstacleLayer ) ;
2025-09-21 07:32:56 +00:00
}
}
}
/// <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
2025-09-24 12:21:21 +02:00
_obstaclePool . maxPerPrefabPoolSize = _devSettings . ObstacleMaxPerPrefabPoolSize ;
_obstaclePool . totalMaxPoolSize = _devSettings . ObstacleTotalMaxPoolSize ;
2025-09-21 07:32:56 +00:00
// 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
2025-09-24 12:21:21 +02:00
float nextSpawnTime = _settings . EndlessDescenderObstacleSpawnInterval +
Random . Range ( - _settings . EndlessDescenderObstacleSpawnIntervalVariation ,
_settings . EndlessDescenderObstacleSpawnIntervalVariation ) ;
2025-09-21 07:32:56 +00:00
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 12:16:32 +00: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
2025-09-24 12:21:21 +02:00
for ( int attempts = 0 ; attempts < _settings . EndlessDescenderObstacleMaxSpawnAttempts ; attempts + + )
2025-09-21 07:32:56 +00:00
{
spawnPosition = GetRandomSpawnPosition ( ) ;
if ( IsValidSpawnPosition ( spawnPosition ) )
{
Debug . Log ( $"[ObstacleSpawner] Found valid position at {spawnPosition} after {attempts + 1} attempts" ) ;
SpawnObstacleAt ( spawnPosition ) ;
foundValidPosition = true ;
break ;
}
else
{
2025-09-24 12:21:21 +02:00
Debug . Log ( $"[ObstacleSpawner] Position {spawnPosition} invalid (attempt {attempts + 1}/{_settings.EndlessDescenderObstacleMaxSpawnAttempts})" ) ;
2025-09-21 07:32:56 +00:00
}
}
if ( ! foundValidPosition )
{
2025-09-24 12:21:21 +02:00
Debug . LogWarning ( $"[ObstacleSpawner] SPAWN MISSED: Could not find valid spawn position after {_settings.EndlessDescenderObstacleMaxSpawnAttempts} attempts at {Time.time:F2}" ) ;
2025-09-21 07:32:56 +00:00
}
}
/// <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
2025-09-24 12:21:21 +02:00
Collider2D collision = Physics2D . OverlapCircle ( position , _settings . EndlessDescenderObstacleSpawnCollisionRadius , _devSettings . ObstacleTileLayerMask ) ;
2025-09-21 07:32:56 +00:00
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
2025-09-24 12:21:21 +02:00
if ( _devSettings . ObstacleUseObjectPooling & & _obstaclePool ! = null )
2025-09-21 07:32:56 +00:00
{
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 12:16:32 +00: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 12:16:32 +00: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 12:16:32 +00: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 12:16:32 +00: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 12:16:32 +00: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 12:16:32 +00: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 ;
2025-09-24 12:21:21 +02:00
// Randomize properties using settings
obstacleComponent . MoveSpeed = Random . Range (
_settings . EndlessDescenderObstacleMinMoveSpeed ,
_settings . EndlessDescenderObstacleMaxMoveSpeed ) ;
2025-09-21 07:32:56 +00:00
// 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
2025-09-24 12:21:21 +02:00
if ( _devSettings . ObstacleUseObjectPooling & & _obstaclePool ! = null )
2025-09-21 07:32:56 +00:00
{
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 )
{
2025-09-24 12:21:21 +02:00
// 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." ) ;
2025-09-21 07:32:56 +00:00
}
/// <summary>
/// Public method to set speed range at runtime
/// </summary>
public void SetSpeedRange ( float min , float max )
{
2025-09-24 12:21:21 +02:00
// 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." ) ;
2025-09-21 07:32:56 +00:00
}
/// <summary>
/// Public method to recalculate screen bounds (useful if camera changes)
/// </summary>
public void RecalculateScreenBounds ( )
{
CalculateScreenBounds ( ) ;
}
2025-09-22 12:16:32 +00:00
/// <summary>
/// Called when the velocity factor changes from the DivingGameManager
/// </summary>
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 < FloatingObstacle > ( ) ;
if ( obstacleComponent ! = null )
{
obstacleComponent . OnVelocityFactorChanged ( velocityFactor ) ;
}
}
}
Debug . Log ( $"[ObstacleSpawner] Velocity factor updated to {_velocityFactor:F2}, propagated to {_activeObstacles.Count} active obstacles" ) ;
}
/// <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 ;
// Notify obstacles about surfacing state (for direction-based logic)
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 for {_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 ( )
{
2025-09-24 12:21:21 +02:00
// Only draw if screen bounds have been calculated and settings are available
if ( _spawnRangeX > 0f & & _settings ! = null )
2025-09-21 07:32:56 +00:00
{
// 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 ;
2025-09-24 12:21:21 +02:00
Gizmos . DrawWireSphere ( center , _settings . EndlessDescenderObstacleSpawnCollisionRadius ) ;
2025-09-21 07:32:56 +00:00
}
}
#endif
}
}