Merge pull request 'Add a pausable interface and make minigameobjecs pausable' (#20) from pause_minigame into main

Reviewed-on: #20
This commit is contained in:
2025-10-08 10:37:46 +00:00
11 changed files with 1101 additions and 395 deletions

View File

@@ -1,6 +1,9 @@
using UnityEngine; using UnityEngine;
using AppleHills.Core.Settings; using AppleHills.Core.Settings;
using System; using System;
using System.Collections.Generic;
using AppleHills.Core.Interfaces;
using UI;
/// <summary> /// <summary>
/// Singleton manager for global game state and settings. Provides accessors for various gameplay parameters. /// Singleton manager for global game state and settings. Provides accessors for various gameplay parameters.
@@ -36,6 +39,21 @@ public class GameManager : MonoBehaviour
[SerializeField] private bool _developerSettingsLoaded = false; [SerializeField] private bool _developerSettingsLoaded = false;
public bool SettingsLoaded => _settingsLoaded; public bool SettingsLoaded => _settingsLoaded;
public bool DeveloperSettingsLoaded => _developerSettingsLoaded; public bool DeveloperSettingsLoaded => _developerSettingsLoaded;
[Header("Game State")]
[SerializeField] private bool _isPaused = false;
/// <summary>
/// Current pause state of the game
/// </summary>
public bool IsPaused => _isPaused;
// List of pausable components that have registered with the GameManager
private List<IPausable> _pausableComponents = new List<IPausable>();
// Events for pause state changes
public event Action OnGamePaused;
public event Action OnGameResumed;
void Awake() void Awake()
{ {
@@ -53,6 +71,136 @@ public class GameManager : MonoBehaviour
// DontDestroyOnLoad(gameObject); // DontDestroyOnLoad(gameObject);
} }
private void Start()
{
// Find and subscribe to PauseMenu events
PauseMenu pauseMenu = PauseMenu.Instance;
if (pauseMenu != null)
{
pauseMenu.OnGamePaused += OnPauseMenuPaused;
pauseMenu.OnGameResumed += OnPauseMenuResumed;
Debug.Log("[GameManager] Subscribed to PauseMenu events");
}
else
{
Debug.LogWarning("[GameManager] PauseMenu not found. Pause functionality won't work properly.");
}
}
private void OnDestroy()
{
// Unsubscribe from PauseMenu events
PauseMenu pauseMenu = FindFirstObjectByType<PauseMenu>();
if (pauseMenu != null)
{
pauseMenu.OnGamePaused -= OnPauseMenuPaused;
pauseMenu.OnGameResumed -= OnPauseMenuResumed;
}
}
/// <summary>
/// Register a component as pausable, so it receives pause/resume notifications
/// </summary>
/// <param name="component">The pausable component to register</param>
public void RegisterPausableComponent(IPausable component)
{
if (component != null && !_pausableComponents.Contains(component))
{
_pausableComponents.Add(component);
// If the game is already paused, pause the component immediately
if (_isPaused)
{
component.Pause();
}
Debug.Log($"[GameManager] Registered pausable component: {(component as MonoBehaviour)?.name ?? "Unknown"}");
}
}
/// <summary>
/// Unregister a pausable component
/// </summary>
/// <param name="component">The pausable component to unregister</param>
public void UnregisterPausableComponent(IPausable component)
{
if (component != null && _pausableComponents.Contains(component))
{
_pausableComponents.Remove(component);
Debug.Log($"[GameManager] Unregistered pausable component: {(component as MonoBehaviour)?.name ?? "Unknown"}");
}
}
/// <summary>
/// Called when the PauseMenu broadcasts a pause event
/// </summary>
private void OnPauseMenuPaused()
{
PauseGame();
}
/// <summary>
/// Called when the PauseMenu broadcasts a resume event
/// </summary>
private void OnPauseMenuResumed()
{
ResumeGame();
}
/// <summary>
/// Pause the game and notify all registered pausable components
/// </summary>
public void PauseGame()
{
if (_isPaused) return; // Already paused
_isPaused = true;
// Pause all registered components
foreach (var component in _pausableComponents)
{
component.Pause();
}
// Broadcast pause event
OnGamePaused?.Invoke();
Debug.Log($"[GameManager] Game paused. Paused {_pausableComponents.Count} components.");
}
/// <summary>
/// Resume the game and notify all registered pausable components
/// </summary>
public void ResumeGame()
{
if (!_isPaused) return; // Already running
_isPaused = false;
// Resume all registered components
foreach (var component in _pausableComponents)
{
component.Resume();
}
// Broadcast resume event
OnGameResumed?.Invoke();
Debug.Log($"[GameManager] Game resumed. Resumed {_pausableComponents.Count} components.");
}
/// <summary>
/// Toggle the pause state of the game
/// </summary>
public void TogglePause()
{
if (_isPaused)
ResumeGame();
else
PauseGame();
}
private void InitializeSettings() private void InitializeSettings()
{ {
@@ -164,5 +312,4 @@ public class GameManager : MonoBehaviour
public float PlayerStopDistance => GetSettings<IInteractionSettings>()?.PlayerStopDistance ?? 6.0f; public float PlayerStopDistance => GetSettings<IInteractionSettings>()?.PlayerStopDistance ?? 6.0f;
public float PlayerStopDistanceDirectInteraction => GetSettings<IInteractionSettings>()?.PlayerStopDistanceDirectInteraction ?? 2.0f; public float PlayerStopDistanceDirectInteraction => GetSettings<IInteractionSettings>()?.PlayerStopDistanceDirectInteraction ?? 2.0f;
public float DefaultPuzzlePromptRange => GetSettings<IInteractionSettings>()?.DefaultPuzzlePromptRange ?? 3.0f; public float DefaultPuzzlePromptRange => GetSettings<IInteractionSettings>()?.DefaultPuzzlePromptRange ?? 3.0f;
} }

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ba87fe038e914973bc341817149a9ebe
timeCreated: 1759916676

View File

@@ -0,0 +1,25 @@
using UnityEngine;
namespace AppleHills.Core.Interfaces
{
/// <summary>
/// Interface for components that can be paused and resumed
/// </summary>
public interface IPausable
{
/// <summary>
/// Pauses the component's functionality
/// </summary>
void Pause();
/// <summary>
/// Resumes the component's functionality
/// </summary>
void Resume();
/// <summary>
/// Gets whether the component is currently paused
/// </summary>
bool IsPaused { get; }
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 3a493ef684ce47208a3f6e7d67727882
timeCreated: 1759916676

View File

@@ -1,6 +1,7 @@
using UnityEngine; using UnityEngine;
using System.Collections; using System.Collections;
using Pooling; using Pooling;
using AppleHills.Core.Interfaces;
namespace Minigames.DivingForPictures namespace Minigames.DivingForPictures
{ {
@@ -8,7 +9,7 @@ namespace Minigames.DivingForPictures
/// Represents a single bubble, handling its movement, wobble effect, scaling, and sprite assignment. /// Represents a single bubble, handling its movement, wobble effect, scaling, and sprite assignment.
/// Uses coroutines for better performance instead of Update() calls. /// Uses coroutines for better performance instead of Update() calls.
/// </summary> /// </summary>
public class Bubble : MonoBehaviour, IPoolableWithReference<BubblePool> public class Bubble : MonoBehaviour, IPoolableWithReference<BubblePool>, IPausable
{ {
public float speed = 1f; public float speed = 1f;
public float wobbleSpeed = 1f; public float wobbleSpeed = 1f;
@@ -26,6 +27,12 @@ namespace Minigames.DivingForPictures
private Coroutine _movementCoroutine; private Coroutine _movementCoroutine;
private Coroutine _wobbleCoroutine; private Coroutine _wobbleCoroutine;
private Coroutine _offScreenCheckCoroutine; private Coroutine _offScreenCheckCoroutine;
// Pause state tracking
private bool _isPaused = false;
// IPausable implementation
public bool IsPaused => _isPaused;
void Awake() void Awake()
{ {
@@ -53,12 +60,42 @@ namespace Minigames.DivingForPictures
{ {
StopBubbleBehavior(); StopBubbleBehavior();
} }
/// <summary>
/// Pauses all bubble behaviors
/// </summary>
public void Pause()
{
if (_isPaused) return; // Already paused
_isPaused = true;
StopBubbleBehavior();
// Debug log for troubleshooting
Debug.Log($"[Bubble] Paused bubble: {name}");
}
/// <summary>
/// Resumes all bubble behaviors
/// </summary>
public void Resume()
{
if (!_isPaused) return; // Already running
_isPaused = false;
StartBubbleBehavior();
// Debug log for troubleshooting
Debug.Log($"[Bubble] Resumed bubble: {name}");
}
/// <summary> /// <summary>
/// Starts all bubble behavior coroutines /// Starts all bubble behavior coroutines
/// </summary> /// </summary>
private void StartBubbleBehavior() private void StartBubbleBehavior()
{ {
if (_isPaused) return; // Don't start if paused
_movementCoroutine = StartCoroutine(MovementCoroutine()); _movementCoroutine = StartCoroutine(MovementCoroutine());
_wobbleCoroutine = StartCoroutine(WobbleCoroutine()); _wobbleCoroutine = StartCoroutine(WobbleCoroutine());
_offScreenCheckCoroutine = StartCoroutine(OffScreenCheckCoroutine()); _offScreenCheckCoroutine = StartCoroutine(OffScreenCheckCoroutine());

View File

@@ -1,13 +1,15 @@
using UnityEngine; using System.Collections;
using UnityEngine;
using Pooling; using Pooling;
using AppleHills.Core.Settings; using AppleHills.Core.Settings;
using AppleHills.Core.Interfaces;
namespace Minigames.DivingForPictures namespace Minigames.DivingForPictures
{ {
/// <summary> /// <summary>
/// Spawns bubbles at intervals, randomizing their properties and assigning a random sprite to each. /// Spawns bubbles at intervals, randomizing their properties and assigning a random sprite to each.
/// </summary> /// </summary>
public class BubbleSpawner : MonoBehaviour public class BubbleSpawner : MonoBehaviour, IPausable
{ {
public Bubble bubblePrefab; public Bubble bubblePrefab;
public Sprite[] bubbleSprites; // Assign in inspector public Sprite[] bubbleSprites; // Assign in inspector
@@ -20,6 +22,15 @@ namespace Minigames.DivingForPictures
private BubblePool _bubblePool; private BubblePool _bubblePool;
private Camera _mainCamera; // Cache camera reference private Camera _mainCamera; // Cache camera reference
private bool _isSurfacing = false; private bool _isSurfacing = false;
// Pause state
private bool _isPaused = false;
// Coroutines for pause/resume
private Coroutine _spawnCoroutine;
// IPausable implementation
public bool IsPaused => _isPaused;
void Awake() void Awake()
{ {
@@ -50,23 +61,110 @@ namespace Minigames.DivingForPictures
InvokeRepeating(nameof(LogPoolStats), 5f, 30f); InvokeRepeating(nameof(LogPoolStats), 5f, 30f);
#endif #endif
} }
SetNextSpawnInterval();
} }
void Start() void Start()
{ {
// Initialize the next spawn interval // Register with DivingGameManager for pause/resume events
_nextSpawnInterval = GetRandomizedInterval(); DivingGameManager gameManager = FindFirstObjectByType<DivingGameManager>();
if (gameManager != null)
{
gameManager.RegisterPausableComponent(this);
}
// Start spawning if not paused
StartSpawningCoroutine();
}
void OnDestroy()
{
// Unregister from DivingGameManager
DivingGameManager gameManager = FindFirstObjectByType<DivingGameManager>();
if (gameManager != null)
{
gameManager.UnregisterPausableComponent(this);
}
// Clean up any active coroutines
StopAllCoroutines();
}
/// <summary>
/// Pauses the bubble spawner and all bubbles
/// </summary>
public void Pause()
{
if (_isPaused) return; // Already paused
_isPaused = true;
// Stop spawning coroutine
if (_spawnCoroutine != null)
{
StopCoroutine(_spawnCoroutine);
_spawnCoroutine = null;
}
// Pause all active bubbles
Bubble[] activeBubbles = FindObjectsOfType<Bubble>();
foreach (var bubble in activeBubbles)
{
if (bubble != null)
{
bubble.Pause();
}
}
Debug.Log("[BubbleSpawner] Paused");
}
/// <summary>
/// Resumes the bubble spawner and all bubbles
/// </summary>
public void Resume()
{
if (!_isPaused) return; // Already running
_isPaused = false;
// Restart spawning coroutine
StartSpawningCoroutine();
// Resume all active bubbles
Bubble[] activeBubbles = FindObjectsOfType<Bubble>();
foreach (var bubble in activeBubbles)
{
if (bubble != null)
{
bubble.Resume();
}
}
Debug.Log("[BubbleSpawner] Resumed");
}
/// <summary>
/// Starts the bubble spawning coroutine if it's not already running
/// </summary>
private void StartSpawningCoroutine()
{
if (_spawnCoroutine == null && !_isPaused)
{
_spawnCoroutine = StartCoroutine(SpawnBubblesRoutine());
}
} }
void Update() /// <summary>
/// Sets the next spawn interval using randomized timing
/// </summary>
private void SetNextSpawnInterval()
{ {
_timer += Time.deltaTime; if (_devSettings == null) return;
if (_timer >= _nextSpawnInterval)
{ _nextSpawnInterval = GetRandomizedInterval();
SpawnBubble(); Debug.Log($"[BubbleSpawner] Next spawn interval set to: {_nextSpawnInterval:F2}s");
_timer = 0f;
_nextSpawnInterval = GetRandomizedInterval();
}
} }
/// <summary> /// <summary>
@@ -128,6 +226,12 @@ namespace Minigames.DivingForPictures
// Pass min/max scale for wobble clamping // Pass min/max scale for wobble clamping
bubble.SetWobbleScaleLimits(_devSettings.BubbleWobbleMinScale, _devSettings.BubbleWobbleMaxScale); bubble.SetWobbleScaleLimits(_devSettings.BubbleWobbleMinScale, _devSettings.BubbleWobbleMaxScale);
// If the game is already paused, pause this bubble immediately
if (_isPaused)
{
bubble.Pause();
}
} }
/// <summary> /// <summary>
@@ -159,5 +263,24 @@ namespace Minigames.DivingForPictures
_bubblePool.LogPoolStats(); _bubblePool.LogPoolStats();
} }
} }
/// <summary>
/// Coroutine that handles the bubble spawning logic
/// </summary>
private IEnumerator SpawnBubblesRoutine()
{
Debug.Log("[BubbleSpawner] Started bubble spawning coroutine");
while (enabled && gameObject.activeInHierarchy && !_isPaused)
{
SpawnBubble();
SetNextSpawnInterval(); // Set interval for next spawn
yield return new WaitForSeconds(_nextSpawnInterval);
}
_spawnCoroutine = null;
Debug.Log("[BubbleSpawner] Bubble spawning coroutine ended");
}
} }
} }

View File

@@ -6,10 +6,12 @@ using UnityEngine.Events;
using UnityEngine.Playables; using UnityEngine.Playables;
using AppleHills.Core.Settings; using AppleHills.Core.Settings;
using Utility; using Utility;
using AppleHills.Core.Interfaces;
using UI;
namespace Minigames.DivingForPictures namespace Minigames.DivingForPictures
{ {
public class DivingGameManager : MonoBehaviour public class DivingGameManager : MonoBehaviour, IPausable
{ {
[Header("Monster Prefabs")] [Header("Monster Prefabs")]
[Tooltip("Array of monster prefabs to spawn randomly")] [Tooltip("Array of monster prefabs to spawn randomly")]
@@ -66,6 +68,15 @@ namespace Minigames.DivingForPictures
// Event for game components to subscribe to // Event for game components to subscribe to
public event Action OnGameInitialized; public event Action OnGameInitialized;
// Pause state
private bool _isPaused = false;
// List of pausable components controlled by this manager
private List<IPausable> _pausableComponents = new List<IPausable>();
// IPausable implementation
public bool IsPaused => _isPaused;
private void Awake() private void Awake()
{ {
// Get settings from GameManager // Get settings from GameManager
@@ -81,6 +92,26 @@ namespace Minigames.DivingForPictures
private void Start() private void Start()
{ {
// Find PauseMenu and subscribe to its events
PauseMenu pauseMenu = PauseMenu.Instance;
if (pauseMenu != null)
{
pauseMenu.OnGamePaused += Pause;
pauseMenu.OnGameResumed += Resume;
Debug.Log("[DivingGameManager] Subscribed to PauseMenu events");
}
else
{
Debug.LogWarning("[DivingGameManager] PauseMenu not found. Pause functionality won't work properly.");
}
// Register this manager with the global GameManager
if (GameManager.Instance != null)
{
GameManager.Instance.RegisterPausableComponent(this);
}
// Subscribe to SceneOrientationEnforcer's event // Subscribe to SceneOrientationEnforcer's event
if (SceneOrientationEnforcer.Instance != null) if (SceneOrientationEnforcer.Instance != null)
{ {
@@ -144,6 +175,23 @@ namespace Minigames.DivingForPictures
{ {
SceneOrientationEnforcer.Instance.OnOrientationCorrect -= InitializeGame; SceneOrientationEnforcer.Instance.OnOrientationCorrect -= InitializeGame;
} }
// Unsubscribe from PauseMenu events
PauseMenu pauseMenu = PauseMenu.Instance;
if (pauseMenu != null)
{
pauseMenu.OnGamePaused -= Pause;
pauseMenu.OnGameResumed -= Resume;
}
// Unregister from GameManager
if (GameManager.Instance != null)
{
GameManager.Instance.UnregisterPausableComponent(this);
}
// Unregister all pausable components
_pausableComponents.Clear();
} }
private void Update() private void Update()
@@ -651,5 +699,74 @@ namespace Minigames.DivingForPictures
// Final assignment to ensure exact target value // Final assignment to ensure exact target value
OnVelocityFactorChanged?.Invoke(_currentVelocityFactor); OnVelocityFactorChanged?.Invoke(_currentVelocityFactor);
} }
/// <summary>
/// Register a component as pausable with this manager
/// </summary>
/// <param name="component">The pausable component to register</param>
public void RegisterPausableComponent(IPausable component)
{
if (component != null && !_pausableComponents.Contains(component))
{
_pausableComponents.Add(component);
// If the game is already paused, pause the component immediately
if (_isPaused)
{
component.Pause();
}
Debug.Log($"[DivingGameManager] Registered pausable component: {(component as MonoBehaviour)?.name ?? "Unknown"}");
}
}
/// <summary>
/// Unregister a pausable component
/// </summary>
/// <param name="component">The pausable component to unregister</param>
public void UnregisterPausableComponent(IPausable component)
{
if (component != null && _pausableComponents.Contains(component))
{
_pausableComponents.Remove(component);
Debug.Log($"[DivingGameManager] Unregistered pausable component: {(component as MonoBehaviour)?.name ?? "Unknown"}");
}
}
/// <summary>
/// Pause the game and all registered components
/// </summary>
public void Pause()
{
if (_isPaused) return; // Already paused
_isPaused = true;
// Pause all registered components
foreach (var component in _pausableComponents)
{
component.Pause();
}
Debug.Log($"[DivingGameManager] Game paused. Paused {_pausableComponents.Count} components.");
}
/// <summary>
/// Resume the game and all registered components
/// </summary>
public void Resume()
{
if (!_isPaused) return; // Already running
_isPaused = false;
// Resume all registered components
foreach (var component in _pausableComponents)
{
component.Resume();
}
Debug.Log($"[DivingGameManager] Game resumed. Resumed {_pausableComponents.Count} components.");
}
} }
} }

View File

@@ -3,6 +3,7 @@ using System.Collections;
using AppleHills.Core.Settings; using AppleHills.Core.Settings;
using Pooling; using Pooling;
using Utils; using Utils;
using AppleHills.Core.Interfaces;
namespace Minigames.DivingForPictures namespace Minigames.DivingForPictures
{ {
@@ -12,7 +13,7 @@ namespace Minigames.DivingForPictures
/// Once an obstacle hits the player, its collider is disabled to prevent further collisions. /// Once an obstacle hits the player, its collider is disabled to prevent further collisions.
/// Uses coroutines for better performance instead of Update() calls. /// Uses coroutines for better performance instead of Update() calls.
/// </summary> /// </summary>
public class FloatingObstacle : MonoBehaviour, IPoolable public class FloatingObstacle : MonoBehaviour, IPoolable, IPausable
{ {
[Header("Obstacle Properties")] [Header("Obstacle Properties")]
[Tooltip("Index of the prefab this obstacle was created from")] [Tooltip("Index of the prefab this obstacle was created from")]
@@ -57,6 +58,12 @@ namespace Minigames.DivingForPictures
private float _screenNormalizationFactor = 1.0f; private float _screenNormalizationFactor = 1.0f;
private IDivingMinigameSettings _settings; private IDivingMinigameSettings _settings;
// Pause state
private bool _isPaused = false;
// IPausable implementation
public bool IsPaused => _isPaused;
private void Awake() private void Awake()
{ {
_collider = GetComponent<Collider2D>(); _collider = GetComponent<Collider2D>();
@@ -115,31 +122,67 @@ namespace Minigames.DivingForPictures
private void OnEnable() private void OnEnable()
{ {
StartObstacleBehavior(); // Only start coroutines if not paused
if (!_isPaused)
{
StartObstacleCoroutines();
}
// Screen bounds are calculated in CheckIfOffScreen method
} }
private void OnDisable() private void OnDisable()
{ {
StopObstacleBehavior(); // Stop coroutines when disabled
StopObstacleCoroutines();
} }
/// <summary> /// <summary>
/// Starts the obstacle behavior coroutines /// Pause this obstacle's movement and behavior
/// </summary> /// </summary>
private void StartObstacleBehavior() public void Pause()
{ {
if (enableMovement) if (_isPaused) return; // Already paused
_isPaused = true;
StopObstacleCoroutines();
Debug.Log($"[FloatingObstacle] Paused obstacle: {name}");
}
/// <summary>
/// Resume this obstacle's movement and behavior
/// </summary>
public void Resume()
{
if (!_isPaused) return; // Already running
_isPaused = false;
StartObstacleCoroutines();
Debug.Log($"[FloatingObstacle] Resumed obstacle: {name}");
}
/// <summary>
/// Start all coroutines used by this obstacle
/// </summary>
private void StartObstacleCoroutines()
{
if (enableMovement && _movementCoroutine == null)
{ {
_movementCoroutine = StartCoroutine(MovementCoroutine()); _movementCoroutine = StartCoroutine(MovementCoroutine());
} }
_offScreenCheckCoroutine = StartCoroutine(OffScreenCheckCoroutine()); if (_offScreenCheckCoroutine == null)
{
_offScreenCheckCoroutine = StartCoroutine(OffScreenCheckCoroutine());
}
} }
/// <summary> /// <summary>
/// Stops all obstacle behavior coroutines /// Stop all coroutines used by this obstacle
/// </summary> /// </summary>
private void StopObstacleBehavior() private void StopObstacleCoroutines()
{ {
if (_movementCoroutine != null) if (_movementCoroutine != null)
{ {
@@ -270,7 +313,7 @@ namespace Minigames.DivingForPictures
{ {
// CRITICAL: Stop all behavior first to prevent race conditions // CRITICAL: Stop all behavior first to prevent race conditions
// This ensures no more off-screen checks or movement happen during pool return // This ensures no more off-screen checks or movement happen during pool return
StopObstacleBehavior(); StopObstacleCoroutines();
if (spawner != null) if (spawner != null)
{ {
@@ -333,7 +376,7 @@ namespace Minigames.DivingForPictures
public void OnDespawn() public void OnDespawn()
{ {
// Stop all coroutines before returning to pool // Stop all coroutines before returning to pool
StopObstacleBehavior(); StopObstacleCoroutines();
// Re-enable collider for next use (in case it was disabled) // Re-enable collider for next use (in case it was disabled)
if (_collider != null) if (_collider != null)
@@ -364,8 +407,8 @@ namespace Minigames.DivingForPictures
// Restart coroutines to apply movement change // Restart coroutines to apply movement change
if (gameObject.activeInHierarchy) if (gameObject.activeInHierarchy)
{ {
StopObstacleBehavior(); StopObstacleCoroutines();
StartObstacleBehavior(); StartObstacleCoroutines();
} }
} }

View File

@@ -4,6 +4,7 @@ using UnityEngine;
using UnityEngine.Events; using UnityEngine.Events;
using Pooling; using Pooling;
using AppleHills.Core.Settings; using AppleHills.Core.Settings;
using AppleHills.Core.Interfaces;
namespace Minigames.DivingForPictures namespace Minigames.DivingForPictures
{ {
@@ -11,7 +12,7 @@ namespace Minigames.DivingForPictures
/// Spawns and manages mobile obstacles for the diving minigame. /// Spawns and manages mobile obstacles for the diving minigame.
/// Uses object pooling and validates spawn positions to avoid colliding with tiles. /// Uses object pooling and validates spawn positions to avoid colliding with tiles.
/// </summary> /// </summary>
public class ObstacleSpawner : MonoBehaviour public class ObstacleSpawner : MonoBehaviour, IPausable
{ {
[Header("Obstacle Prefabs")] [Header("Obstacle Prefabs")]
[Tooltip("List of possible obstacle prefabs to spawn")] [Tooltip("List of possible obstacle prefabs to spawn")]
@@ -34,11 +35,19 @@ namespace Minigames.DivingForPictures
private float _screenBottom; private float _screenBottom;
private float _spawnRangeX; private float _spawnRangeX;
private Coroutine _spawnCoroutine; private Coroutine _spawnCoroutine;
private Coroutine _moveCoroutine;
private Coroutine _despawnCoroutine;
private readonly List<GameObject> _activeObstacles = new List<GameObject>(); private readonly List<GameObject> _activeObstacles = new List<GameObject>();
private int _obstacleCounter = 0; // Counter for unique obstacle naming private int _obstacleCounter = 0; // Counter for unique obstacle naming
private bool _isSurfacing = false; // Flag to track surfacing state private bool _isSurfacing = false; // Flag to track surfacing state
private float _velocityFactor = 1.0f; // Current velocity factor from the game manager private float _velocityFactor = 1.0f; // Current velocity factor from the game manager
// Pause state
private bool _isPaused = false;
// IPausable implementation
public bool IsPaused => _isPaused;
private void Awake() private void Awake()
{ {
_mainCamera = Camera.main; _mainCamera = Camera.main;
@@ -75,14 +84,15 @@ namespace Minigames.DivingForPictures
private void Start() private void Start()
{ {
// Find DivingGameManager and subscribe to its initialization event // Find DivingGameManager and subscribe to its initialization event
DivingGameManager gameManager = FindFirstObjectByType<DivingGameManager>(); DivingGameManager gameManager = FindFirstObjectByType<DivingGameManager>();
if (gameManager != null) if (gameManager != null)
{ {
gameManager.OnGameInitialized += Initialize; gameManager.OnGameInitialized += Initialize;
// Register with the DivingGameManager for pause/resume events
gameManager.RegisterPausableComponent(this);
// If game is already initialized, initialize immediately // If game is already initialized, initialize immediately
if (gameManager.GetType().GetField("_isGameInitialized", if (gameManager.GetType().GetField("_isGameInitialized",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.NonPublic |
@@ -97,20 +107,130 @@ namespace Minigames.DivingForPictures
Initialize(); Initialize();
} }
} }
private void OnDestroy()
{
// Unregister from DivingGameManager
DivingGameManager gameManager = FindFirstObjectByType<DivingGameManager>();
if (gameManager != null)
{
gameManager.UnregisterPausableComponent(this);
}
// Clean up any active coroutines
StopAllCoroutines();
}
/// <summary> /// <summary>
/// Initializes the obstacle spawner when triggered by DivingGameManager /// Initializes the obstacle spawner when triggered by DivingGameManager
/// </summary> /// </summary>
private void Initialize() private void Initialize()
{ {
// Calculate screen bounds
CalculateScreenBounds(); CalculateScreenBounds();
StartSpawning();
// Start coroutines if not paused
StartSpawnCoroutine();
StartMoveCoroutine();
StartDespawnCoroutine();
Debug.Log("[ObstacleSpawner] Initialized"); Debug.Log("[ObstacleSpawner] Initialized");
} }
private void OnDestroy() /// <summary>
/// Pauses the spawner and all associated processes
/// </summary>
public void Pause()
{ {
StopSpawning(); if (_isPaused) return; // Already paused
_isPaused = true;
// Stop spawning coroutine
if (_spawnCoroutine != null)
{
StopCoroutine(_spawnCoroutine);
_spawnCoroutine = null;
}
// Pause all active obstacles
foreach (var obstacle in _activeObstacles.ToArray())
{
if (obstacle != null)
{
FloatingObstacle obstacleComponent = obstacle.GetComponent<FloatingObstacle>();
if (obstacleComponent != null)
{
obstacleComponent.Pause();
}
}
}
Debug.Log($"[ObstacleSpawner] Paused with {_activeObstacles.Count} active obstacles");
}
/// <summary>
/// Resumes the spawner and all associated processes
/// </summary>
public void Resume()
{
if (!_isPaused) return; // Already running
_isPaused = false;
// Restart spawning coroutine if not in surfacing mode
if (!_isSurfacing)
{
StartSpawnCoroutine();
}
// Resume all active obstacles
foreach (var obstacle in _activeObstacles.ToArray())
{
if (obstacle != null)
{
FloatingObstacle obstacleComponent = obstacle.GetComponent<FloatingObstacle>();
if (obstacleComponent != null)
{
obstacleComponent.Resume();
}
}
}
Debug.Log($"[ObstacleSpawner] Resumed with {_activeObstacles.Count} active obstacles");
}
/// <summary>
/// Starts the obstacle spawning coroutine if not already running
/// </summary>
private void StartSpawnCoroutine()
{
if (_spawnCoroutine == null && !_isPaused && !_isSurfacing)
{
_spawnCoroutine = StartCoroutine(SpawnObstacleRoutine());
}
}
/// <summary>
/// Starts the obstacle movement coroutine if not already running
/// </summary>
private void StartMoveCoroutine()
{
if (_moveCoroutine == null && !_isPaused)
{
_moveCoroutine = StartCoroutine(MoveObstaclesRoutine());
}
}
/// <summary>
/// Starts the obstacle despawning coroutine if not already running
/// </summary>
private void StartDespawnCoroutine()
{
if (_despawnCoroutine == null && !_isPaused)
{
_despawnCoroutine = StartCoroutine(DespawnObstaclesRoutine());
}
} }
/// <summary> /// <summary>
@@ -444,8 +564,14 @@ namespace Minigames.DivingForPictures
_settings.ObstacleMinMoveSpeed, _settings.ObstacleMinMoveSpeed,
_settings.ObstacleMaxMoveSpeed); _settings.ObstacleMaxMoveSpeed);
// Set spawner reference (since FloatingObstacle has this built-in now) // Set spawner reference
obstacleComponent.SetSpawner(this); obstacleComponent.SetSpawner(this);
// If spawner is already paused, pause the obstacle immediately
if (_isPaused)
{
obstacleComponent.Pause();
}
} }
} }
@@ -559,6 +685,147 @@ namespace Minigames.DivingForPictures
/// </summary> /// </summary>
public int ActiveObstacleCount => _activeObstacles.Count; public int ActiveObstacleCount => _activeObstacles.Count;
/// <summary>
/// Coroutine that handles obstacle spawning at regular intervals
/// </summary>
private IEnumerator SpawnObstacleRoutine()
{
Debug.Log("[ObstacleSpawner] Started spawning coroutine");
while (enabled && gameObject.activeInHierarchy && !_isPaused && !_isSurfacing)
{
// Calculate next spawn time with variation
float nextSpawnTime = _settings.ObstacleSpawnInterval +
Random.Range(-_settings.ObstacleSpawnIntervalVariation,
_settings.ObstacleSpawnIntervalVariation);
nextSpawnTime = Mathf.Max(0.1f, nextSpawnTime); // Ensure minimum interval
// Attempt to spawn an obstacle
TrySpawnObstacle();
yield return new WaitForSeconds(nextSpawnTime);
}
// Clear coroutine reference when stopped
_spawnCoroutine = null;
Debug.Log("[ObstacleSpawner] Spawning coroutine ended");
}
/// <summary>
/// Coroutine that handles checking obstacle positions
/// Unlike the previous implementation, we don't need to move obstacles manually
/// since the FloatingObstacle handles its own movement via coroutines
/// </summary>
private IEnumerator MoveObstaclesRoutine()
{
Debug.Log("[ObstacleSpawner] Started obstacle monitoring coroutine");
// This coroutine now just monitors obstacles, not moves them
while (enabled && gameObject.activeInHierarchy && !_isPaused)
{
// Clean up any null references in the active obstacles list
_activeObstacles.RemoveAll(obstacle => obstacle == null);
yield return new WaitForSeconds(0.5f);
}
// Clear coroutine reference when stopped
_moveCoroutine = null;
Debug.Log("[ObstacleSpawner] Obstacle monitoring coroutine ended");
}
/// <summary>
/// Coroutine that checks for obstacles that are off-screen and should be despawned
/// </summary>
private IEnumerator DespawnObstaclesRoutine()
{
const float checkInterval = 0.5f; // Check every half second
Debug.Log("[ObstacleSpawner] Started despawn coroutine with interval: " + checkInterval);
while (enabled && gameObject.activeInHierarchy && !_isPaused)
{
// Calculate screen bounds for despawning
float despawnBuffer = 2f; // Extra buffer beyond screen edges
if (_mainCamera == null)
{
_mainCamera = Camera.main;
if (_mainCamera == null)
{
yield return new WaitForSeconds(checkInterval);
continue;
}
}
Vector3 topWorldPoint = _mainCamera.ViewportToWorldPoint(new Vector3(0.5f, 1f, _mainCamera.transform.position.z));
float _screenTop = topWorldPoint.y;
Vector3 bottomWorldPoint = _mainCamera.ViewportToWorldPoint(new Vector3(0.5f, 0f, _mainCamera.transform.position.z));
float _screenBottom = bottomWorldPoint.y;
float topEdge = _screenTop + despawnBuffer;
float bottomEdge = _screenBottom - despawnBuffer;
// Find obstacles that need to be despawned
List<GameObject> obstaclesToRemove = new List<GameObject>();
foreach (var obstacle in _activeObstacles)
{
if (obstacle == null)
{
obstaclesToRemove.Add(obstacle);
continue;
}
// Check if obstacle is out of screen bounds
float obstacleY = obstacle.transform.position.y;
bool shouldDespawn;
if (_isSurfacing)
{
// When surfacing, despawn obstacles below the bottom edge
shouldDespawn = obstacleY < bottomEdge;
}
else
{
// When descending, despawn obstacles above the top edge
shouldDespawn = obstacleY > topEdge;
}
if (shouldDespawn)
{
obstaclesToRemove.Add(obstacle);
}
}
// Remove and despawn obstacles
foreach (var obstacle in obstaclesToRemove)
{
FloatingObstacle obstacleComponent = obstacle?.GetComponent<FloatingObstacle>();
if (obstacleComponent != null)
{
// Instead of calling a non-existent DespawnObstacle method,
// use FloatingObstacle's ForceReturnToPool method
obstacleComponent.ForceReturnToPool();
}
else
{
// Fallback if component not found
ReturnObstacleToPool(obstacle, -1);
}
}
yield return new WaitForSeconds(checkInterval);
}
// Clear coroutine reference when stopped
_despawnCoroutine = null;
Debug.Log("[ObstacleSpawner] Despawn coroutine ended");
}
#if UNITY_EDITOR #if UNITY_EDITOR
private void OnDrawGizmosSelected() private void OnDrawGizmosSelected()
{ {

View File

@@ -6,13 +6,14 @@ using UnityEngine.Serialization;
using Pooling; using Pooling;
using AppleHills.Core.Settings; using AppleHills.Core.Settings;
using Utils; using Utils;
using AppleHills.Core.Interfaces;
namespace Minigames.DivingForPictures namespace Minigames.DivingForPictures
{ {
/// <summary> /// <summary>
/// Spawns and manages trench wall tiles for the endless descender minigame. /// Spawns and manages trench wall tiles for the endless descender minigame.
/// </summary> /// </summary>
public class TrenchTileSpawner : MonoBehaviour public class TrenchTileSpawner : MonoBehaviour, IPausable
{ {
[Header("Tile Prefabs")] [Header("Tile Prefabs")]
[Tooltip("List of possible trench tile prefabs.")] [Tooltip("List of possible trench tile prefabs.")]
@@ -67,6 +68,12 @@ namespace Minigames.DivingForPictures
// Screen normalization // Screen normalization
private float _screenNormalizationFactor = 1.0f; private float _screenNormalizationFactor = 1.0f;
// Pause state
private bool _isPaused = false;
// IPausable implementation
public bool IsPaused => _isPaused;
private void Awake() private void Awake()
{ {
_mainCamera = Camera.main; _mainCamera = Camera.main;
@@ -124,6 +131,9 @@ namespace Minigames.DivingForPictures
{ {
gameManager.OnGameInitialized += Initialize; gameManager.OnGameInitialized += Initialize;
// Register with the DivingGameManager for pause/resume events
gameManager.RegisterPausableComponent(this);
// If game is already initialized, initialize immediately // If game is already initialized, initialize immediately
if (gameManager.GetType().GetField("_isGameInitialized", if (gameManager.GetType().GetField("_isGameInitialized",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.NonPublic |
@@ -139,6 +149,71 @@ namespace Minigames.DivingForPictures
} }
} }
private void OnDestroy()
{
// Unregister from DivingGameManager
DivingGameManager gameManager = FindFirstObjectByType<DivingGameManager>();
if (gameManager != null)
{
gameManager.UnregisterPausableComponent(this);
}
}
/// <summary>
/// Pauses the spawner and all associated processes
/// </summary>
public void Pause()
{
if (_isPaused) return; // Already paused
_isPaused = true;
// Stop all active coroutines but save their references
if (_movementCoroutine != null)
{
StopCoroutine(_movementCoroutine);
_movementCoroutine = null;
}
if (_tileDestructionCoroutine != null)
{
StopCoroutine(_tileDestructionCoroutine);
_tileDestructionCoroutine = null;
}
if (_tileSpawningCoroutine != null)
{
StopCoroutine(_tileSpawningCoroutine);
_tileSpawningCoroutine = null;
}
if (_speedRampingCoroutine != null)
{
StopCoroutine(_speedRampingCoroutine);
_speedRampingCoroutine = null;
}
Debug.Log("[TrenchTileSpawner] Paused");
}
/// <summary>
/// Resumes the spawner and all associated processes
/// </summary>
public void Resume()
{
if (!_isPaused) return; // Already running
_isPaused = false;
// Restart all necessary coroutines
StartMovementCoroutine();
StartTileDestructionCoroutine();
StartTileSpawningCoroutine();
StartSpeedRampingCoroutine();
Debug.Log("[TrenchTileSpawner] Resumed");
}
/// <summary> /// <summary>
/// Initializes the tile spawner when triggered by DivingGameManager /// Initializes the tile spawner when triggered by DivingGameManager
/// </summary> /// </summary>
@@ -385,25 +460,57 @@ namespace Minigames.DivingForPictures
} }
/// <summary> /// <summary>
/// Starts the movement coroutine and stores its reference /// Starts the movement coroutine if it's not already running
/// </summary> /// </summary>
private void StartMovementCoroutine() private void StartMovementCoroutine()
{ {
if (_movementCoroutine != null) if (_movementCoroutine == null && !_isPaused)
{ {
StopCoroutine(_movementCoroutine); _movementCoroutine = StartCoroutine(MoveActiveTilesRoutine());
} }
_movementCoroutine = StartCoroutine(MovementCoroutine());
} }
/// <summary>
/// Starts the tile destruction coroutine if it's not already running
/// </summary>
private void StartTileDestructionCoroutine()
{
if (_tileDestructionCoroutine == null && !_isPaused)
{
_tileDestructionCoroutine = StartCoroutine(TileDestructionRoutine());
}
}
/// <summary>
/// Starts the tile spawning coroutine if it's not already running
/// </summary>
private void StartTileSpawningCoroutine()
{
if (_tileSpawningCoroutine == null && !_isPaused && !_stopSpawning)
{
_tileSpawningCoroutine = StartCoroutine(TileSpawningRoutine());
}
}
/// <summary>
/// Starts the speed ramping coroutine if it's not already running
/// </summary>
private void StartSpeedRampingCoroutine()
{
if (_speedRampingCoroutine == null && !_isPaused)
{
_speedRampingCoroutine = StartCoroutine(SpeedRampingRoutine());
}
}
/// <summary> /// <summary>
/// Coroutine that handles obstacle movement using normalized screen-relative speed /// Coroutine that handles obstacle movement using normalized screen-relative speed
/// </summary> /// </summary>
private IEnumerator MovementCoroutine() private IEnumerator MoveActiveTilesRoutine()
{ {
Debug.Log($"[TrenchTileSpawner] Started movement coroutine with normalized speed: {_baseMoveSpeed:F3}"); Debug.Log($"[TrenchTileSpawner] Started movement coroutine with normalized speed: {_baseMoveSpeed:F3}");
while (enabled && gameObject.activeInHierarchy) while (enabled && gameObject.activeInHierarchy && !_isPaused)
{ {
// Skip if no active tiles // Skip if no active tiles
if (_activeTiles.Count == 0) if (_activeTiles.Count == 0)
@@ -431,167 +538,194 @@ namespace Minigames.DivingForPictures
// Wait for next frame // Wait for next frame
yield return null; yield return null;
} }
// Clear coroutine reference when stopped
_movementCoroutine = null;
}
/// <summary>
/// Coroutine that checks for tiles to destroy periodically
/// </summary>
private IEnumerator TileDestructionRoutine()
{
const float checkInterval = 0.5f; // Check every half second
Debug.Log($"[TrenchTileSpawner] Started tile destruction coroutine with interval: {checkInterval}s");
while (enabled && gameObject.activeInHierarchy && !_isPaused)
{
// Check and handle tile destruction
if (_activeTiles.Count > 0)
{
GameObject topTile = _activeTiles[0];
if (topTile == null)
{
_activeTiles.RemoveAt(0);
}
else
{
float tileHeight = GetTileHeight(topTile);
bool shouldDestroy;
if (_isSurfacing)
{
// When surfacing, destroy tiles at the bottom
shouldDestroy = topTile.transform.position.y + tileHeight / 2 < _screenBottom - _settings.TileSpawnBuffer;
}
else
{
// When descending, destroy tiles at the top
shouldDestroy = topTile.transform.position.y - tileHeight / 2 > _screenTop + _settings.TileSpawnBuffer;
}
if (shouldDestroy)
{
_activeTiles.RemoveAt(0);
onTileDestroyed?.Invoke(topTile);
if (_devSettings != null && _devSettings.TrenchTileUseObjectPooling && _tilePool != null)
{
// Find the prefab index for this tile
int prefabIndex = GetPrefabIndex(topTile);
if (prefabIndex >= 0)
{
_tilePool.ReturnTile(topTile, prefabIndex);
}
else
{
Destroy(topTile);
}
}
else
{
Destroy(topTile);
}
}
}
}
// Wait for the next check interval
yield return new WaitForSeconds(checkInterval);
}
// Clear coroutine reference when stopped
_tileDestructionCoroutine = null;
} }
/// <summary> /// <summary>
/// Handles the movement of all active tiles based on the current velocity /// Coroutine that checks if new tiles need to be spawned periodically
/// </summary> /// </summary>
private void HandleMovement() private IEnumerator TileSpawningRoutine()
{ {
foreach (var tile in _activeTiles) const float checkInterval = 0.2f; // Check every fifth of a second
{ Debug.Log($"[TrenchTileSpawner] Started tile spawning coroutine with interval: {checkInterval}s");
if (tile != null)
{
// Use velocity factor to determine direction
Vector3 direction = Vector3.up * Mathf.Sign(_velocityFactor);
float speed = _currentVelocity;
// Apply movement
tile.transform.position += direction * speed;
}
}
}
/// <summary>
/// Check for tiles that have moved off screen and should be destroyed or returned to pool
/// </summary>
private void HandleTileDestruction()
{
if (_activeTiles.Count == 0) return;
GameObject topTile = _activeTiles[0]; while (enabled && gameObject.activeInHierarchy && !_isPaused && !_stopSpawning)
if (topTile == null)
{ {
_activeTiles.RemoveAt(0); // Check if we need to spawn new tiles
return; if (_activeTiles.Count == 0)
}
float tileHeight = GetTileHeight(topTile);
bool shouldDestroy;
if (_isSurfacing)
{
// When surfacing, destroy tiles at the bottom
shouldDestroy = topTile.transform.position.y + tileHeight / 2 < _screenBottom - _settings.TileSpawnBuffer;
}
else
{
// When descending, destroy tiles at the top
shouldDestroy = topTile.transform.position.y - tileHeight / 2 > _screenTop + _settings.TileSpawnBuffer;
}
if (shouldDestroy)
{
_activeTiles.RemoveAt(0);
onTileDestroyed?.Invoke(topTile);
if (_devSettings != null && _devSettings.TrenchTileUseObjectPooling && _tilePool != null)
{ {
// Find the prefab index for this tile // If we have no active tiles and spawning is stopped, trigger the event
int prefabIndex = GetPrefabIndex(topTile); if (_stopSpawning)
if (prefabIndex >= 0)
{
_tilePool.ReturnTile(topTile, prefabIndex);
}
else
{
Destroy(topTile);
}
}
else
{
Destroy(topTile);
}
}
}
/// <summary>
/// Check if new tiles need to be spawned
/// </summary>
private void HandleTileSpawning()
{
if (_activeTiles.Count == 0)
{
// If we have no active tiles and spawning is stopped, trigger the event
if (_stopSpawning)
{
onLastTileLeft.Invoke();
}
return;
}
GameObject bottomTile = _activeTiles[^1];
if (bottomTile == null)
{
_activeTiles.RemoveAt(_activeTiles.Count - 1);
return;
}
// Get the tile height once to use in all calculations
float tileHeight = GetTileHeight(bottomTile);
// If we're in stop spawning mode, don't spawn new tiles
if (_stopSpawning)
{
// Check if this is the last tile, and if it's about to leave the screen
if (_activeTiles.Count == 1)
{
bool isLastTileLeaving;
if (_isSurfacing)
{
// When surfacing, check if the bottom of the tile is above the top of the screen
isLastTileLeaving = bottomTile.transform.position.y - tileHeight / 2 > _screenTop + _settings.TileSpawnBuffer;
}
else
{
// When descending, check if the top of the tile is below the bottom of the screen
isLastTileLeaving = bottomTile.transform.position.y + tileHeight / 2 < _screenBottom - _settings.TileSpawnBuffer;
}
if (isLastTileLeaving)
{ {
onLastTileLeft.Invoke(); onLastTileLeft.Invoke();
} }
} }
return; else
} {
GameObject bottomTile = _activeTiles[^1];
bool shouldSpawn; if (bottomTile == null)
float newY; {
_activeTiles.RemoveAt(_activeTiles.Count - 1);
if (_isSurfacing) }
{ else
// When surfacing, spawn new tiles at the top {
float topEdge = bottomTile.transform.position.y + tileHeight / 2; // Get the tile height once to use in all calculations
shouldSpawn = topEdge < _screenTop + _settings.TileSpawnBuffer; float tileHeight = GetTileHeight(bottomTile);
newY = bottomTile.transform.position.y + tileHeight;
} // If we're in stop spawning mode, check if last tile is leaving
else if (_stopSpawning)
{ {
// When descending, spawn new tiles at the bottom // Check if this is the last tile, and if it's about to leave the screen
float bottomEdge = bottomTile.transform.position.y - tileHeight / 2; if (_activeTiles.Count == 1)
shouldSpawn = bottomEdge > _screenBottom - _settings.TileSpawnBuffer; {
newY = bottomTile.transform.position.y - tileHeight; bool isLastTileLeaving;
if (_isSurfacing)
{
// When surfacing, check if bottom of tile is above top of screen
isLastTileLeaving = bottomTile.transform.position.y - tileHeight / 2 > _screenTop + _settings.TileSpawnBuffer;
}
else
{
// When descending, check if top of tile is below bottom of screen
isLastTileLeaving = bottomTile.transform.position.y + tileHeight / 2 < _screenBottom - _settings.TileSpawnBuffer;
}
if (isLastTileLeaving)
{
onLastTileLeft.Invoke();
}
}
}
else
{
// Normal spawning mode
bool shouldSpawn;
float newY;
if (_isSurfacing)
{
// When surfacing, spawn new tiles at the top
float topEdge = bottomTile.transform.position.y + tileHeight / 2;
shouldSpawn = topEdge < _screenTop + _settings.TileSpawnBuffer;
newY = bottomTile.transform.position.y + tileHeight;
}
else
{
// When descending, spawn new tiles at the bottom
float bottomEdge = bottomTile.transform.position.y - tileHeight / 2;
shouldSpawn = bottomEdge > _screenBottom - _settings.TileSpawnBuffer;
newY = bottomTile.transform.position.y - tileHeight;
}
if (shouldSpawn)
{
SpawnTileAtY(newY);
}
}
}
}
// Wait for the next check interval
yield return new WaitForSeconds(checkInterval);
} }
if (shouldSpawn) // Clear coroutine reference when stopped
{ _tileSpawningCoroutine = null;
SpawnTileAtY(newY);
}
} }
/// <summary> /// <summary>
/// Handle increasing the movement speed over time /// Coroutine that handles increasing the movement speed over time
/// </summary> /// </summary>
private void HandleSpeedRamping() private IEnumerator SpeedRampingRoutine()
{ {
_speedUpTimer += Time.deltaTime; const float checkInterval = 1.0f; // Check once per second
if (_speedUpTimer >= _settings.SpeedUpInterval) Debug.Log($"[TrenchTileSpawner] Started speed ramping coroutine with interval: {checkInterval}s");
while (enabled && gameObject.activeInHierarchy && !_isPaused)
{ {
// Increase the base move speed up to the maximum
_baseMoveSpeed = Mathf.Min(_baseMoveSpeed + _settings.SpeedUpFactor, _settings.MaxNormalizedMoveSpeed); _baseMoveSpeed = Mathf.Min(_baseMoveSpeed + _settings.SpeedUpFactor, _settings.MaxNormalizedMoveSpeed);
_speedUpTimer = 0f;
// Recalculate velocity with the new base speed
CalculateVelocity();
// Wait for the next check interval
yield return new WaitForSeconds(checkInterval);
} }
// Clear coroutine reference when stopped
_speedRampingCoroutine = null;
} }
/// <summary> /// <summary>
@@ -816,217 +950,5 @@ namespace Minigames.DivingForPictures
} }
} }
} }
/// <summary>
/// Starts the tile destruction coroutine and stores its reference
/// </summary>
private void StartTileDestructionCoroutine()
{
if (_tileDestructionCoroutine != null)
{
StopCoroutine(_tileDestructionCoroutine);
}
_tileDestructionCoroutine = StartCoroutine(TileDestructionCoroutine());
}
/// <summary>
/// Coroutine that checks for tiles to destroy periodically
/// </summary>
private IEnumerator TileDestructionCoroutine()
{
const float checkInterval = 0.5f; // Check every half second as requested
Debug.Log($"[TrenchTileSpawner] Started tile destruction coroutine with interval: {checkInterval}s");
while (enabled && gameObject.activeInHierarchy)
{
// Check and handle tile destruction
if (_activeTiles.Count > 0)
{
GameObject topTile = _activeTiles[0];
if (topTile == null)
{
_activeTiles.RemoveAt(0);
}
else
{
float tileHeight = GetTileHeight(topTile);
bool shouldDestroy;
if (_isSurfacing)
{
// When surfacing, destroy tiles at the bottom
shouldDestroy = topTile.transform.position.y + tileHeight / 2 < _screenBottom - _settings.TileSpawnBuffer;
}
else
{
// When descending, destroy tiles at the top
shouldDestroy = topTile.transform.position.y - tileHeight / 2 > _screenTop + _settings.TileSpawnBuffer;
}
if (shouldDestroy)
{
_activeTiles.RemoveAt(0);
onTileDestroyed?.Invoke(topTile);
if (_devSettings != null && _devSettings.TrenchTileUseObjectPooling && _tilePool != null)
{
// Find the prefab index for this tile
int prefabIndex = GetPrefabIndex(topTile);
if (prefabIndex >= 0)
{
_tilePool.ReturnTile(topTile, prefabIndex);
}
else
{
Destroy(topTile);
}
}
else
{
Destroy(topTile);
}
}
}
}
// Wait for the next check interval
yield return new WaitForSeconds(checkInterval);
}
}
/// <summary>
/// Starts the tile spawning coroutine and stores its reference
/// </summary>
private void StartTileSpawningCoroutine()
{
if (_tileSpawningCoroutine != null)
{
StopCoroutine(_tileSpawningCoroutine);
}
_tileSpawningCoroutine = StartCoroutine(TileSpawningCoroutine());
}
/// <summary>
/// Coroutine that checks if new tiles need to be spawned periodically
/// </summary>
private IEnumerator TileSpawningCoroutine()
{
const float checkInterval = 0.2f; // Check every half second as requested
Debug.Log($"[TrenchTileSpawner] Started tile spawning coroutine with interval: {checkInterval}s");
while (enabled && gameObject.activeInHierarchy)
{
// Check if we need to spawn new tiles
if (_activeTiles.Count == 0)
{
// If we have no active tiles and spawning is stopped, trigger the event
if (_stopSpawning)
{
onLastTileLeft.Invoke();
}
}
else
{
GameObject bottomTile = _activeTiles[^1];
if (bottomTile == null)
{
_activeTiles.RemoveAt(_activeTiles.Count - 1);
}
else
{
// Get the tile height once to use in all calculations
float tileHeight = GetTileHeight(bottomTile);
// If we're in stop spawning mode, check if last tile is leaving
if (_stopSpawning)
{
// Check if this is the last tile, and if it's about to leave the screen
if (_activeTiles.Count == 1)
{
bool isLastTileLeaving;
if (_isSurfacing)
{
// When surfacing, check if bottom of tile is above top of screen
isLastTileLeaving = bottomTile.transform.position.y - tileHeight / 2 > _screenTop + _settings.TileSpawnBuffer;
}
else
{
// When descending, check if top of tile is below bottom of screen
isLastTileLeaving = bottomTile.transform.position.y + tileHeight / 2 < _screenBottom - _settings.TileSpawnBuffer;
}
if (isLastTileLeaving)
{
onLastTileLeft.Invoke();
}
}
}
else
{
// Normal spawning mode
bool shouldSpawn;
float newY;
if (_isSurfacing)
{
// When surfacing, spawn new tiles at the top
float topEdge = bottomTile.transform.position.y + tileHeight / 2;
shouldSpawn = topEdge < _screenTop + _settings.TileSpawnBuffer;
newY = bottomTile.transform.position.y + tileHeight;
}
else
{
// When descending, spawn new tiles at the bottom
float bottomEdge = bottomTile.transform.position.y - tileHeight / 2;
shouldSpawn = bottomEdge > _screenBottom - _settings.TileSpawnBuffer;
newY = bottomTile.transform.position.y - tileHeight;
}
if (shouldSpawn)
{
SpawnTileAtY(newY);
}
}
}
}
// Wait for the next check interval
yield return new WaitForSeconds(checkInterval);
}
}
/// <summary>
/// Starts the speed ramping coroutine and stores its reference
/// </summary>
private void StartSpeedRampingCoroutine()
{
if (_speedRampingCoroutine != null)
{
StopCoroutine(_speedRampingCoroutine);
}
_speedRampingCoroutine = StartCoroutine(SpeedRampingCoroutine());
}
/// <summary>
/// Coroutine that handles increasing the movement speed over time
/// </summary>
private IEnumerator SpeedRampingCoroutine()
{
const float checkInterval = 1.0f; // Check once per second as requested
Debug.Log($"[TrenchTileSpawner] Started speed ramping coroutine with interval: {checkInterval}s");
while (enabled && gameObject.activeInHierarchy)
{
// Increase the base move speed up to the maximum
_baseMoveSpeed = Mathf.Min(_baseMoveSpeed + _settings.SpeedUpFactor, _settings.MaxNormalizedMoveSpeed);
// Recalculate velocity with the new base speed
CalculateVelocity();
// Wait for the next check interval
yield return new WaitForSeconds(checkInterval);
}
}
} }
} }

View File

@@ -35,6 +35,13 @@ namespace UI
public event Action OnGamePaused; public event Action OnGamePaused;
public event Action OnGameResumed; public event Action OnGameResumed;
private bool _isPaused = false;
/// <summary>
/// Returns whether the game is currently paused
/// </summary>
public bool IsPaused => _isPaused;
private void Start() private void Start()
{ {
// Subscribe to scene loaded events // Subscribe to scene loaded events
@@ -92,6 +99,12 @@ namespace UI
// Change input mode to UI when menu is open // Change input mode to UI when menu is open
InputManager.Instance.SetInputMode(InputMode.UI); InputManager.Instance.SetInputMode(InputMode.UI);
// Set paused flag and broadcast event
_isPaused = true;
OnGamePaused?.Invoke();
Debug.Log("[PauseMenu] Game Paused");
} }
/// <summary> /// <summary>
@@ -108,6 +121,12 @@ namespace UI
// Change input mode back to Game when menu is closed // Change input mode back to Game when menu is closed
if(resetInput) if(resetInput)
InputManager.Instance.SetInputMode(InputMode.Game); InputManager.Instance.SetInputMode(InputMode.Game);
// Clear paused flag and broadcast event
_isPaused = false;
OnGameResumed?.Invoke();
Debug.Log("[PauseMenu] Game Resumed");
} }
/// <summary> /// <summary>