using UnityEngine; using System.Collections; using AppleHills.Core.Settings; using Pooling; using Utils; using AppleHills.Core.Interfaces; using Core; namespace Minigames.DivingForPictures { /// /// Complete floating obstacle component that handles movement and pooling. /// Obstacles move upward toward the surface. Collision detection is handled by the player. /// Once an obstacle hits the player, its collider is disabled to prevent further collisions. /// Uses coroutines for better performance instead of Update() calls. /// public class FloatingObstacle : MonoBehaviour, IPoolable, IPausable { [Header("Obstacle Properties")] [Tooltip("Index of the prefab this obstacle was created from")] [SerializeField] private int prefabIndex; [Tooltip("Movement speed of this obstacle (will be overridden by normalized settings)")] [SerializeField] private float moveSpeed = 2f; [Header("Movement")] [Tooltip("Whether this obstacle moves (can be disabled for static obstacles)")] [SerializeField] private bool enableMovement = true; [Header("References")] [Tooltip("Reference to the spawner that created this obstacle")] [SerializeField] private ObstacleSpawner spawner; // Public properties public int PrefabIndex { get => prefabIndex; set => prefabIndex = value; } public float MoveSpeed { get => moveSpeed; set => moveSpeed = value; } // Private fields private Collider2D _collider; private UnityEngine.Camera _mainCamera; private float _screenTop; private float _screenBottom; // Added to track bottom of screen private Coroutine _movementCoroutine; private Coroutine _offScreenCheckCoroutine; private bool _isSurfacing = false; // Flag to track surfacing state private float _velocityFactor = 1.0f; // Current velocity factor from game manager private float _baseMoveSpeed; // Original move speed before velocity factor is applied // Screen normalization private float _screenNormalizationFactor = 1.0f; private IDivingMinigameSettings _settings; // Pause state private bool _isPaused = false; // IPausable implementation public bool IsPaused => _isPaused; private void Awake() { _collider = GetComponent(); if (_collider == null) { _collider = GetComponentInChildren(); } if (_collider == null) { Debug.LogError($"[FloatingObstacle] No Collider2D found on {gameObject.name}!"); } _mainCamera = UnityEngine.Camera.main; // Get settings _settings = GameManager.GetSettingsObject(); if (_settings == null) { Logging.Warning("[FloatingObstacle] Could not retrieve settings, using default values"); _baseMoveSpeed = moveSpeed; // Use the serialized value as fallback } else { // Initialize with the obstacle-specific normalized speed settings float minSpeed = _settings.ObstacleMinMoveSpeed; float maxSpeed = _settings.ObstacleMaxMoveSpeed; // For variety, randomly assign a speed between min and max _baseMoveSpeed = Random.Range(minSpeed, maxSpeed); Logging.Debug($"[FloatingObstacle] Initialized with normalized speed: {_baseMoveSpeed} (range: {minSpeed}-{maxSpeed})"); } // Calculate screen normalization factor CalculateScreenNormalizationFactor(); } /// /// Calculates the screen normalization factor based on current screen height /// private void CalculateScreenNormalizationFactor() { // Get reference height from settings with fallback if not available float referenceHeight = 1080f; // Default fallback value if (_settings != null) { referenceHeight = _settings.ReferenceScreenHeight; } // Calculate normalization factor based on screen height _screenNormalizationFactor = Screen.height / referenceHeight; Logging.Debug($"[FloatingObstacle] Screen normalization factor: {_screenNormalizationFactor} (Screen height: {Screen.height}, Reference: {referenceHeight})"); } private void OnEnable() { // Only start coroutines if not paused if (!_isPaused) { StartObstacleCoroutines(); } // Screen bounds are calculated in CheckIfOffScreen method } private void OnDisable() { // Stop coroutines when disabled StopObstacleCoroutines(); } /// /// Pause this obstacle's movement and behavior /// public void Pause() { if (_isPaused) return; // Already paused _isPaused = true; StopObstacleCoroutines(); Logging.Debug($"[FloatingObstacle] Paused obstacle: {name}"); } /// /// Resume this obstacle's movement and behavior /// public void DoResume() { if (!_isPaused) return; // Already running _isPaused = false; StartObstacleCoroutines(); Logging.Debug($"[FloatingObstacle] Resumed obstacle: {name}"); } /// /// Start all coroutines used by this obstacle /// private void StartObstacleCoroutines() { if (!isActiveAndEnabled) return; if (enableMovement && _movementCoroutine == null) { _movementCoroutine = StartCoroutine(MovementCoroutine()); } if (_offScreenCheckCoroutine == null) { _offScreenCheckCoroutine = StartCoroutine(OffScreenCheckCoroutine()); } } /// /// Stop all coroutines used by this obstacle /// private void StopObstacleCoroutines() { if (_movementCoroutine != null) { StopCoroutine(_movementCoroutine); _movementCoroutine = null; } if (_offScreenCheckCoroutine != null) { StopCoroutine(_offScreenCheckCoroutine); _offScreenCheckCoroutine = null; } } /// /// Called when the velocity factor changes from the DivingGameManager via ObstacleSpawner /// public void OnVelocityFactorChanged(float velocityFactor) { _velocityFactor = velocityFactor; // Update actual move speed based on velocity factor and base speed // We use Abs for magnitude and Sign for direction moveSpeed = _baseMoveSpeed * Mathf.Abs(_velocityFactor) * _screenNormalizationFactor; // Restart movement with new speed if needed if (enableMovement && gameObject.activeInHierarchy) { if (_movementCoroutine != null) { StopCoroutine(_movementCoroutine); } _movementCoroutine = StartCoroutine(MovementCoroutine()); } Logging.Debug($"[FloatingObstacle] {gameObject.name} velocity factor updated to {_velocityFactor:F2}, normalized speed: {moveSpeed:F2}"); } /// /// Coroutine that handles obstacle movement using normalized velocities /// private IEnumerator MovementCoroutine() { Logging.Debug($"[FloatingObstacle] Started movement coroutine with speed: {_baseMoveSpeed:F3}"); while (enabled && gameObject.activeInHierarchy) { // Use velocity factor sign to determine direction Vector3 direction = Vector3.up * Mathf.Sign(_velocityFactor); // Apply normalized movement using the shared utility method float speed = AppleHillsUtils.CalculateNormalizedMovementSpeed(_baseMoveSpeed); // Apply movement in correct direction transform.position += direction * speed; // Wait for next frame yield return null; } } /// /// Coroutine that checks if obstacle has moved off-screen /// Runs at a lower frequency than movement for better performance /// private IEnumerator OffScreenCheckCoroutine() { const float checkInterval = 0.2f; // Check every 200ms instead of every frame while (enabled && gameObject.activeInHierarchy) { CheckIfOffScreen(); // Wait for the check interval yield return new WaitForSeconds(checkInterval); } } /// /// Disables the collider after hitting the player to prevent further collisions /// This is more performant than tracking hit state /// public void MarkDamageDealt() { if (_collider != null && _collider.enabled) { _collider.enabled = false; Logging.Debug($"[FloatingObstacle] Obstacle {gameObject.name} hit player - collider disabled"); } } /// /// Checks if the obstacle has moved off-screen and should be despawned /// private void CheckIfOffScreen() { if (_mainCamera == null) { _mainCamera = UnityEngine.Camera.main; if (_mainCamera == null) return; } // Always recalculate screen bounds to ensure accuracy Vector3 topWorldPoint = _mainCamera.ViewportToWorldPoint(new Vector3(0.5f, 1f, _mainCamera.transform.position.z)); _screenTop = topWorldPoint.y; Vector3 bottomWorldPoint = _mainCamera.ViewportToWorldPoint(new Vector3(0.5f, 0f, _mainCamera.transform.position.z)); _screenBottom = bottomWorldPoint.y; // Check if obstacle is significantly above screen top (obstacles move upward) // Use a larger buffer to ensure obstacles are truly off-screen before returning to pool if (transform.position.y > _screenTop + 5f) { Logging.Debug($"[FloatingObstacle] {gameObject.name} off-screen at Y:{transform.position.y:F2}, screen top:{_screenTop:F2}"); ReturnToPool(); } else if (transform.position.y < _screenBottom - 5f) // Added check for bottom screen edge { Logging.Debug($"[FloatingObstacle] {gameObject.name} below screen at Y:{transform.position.y:F2}, screen bottom:{_screenBottom:F2}"); ReturnToPool(); } } /// /// Returns this obstacle to the spawner's pool /// private void ReturnToPool() { // CRITICAL: Stop all behavior first to prevent race conditions // This ensures no more off-screen checks or movement happen during pool return StopObstacleCoroutines(); if (spawner != null) { spawner.ReturnObstacleToPool(gameObject, prefabIndex); } else { // Try to find the spawner instead of destroying the object ObstacleSpawner foundSpawner = FindFirstObjectByType(); if (foundSpawner != null) { Logging.Warning($"[FloatingObstacle] Obstacle {gameObject.name} lost spawner reference, found replacement spawner"); spawner = foundSpawner; spawner.ReturnObstacleToPool(gameObject, prefabIndex); } else { // No spawner found - just deactivate the object instead of destroying it Logging.Warning($"[FloatingObstacle] No spawner found for {gameObject.name}, deactivating safely"); gameObject.SetActive(false); // Move to a safe location to avoid interference transform.position = new Vector3(1000f, 1000f, 0f); } } } /// /// Sets the spawner reference for this obstacle /// /// The spawner that created this obstacle public void SetSpawner(ObstacleSpawner obstacleSpawner) { spawner = obstacleSpawner; } /// /// Called when the obstacle is retrieved from the pool /// public void OnSpawn() { // Reset all state first _screenTop = 0f; // Reset cached screen bounds _mainCamera = UnityEngine.Camera.main; // Refresh camera reference // Re-enable the collider for reuse if (_collider != null) { _collider.enabled = true; } Logging.Debug($"[FloatingObstacle] Obstacle {gameObject.name} spawned from pool"); // Note: Don't start coroutines here - OnEnable() will handle that when SetActive(true) is called } /// /// Called when the obstacle is returned to the pool /// public void OnDespawn() { // Stop all coroutines before returning to pool StopObstacleCoroutines(); // Re-enable collider for next use (in case it was disabled) if (_collider != null) { _collider.enabled = true; } Logging.Debug($"[FloatingObstacle] Obstacle {gameObject.name} despawned to pool"); } /// /// Public method to manually trigger return to pool (for external systems) /// public void ForceReturnToPool() { ReturnToPool(); } /// /// Public method to enable/disable movement at runtime /// public void SetMovementEnabled(bool enabled) { if (enableMovement == enabled) return; enableMovement = enabled; // Restart coroutines to apply movement change if (gameObject.activeInHierarchy) { StopObstacleCoroutines(); StartObstacleCoroutines(); } } /// /// Sets surfacing mode, which reverses obstacle movement direction /// public void StartSurfacing() { if (_isSurfacing) return; // Already surfacing _isSurfacing = true; // Reverse movement speed (already handled by ObstacleSpawner, but this ensures consistency) moveSpeed *= -1; Logging.Debug($"[FloatingObstacle] {gameObject.name} started surfacing with speed: {moveSpeed}"); } } }