using System.Collections.Generic; using UnityEngine; using UnityEngine.Events; using UnityEngine.Serialization; using Pooling; 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("Tile Settings")] [SerializeField] private int initialTileCount = 3; [SerializeField] private float tileSpawnBuffer = 1f; [Header("Movement Settings")] [SerializeField] private float moveSpeed = 3f; [SerializeField] private float speedUpFactor = 0.2f; [SerializeField] private float speedUpInterval = 10f; [SerializeField] private float maxMoveSpeed = 12f; [Header("Object Pooling")] [SerializeField] private bool useObjectPooling = true; [SerializeField] private int maxPerPrefabPoolSize = 2; [SerializeField] private int totalMaxPoolSize = 10; [Header("Events")] [FormerlySerializedAs("OnTileSpawned")] public UnityEvent onTileSpawned; [FormerlySerializedAs("OnTileDestroyed")] public UnityEvent onTileDestroyed; // 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; private const float TileSpawnZ = -1f; private const float DefaultTileHeight = 5f; private void Awake() { _mainCamera = Camera.main; // Calculate tile heights for each prefab CalculateTileHeights(); // Ensure all prefabs have Tile components ValidateTilePrefabs(); if (useObjectPooling) { 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() { CalculateScreenBounds(); SpawnInitialTiles(); } private void Update() { MoveTiles(); HandleTileDestruction(); HandleTileSpawning(); HandleSpeedRamping(); } /// /// 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 _tilePool.maxPerPrefabPoolSize = maxPerPrefabPoolSize; _tilePool.totalMaxPoolSize = totalMaxPoolSize; // 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() { for (int i = 0; i < initialTileCount; i++) { float y = _screenBottom; // 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; } /// /// Move all active tiles upward /// private void MoveTiles() { float moveDelta = moveSpeed * Time.deltaTime; foreach (var tile in _activeTiles) { if (tile != null) { tile.transform.position += Vector3.up * moveDelta; } } } /// /// 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); if (topTile.transform.position.y - tileHeight / 2 > _screenTop + tileSpawnBuffer) { _activeTiles.RemoveAt(0); onTileDestroyed?.Invoke(topTile); if (useObjectPooling && _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) return; GameObject bottomTile = _activeTiles[^1]; if (bottomTile == null) { _activeTiles.RemoveAt(_activeTiles.Count - 1); return; } float tileHeight = GetTileHeight(bottomTile); float bottomEdge = bottomTile.transform.position.y - tileHeight / 2; if (bottomEdge > _screenBottom - tileSpawnBuffer) { float newY = bottomTile.transform.position.y - tileHeight; SpawnTileAtY(newY); } } /// /// Handle increasing the movement speed over time /// private void HandleSpeedRamping() { _speedUpTimer += Time.deltaTime; if (_speedUpTimer >= speedUpInterval) { moveSpeed = Mathf.Min(moveSpeed + speedUpFactor, maxMoveSpeed); _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 (useObjectPooling && _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); } else { // Use the prefab's original rotation tile = Instantiate(prefab, new Vector3(0f, y, TileSpawnZ), prefab.transform.rotation, transform); } _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; for (int i = 0; i < 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 } }