using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Events; using UnityEngine.Serialization; using Pooling; using AppleHills.Core.Settings; namespace Minigames.DivingForPictures { /// /// Spawns and manages trench wall tiles for the endless descender minigame. /// public class TrenchTileSpawner : MonoBehaviour { [Header("Tile Prefabs")] [Tooltip("List of possible trench tile prefabs.")] [SerializeField] private List tilePrefabs; [Header("Events")] [FormerlySerializedAs("OnTileSpawned")] public UnityEvent onTileSpawned; [FormerlySerializedAs("OnTileDestroyed")] public UnityEvent onTileDestroyed; // Settings references private IDivingMinigameSettings _settings; private DivingDeveloperSettings _devSettings; // Private fields private readonly Dictionary _tileHeights = new Dictionary(); private readonly List _activeTiles = new List(); private readonly Dictionary _tileLastUsed = new Dictionary(); private int _spawnCounter; private float _speedUpTimer; private Camera _mainCamera; private float _screenBottom; private float _screenTop; private TrenchTilePool _tilePool; // Current velocity for tile movement private float _currentVelocity; private const float TileSpawnZ = -1f; private const float DefaultTileHeight = 5f; // Direction state private bool _isSurfacing = false; private bool _stopSpawning = false; // Event triggered when the last tile leaves the screen after stopping spawning public UnityEvent onLastTileLeft = new UnityEvent(); // Velocity management private float _baseMoveSpeed; private float _velocityFactor = 1.0f; // Coroutine references private Coroutine _movementCoroutine; private Coroutine _tileDestructionCoroutine; private Coroutine _tileSpawningCoroutine; private Coroutine _speedRampingCoroutine; // Screen normalization private float _screenNormalizationFactor = 1.0f; private void Awake() { _mainCamera = Camera.main; // Get settings from GameManager _settings = GameManager.GetSettingsObject(); _devSettings = GameManager.GetDeveloperSettings(); if (_settings == null) { Debug.LogError("[TrenchTileSpawner] Failed to load diving minigame settings!"); } if (_devSettings == null) { Debug.LogError("[TrenchTileSpawner] Failed to load diving developer settings!"); } _baseMoveSpeed = _settings?.NormalizedMoveSpeed ?? 3f; // Store the original base speed // Calculate tile heights for each prefab CalculateTileHeights(); // Ensure all prefabs have Tile components ValidateTilePrefabs(); if (_devSettings != null && _devSettings.TrenchTileUseObjectPooling) { InitializeObjectPool(); } } // Validate that all prefabs have Tile components private void ValidateTilePrefabs() { for (int i = 0; i < tilePrefabs.Count; i++) { if (tilePrefabs[i] == null) continue; // Check if the prefab has a Tile component if (tilePrefabs[i].GetComponent() == null) { Debug.LogWarning($"Prefab {tilePrefabs[i].name} does not have a Tile component. Adding one automatically."); // Add the Tile component if it doesn't exist tilePrefabs[i].AddComponent(); } } } private void Start() { // Find DivingGameManager and subscribe to its initialization event DivingGameManager gameManager = FindFirstObjectByType(); if (gameManager != null) { gameManager.OnGameInitialized += Initialize; // If game is already initialized, initialize immediately if (gameManager.GetType().GetField("_isGameInitialized", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?.GetValue(gameManager) is bool isInitialized && isInitialized) { Initialize(); } } else { Debug.LogWarning("[TrenchTileSpawner] DivingGameManager not found. Initializing immediately."); Initialize(); } } /// /// Initializes the tile spawner when triggered by DivingGameManager /// private void Initialize() { // Calculate screen bounds and normalization factor CalculateScreenBounds(); CalculateScreenNormalizationFactor(); // Spawn initial tiles to fill the screen SpawnInitialTiles(); // Initialize velocity and apply screen normalization _baseMoveSpeed = _settings?.NormalizedMoveSpeed ?? 3f; _currentVelocity = _baseMoveSpeed * Time.fixedDeltaTime * _screenNormalizationFactor; // Start all coroutines StartCoroutine(VelocityCalculationRoutine()); StartMovementCoroutine(); StartTileDestructionCoroutine(); StartTileSpawningCoroutine(); StartSpeedRampingCoroutine(); Debug.Log("[TrenchTileSpawner] Initialized with normalized speed"); } /// /// Gets the current velocity of the tiles /// /// The current upward velocity of the tiles public float GetCurrentVelocity() { return _currentVelocity; } /// /// Sets a custom velocity, overriding the calculated one /// /// The new velocity value public void SetVelocity(float velocity) { _currentVelocity = velocity; } /// /// Coroutine that periodically calculates the velocity based on game state /// private IEnumerator VelocityCalculationRoutine() { while (true) { CalculateVelocity(); yield return new WaitForSeconds(_settings.VelocityCalculationInterval); } } /// /// Calculates the current velocity based on move speed /// private void CalculateVelocity() { _currentVelocity = _baseMoveSpeed * Time.fixedDeltaTime; } /// /// Calculate height values for all tile prefabs /// private void CalculateTileHeights() { foreach (var prefab in tilePrefabs) { if (prefab == null) { Debug.LogError("Null prefab found in tilePrefabs list!"); continue; } Renderer renderer = prefab.GetComponentInChildren(); if (renderer != null) { _tileHeights[prefab] = renderer.bounds.size.y; } else { // Fallback in case no renderer is found _tileHeights[prefab] = DefaultTileHeight; Debug.LogWarning($"No renderer found in prefab {prefab.name}. Using default height of {DefaultTileHeight}."); } } } /// /// Initialize the object pool system /// private void InitializeObjectPool() { // Create the tile pool GameObject poolGO = new GameObject("TrenchTilePool"); poolGO.transform.SetParent(transform); _tilePool = poolGO.AddComponent(); // Set up the pool configuration using developer settings _tilePool.maxPerPrefabPoolSize = _devSettings.TrenchTileMaxPerPrefabPoolSize; _tilePool.totalMaxPoolSize = _devSettings.TrenchTileTotalMaxPoolSize; // Convert the GameObject list to a Tile list List prefabTiles = new List(tilePrefabs.Count); foreach (var prefab in tilePrefabs) { if (prefab != null) { Tile tileComponent = prefab.GetComponent(); if (tileComponent != null) { prefabTiles.Add(tileComponent); } else { Debug.LogError($"Prefab {prefab.name} is missing a Tile component!"); } } } // Initialize the pool with the tile component list _tilePool.Initialize(prefabTiles); // Periodically trim the pool to optimize memory usage InvokeRepeating(nameof(TrimExcessPooledTiles), 10f, 30f); } /// /// Spawn the initial set of tiles /// private void SpawnInitialTiles() { // Calculate starting Y position - moved 2 tiles up from the bottom of the screen float startingY = _screenBottom; // If we have prefab tiles with known heights, use their average height for offset calculation float tileHeightEstimate = DefaultTileHeight; if (tilePrefabs != null && tilePrefabs.Count > 0 && tilePrefabs[0] != null) { if (_tileHeights.TryGetValue(tilePrefabs[0], out float height)) { tileHeightEstimate = height; } } // Move starting position up by 2 tile heights startingY += tileHeightEstimate * 2; for (int i = 0; i < _settings.InitialTileCount; i++) { float y = startingY; // Calculate proper Y position based on previous tiles if (i > 0 && _activeTiles.Count > 0) { GameObject prevTile = _activeTiles[_activeTiles.Count - 1]; float prevHeight = GetTileHeight(prevTile); y = prevTile.transform.position.y - prevHeight; } SpawnTileAtY(y); } } /// /// Calculate the screen bounds in world space /// private void CalculateScreenBounds() { if (_mainCamera == null) { _mainCamera = Camera.main; if (_mainCamera == null) { Debug.LogError("No main camera found!"); return; } } Vector3 bottom = _mainCamera.ViewportToWorldPoint(new Vector3(0.5f, 0f, _mainCamera.nearClipPlane)); Vector3 top = _mainCamera.ViewportToWorldPoint(new Vector3(0.5f, 1f, _mainCamera.nearClipPlane)); _screenBottom = bottom.y; _screenTop = top.y; } /// /// 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; Debug.Log($"[TrenchTileSpawner] Screen normalization factor: {_screenNormalizationFactor} (Screen height: {Screen.height}, Reference: {referenceHeight})"); } /// /// Called when the velocity factor changes from the DivingGameManager /// public void OnVelocityFactorChanged(float velocityFactor) { _velocityFactor = velocityFactor; // Update the actual move speed based on the velocity factor // This keeps the original move speed intact for game logic _baseMoveSpeed = _settings.NormalizedMoveSpeed * Mathf.Abs(_velocityFactor); // Recalculate velocity immediately CalculateVelocity(); Debug.Log($"[TrenchTileSpawner] Velocity factor updated to {_velocityFactor:F2}, moveSpeed: {_baseMoveSpeed:F2}"); } /// /// Reverses direction to start surfacing /// public void StartSurfacing() { if (_isSurfacing) return; // Already surfacing // Set surfacing flag for spawn/despawn logic _isSurfacing = true; // Reverse the active tiles array to maintain consistent indexing logic _activeTiles.Reverse(); Debug.Log("[TrenchTileSpawner] Started surfacing - reversed array order"); } /// /// Stops spawning new tiles /// public void StopSpawning() { _stopSpawning = true; } /// /// Starts the movement coroutine and stores its reference /// private void StartMovementCoroutine() { if (_movementCoroutine != null) { StopCoroutine(_movementCoroutine); } _movementCoroutine = StartCoroutine(MovementCoroutine()); } /// /// Coroutine that handles obstacle movement using normalized screen-relative speed /// private IEnumerator MovementCoroutine() { Debug.Log($"[TrenchTileSpawner] Started movement coroutine with normalized speed: {_currentVelocity:F3}"); while (enabled && gameObject.activeInHierarchy) { // Skip if no active tiles if (_activeTiles.Count == 0) { yield return null; continue; } // Use velocity factor sign to determine direction Vector3 direction = Vector3.up * Mathf.Sign(_velocityFactor); // Apply normalized movement with deltaTime for frame rate independence float speed = _currentVelocity * _screenNormalizationFactor; // Move all active tiles foreach (var tile in _activeTiles) { if (tile != null) { // Apply movement in correct direction tile.transform.position += direction * speed; } } // Wait for next frame yield return null; } } /// /// Handles the movement of all active tiles based on the current velocity /// private void HandleMovement() { foreach (var tile in _activeTiles) { 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; } } } /// /// Check for tiles that have moved off screen and should be destroyed or returned to pool /// private void HandleTileDestruction() { if (_activeTiles.Count == 0) return; GameObject topTile = _activeTiles[0]; if (topTile == null) { _activeTiles.RemoveAt(0); return; } 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); } } } /// /// Check if new tiles need to be spawned /// 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(); } } return; } 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); } } /// /// Handle increasing the movement speed over time /// private void HandleSpeedRamping() { _speedUpTimer += Time.deltaTime; if (_speedUpTimer >= _settings.SpeedUpInterval) { _baseMoveSpeed = Mathf.Min(_baseMoveSpeed + _settings.SpeedUpFactor, _settings.MaxNormalizedMoveSpeed); _speedUpTimer = 0f; } } /// /// Spawn a new tile at the specified Y position /// /// The Y position to spawn at private void SpawnTileAtY(float y) { if (tilePrefabs == null || tilePrefabs.Count == 0) { Debug.LogError("No tile prefabs available for spawning!"); return; } int prefabIndex = GetWeightedRandomTileIndex(); GameObject prefab = tilePrefabs[prefabIndex]; if (prefab == null) { Debug.LogError($"Tile prefab at index {prefabIndex} is null!"); return; } GameObject tile; if (_devSettings != null && _devSettings.TrenchTileUseObjectPooling && _tilePool != null) { tile = _tilePool.GetTile(prefabIndex); if (tile == null) { Debug.LogError("Failed to get tile from pool!"); return; } tile.transform.position = new Vector3(0f, y, TileSpawnZ); tile.transform.rotation = prefab.transform.rotation; tile.transform.SetParent(transform); // Set the layer to the configured trench tile layer if (_devSettings != null) { tile.layer = _devSettings.TrenchTileLayer; SetLayerRecursively(tile, _devSettings.TrenchTileLayer); } } else { // Use the prefab's original rotation tile = Instantiate(prefab, new Vector3(0f, y, TileSpawnZ), prefab.transform.rotation, transform); // Set the layer to the configured trench tile layer if (_devSettings != null) { tile.layer = _devSettings.TrenchTileLayer; SetLayerRecursively(tile, _devSettings.TrenchTileLayer); } } _activeTiles.Add(tile); _tileLastUsed[prefabIndex] = _spawnCounter++; onTileSpawned?.Invoke(tile); } /// /// Gets a weighted random tile index, favoring tiles that haven't been used recently /// /// The selected prefab index private int GetWeightedRandomTileIndex() { int prefabCount = tilePrefabs.Count; List weights = new List(prefabCount); for (int i = 0; i < prefabCount; i++) { int lastUsed = _tileLastUsed.TryGetValue(i, out var value) ? value : -prefabCount; int age = _spawnCounter - lastUsed; float weight = Mathf.Clamp(age, 1, prefabCount * 2); // More unused = higher weight weights.Add(weight); } float totalWeight = 0f; foreach (var weight in weights) { totalWeight += weight; } float randomValue = Random.value * totalWeight; for (int i = 0; i < prefabCount; i++) { if (randomValue < weights[i]) { return i; } randomValue -= weights[i]; } return Random.Range(0, prefabCount); // fallback } /// /// Gets the height of a tile based on its prefab or renderer bounds /// /// The tile to measure /// The height of the tile private float GetTileHeight(GameObject tile) { if (tile == null) { Debug.LogWarning("Attempted to get height of null tile!"); return DefaultTileHeight; } // Check if this is a known prefab foreach (var prefab in tilePrefabs) { if (prefab == null) continue; // Check if this tile was created from this prefab if (tile.name.StartsWith(prefab.name)) { if (_tileHeights.TryGetValue(prefab, out float height)) { return height; } } } // If not found, calculate it from the renderer Renderer renderer = tile.GetComponentInChildren(); if (renderer != null) { return renderer.bounds.size.y; } // Fallback return DefaultTileHeight; } /// /// Gets the index of the prefab that was used to create this tile /// /// The tile to check /// The index of the prefab or -1 if not found private int GetPrefabIndex(GameObject tile) { if (tile == null || tilePrefabs == null) { return -1; } for (int i = 0; i < tilePrefabs.Count; i++) { if (tilePrefabs[i] == null) continue; if (tile.name.StartsWith(tilePrefabs[i].name)) { return i; } } return -1; } /// /// Called periodically to trim excess pooled tiles /// private void TrimExcessPooledTiles() { if (_tilePool != null) { _tilePool.TrimExcess(); } } #if UNITY_EDITOR private void OnDrawGizmosSelected() { if (!Application.isPlaying) { // Only try to calculate this if _screenBottom hasn't been set by the game Camera editorCam = Camera.main; if (editorCam != null) { Vector3 bottom = editorCam.ViewportToWorldPoint(new Vector3(0.5f, 0f, editorCam.nearClipPlane)); _screenBottom = bottom.y; } } // Draw tile bounds for debugging Gizmos.color = Color.cyan; if (_settings != null) { for (int i = 0; i < _settings.InitialTileCount; i++) { float height = DefaultTileHeight; if (tilePrefabs != null && tilePrefabs.Count > 0 && tilePrefabs[0] != null && _tileHeights.TryGetValue(tilePrefabs[0], out float h)) { height = h; } Vector3 center = new Vector3(0f, _screenBottom + i * height, 0f); Gizmos.DrawWireCube(center, new Vector3(10f, height, 1f)); } } } #endif /// /// Set the layer of a GameObject and all its children recursively /// /// The GameObject to set the layer for /// The layer index to set private void SetLayerRecursively(GameObject obj, int layer) { if (obj == null) return; obj.layer = layer; foreach (Transform child in obj.transform) { if (child != null) { SetLayerRecursively(child.gameObject, layer); } } } /// /// Starts the tile destruction coroutine and stores its reference /// private void StartTileDestructionCoroutine() { if (_tileDestructionCoroutine != null) { StopCoroutine(_tileDestructionCoroutine); } _tileDestructionCoroutine = StartCoroutine(TileDestructionCoroutine()); } /// /// Coroutine that checks for tiles to destroy periodically /// 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); } } /// /// Starts the tile spawning coroutine and stores its reference /// private void StartTileSpawningCoroutine() { if (_tileSpawningCoroutine != null) { StopCoroutine(_tileSpawningCoroutine); } _tileSpawningCoroutine = StartCoroutine(TileSpawningCoroutine()); } /// /// Coroutine that checks if new tiles need to be spawned periodically /// 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); } } /// /// Starts the speed ramping coroutine and stores its reference /// private void StartSpeedRampingCoroutine() { if (_speedRampingCoroutine != null) { StopCoroutine(_speedRampingCoroutine); } _speedRampingCoroutine = StartCoroutine(SpeedRampingCoroutine()); } /// /// Coroutine that handles increasing the movement speed over time /// 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); } } } }