2025-09-21 07:32:56 +00:00
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
using System.Collections;
|
|
|
|
|
|
using Pooling;
|
|
|
|
|
|
|
|
|
|
|
|
namespace Minigames.DivingForPictures
|
|
|
|
|
|
{
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 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.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
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;
|
2025-09-22 12:16:32 +00:00
|
|
|
|
private float _screenBottom; // Added to track bottom of screen
|
2025-09-21 07:32:56 +00:00
|
|
|
|
private Coroutine _movementCoroutine;
|
|
|
|
|
|
private Coroutine _offScreenCheckCoroutine;
|
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 game manager
|
|
|
|
|
|
private float _baseMoveSpeed; // Original move speed before velocity factor is applied
|
2025-09-21 07:32:56 +00:00
|
|
|
|
|
|
|
|
|
|
private void Awake()
|
|
|
|
|
|
{
|
|
|
|
|
|
_collider = GetComponent<Collider2D>();
|
|
|
|
|
|
|
|
|
|
|
|
if (_collider == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
_collider = GetComponentInChildren<Collider2D>();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (_collider == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogError($"[FloatingObstacle] No Collider2D found on {gameObject.name}!");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_mainCamera = Camera.main;
|
2025-09-22 12:16:32 +00:00
|
|
|
|
_baseMoveSpeed = moveSpeed; // Store original speed
|
2025-09-21 07:32:56 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void OnEnable()
|
|
|
|
|
|
{
|
|
|
|
|
|
StartObstacleBehavior();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void OnDisable()
|
|
|
|
|
|
{
|
|
|
|
|
|
StopObstacleBehavior();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Starts the obstacle behavior coroutines
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private void StartObstacleBehavior()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (enableMovement)
|
|
|
|
|
|
{
|
|
|
|
|
|
_movementCoroutine = StartCoroutine(MovementCoroutine());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_offScreenCheckCoroutine = StartCoroutine(OffScreenCheckCoroutine());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Stops all obstacle behavior coroutines
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private void StopObstacleBehavior()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_movementCoroutine != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
StopCoroutine(_movementCoroutine);
|
|
|
|
|
|
_movementCoroutine = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (_offScreenCheckCoroutine != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
StopCoroutine(_offScreenCheckCoroutine);
|
|
|
|
|
|
_offScreenCheckCoroutine = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-22 12:16:32 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Called when the velocity factor changes from the DivingGameManager via ObstacleSpawner
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
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}");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-21 07:32:56 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Coroutine that handles obstacle movement
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private IEnumerator MovementCoroutine()
|
|
|
|
|
|
{
|
|
|
|
|
|
while (enabled && gameObject.activeInHierarchy)
|
|
|
|
|
|
{
|
2025-09-22 12:16:32 +00:00
|
|
|
|
// 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;
|
2025-09-21 07:32:56 +00:00
|
|
|
|
|
|
|
|
|
|
// Wait for next frame
|
|
|
|
|
|
yield return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Coroutine that checks if obstacle has moved off-screen
|
|
|
|
|
|
/// Runs at a lower frequency than movement for better performance
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Disables the collider after hitting the player to prevent further collisions
|
|
|
|
|
|
/// This is more performant than tracking hit state
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public void MarkDamageDealt()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_collider != null && _collider.enabled)
|
|
|
|
|
|
{
|
|
|
|
|
|
_collider.enabled = false;
|
|
|
|
|
|
Debug.Log($"[FloatingObstacle] Obstacle {gameObject.name} hit player - collider disabled");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Checks if the obstacle has moved off-screen and should be despawned
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
2025-09-22 12:16:32 +00:00
|
|
|
|
Vector3 bottomWorldPoint = _mainCamera.ViewportToWorldPoint(new Vector3(0.5f, 0f, _mainCamera.transform.position.z));
|
|
|
|
|
|
_screenBottom = bottomWorldPoint.y;
|
|
|
|
|
|
|
2025-09-21 07:32:56 +00:00
|
|
|
|
// 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();
|
|
|
|
|
|
}
|
2025-09-22 12:16:32 +00:00
|
|
|
|
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();
|
|
|
|
|
|
}
|
2025-09-21 07:32:56 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Returns this obstacle to the spawner's pool
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
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<ObstacleSpawner>();
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Sets the spawner reference for this obstacle
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="obstacleSpawner">The spawner that created this obstacle</param>
|
|
|
|
|
|
public void SetSpawner(ObstacleSpawner obstacleSpawner)
|
|
|
|
|
|
{
|
|
|
|
|
|
spawner = obstacleSpawner;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Called when the obstacle is retrieved from the pool
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Called when the obstacle is returned to the pool
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
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");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Public method to manually trigger return to pool (for external systems)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public void ForceReturnToPool()
|
|
|
|
|
|
{
|
|
|
|
|
|
ReturnToPool();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Public method to enable/disable movement at runtime
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public void SetMovementEnabled(bool enabled)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (enableMovement == enabled) return;
|
|
|
|
|
|
|
|
|
|
|
|
enableMovement = enabled;
|
|
|
|
|
|
|
|
|
|
|
|
// Restart coroutines to apply movement change
|
|
|
|
|
|
if (gameObject.activeInHierarchy)
|
|
|
|
|
|
{
|
|
|
|
|
|
StopObstacleBehavior();
|
|
|
|
|
|
StartObstacleBehavior();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-22 12:16:32 +00:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Sets surfacing mode, which reverses obstacle movement direction
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
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}");
|
|
|
|
|
|
}
|
2025-09-21 07:32:56 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|