Files
AppleHillsProduction/Assets/Scripts/Minigames/DivingForPictures/FloatingObstacle.cs
2025-09-21 08:56:35 +02:00

258 lines
8.1 KiB
C#

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;
private Coroutine _movementCoroutine;
private Coroutine _offScreenCheckCoroutine;
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;
}
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;
}
}
/// <summary>
/// Coroutine that handles obstacle movement
/// </summary>
private IEnumerator MovementCoroutine()
{
while (enabled && gameObject.activeInHierarchy)
{
// Move the obstacle upward
transform.position += Vector3.up * (moveSpeed * Time.deltaTime);
// 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) return;
// Calculate screen top if not cached
if (_screenTop == 0f)
{
Vector3 topWorldPoint = _mainCamera.ViewportToWorldPoint(new Vector3(0.5f, 1f, _mainCamera.nearClipPlane));
_screenTop = topWorldPoint.y;
}
// Check if obstacle is above screen
if (transform.position.y > _screenTop + 2f) // Extra buffer for safety
{
ReturnToPool();
}
}
/// <summary>
/// Returns this obstacle to the spawner's pool
/// </summary>
private void ReturnToPool()
{
if (spawner != null)
{
spawner.ReturnObstacleToPool(gameObject, prefabIndex);
}
else
{
Debug.LogWarning($"[FloatingObstacle] Cannot return {gameObject.name} to pool - missing spawner reference");
Destroy(gameObject);
}
}
/// <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()
{
_screenTop = 0f; // Reset cached screen bounds
// Re-enable the collider for reuse
if (_collider != null)
{
_collider.enabled = true;
}
// Ensure the obstacle is active and visible
gameObject.SetActive(true);
Debug.Log($"[FloatingObstacle] Obstacle {gameObject.name} spawned");
}
/// <summary>
/// Called when the obstacle is returned to the pool
/// </summary>
public void OnDespawn()
{
// Re-enable collider for next use (in case it was disabled)
if (_collider != null)
{
_collider.enabled = true;
}
Debug.Log($"[FloatingObstacle] Obstacle {gameObject.name} despawned");
}
/// <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();
}
}
}
}