using UnityEngine; using System.Collections; using Pooling; 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 { [Header("Obstacle Properties")] [Tooltip("Index of the prefab this obstacle was created from")] [SerializeField] private int prefabIndex; [Tooltip("Movement speed of this obstacle")] [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 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 private void Awake() { _collider = GetComponent(); if (_collider == null) { _collider = GetComponentInChildren(); } if (_collider == null) { Debug.LogError($"[FloatingObstacle] No Collider2D found on {gameObject.name}!"); } _mainCamera = Camera.main; _baseMoveSpeed = moveSpeed; // Store original speed } private void OnEnable() { StartObstacleBehavior(); } private void OnDisable() { StopObstacleBehavior(); } /// /// Starts the obstacle behavior coroutines /// private void StartObstacleBehavior() { if (enableMovement) { _movementCoroutine = StartCoroutine(MovementCoroutine()); } _offScreenCheckCoroutine = StartCoroutine(OffScreenCheckCoroutine()); } /// /// Stops all obstacle behavior coroutines /// private void StopObstacleBehavior() { 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); // Restart movement with new speed if needed if (enableMovement && gameObject.activeInHierarchy) { if (_movementCoroutine != null) { StopCoroutine(_movementCoroutine); } _movementCoroutine = StartCoroutine(MovementCoroutine()); } Debug.Log($"[FloatingObstacle] {gameObject.name} velocity factor updated to {_velocityFactor:F2}, speed: {moveSpeed:F2}"); } /// /// Coroutine that handles obstacle movement /// private IEnumerator MovementCoroutine() { while (enabled && gameObject.activeInHierarchy) { // Use velocity factor sign to determine direction Vector3 direction = Vector3.up * Mathf.Sign(_velocityFactor); float speed = moveSpeed * Time.deltaTime; // 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; Debug.Log($"[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 = 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) { Debug.Log($"[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 { Debug.Log($"[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 StopObstacleBehavior(); if (spawner != null) { spawner.ReturnObstacleToPool(gameObject, prefabIndex); } else { // Try to find the spawner instead of destroying the object ObstacleSpawner foundSpawner = FindFirstObjectByType(); if (foundSpawner != null) { Debug.LogWarning($"[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 Debug.LogWarning($"[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 = Camera.main; // Refresh camera reference // Re-enable the collider for reuse if (_collider != null) { _collider.enabled = true; } Debug.Log($"[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 StopObstacleBehavior(); // Re-enable collider for next use (in case it was disabled) if (_collider != null) { _collider.enabled = true; } Debug.Log($"[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) { StopObstacleBehavior(); StartObstacleBehavior(); } } /// /// 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; Debug.Log($"[FloatingObstacle] {gameObject.name} started surfacing with speed: {moveSpeed}"); } } }