Files
AppleHillsProduction/Assets/Scripts/Minigames/DivingForPictures/TrenchTileSpawner.cs

674 lines
23 KiB
C#

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Serialization;
using Pooling;
namespace Minigames.DivingForPictures
{
/// <summary>
/// Spawns and manages trench wall tiles for the endless descender minigame.
/// </summary>
public class TrenchTileSpawner : MonoBehaviour
{
[Header("Tile Prefabs")]
[Tooltip("List of possible trench tile prefabs.")]
[SerializeField] private List<GameObject> 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<GameObject> onTileSpawned;
[FormerlySerializedAs("OnTileDestroyed")]
public UnityEvent<GameObject> onTileDestroyed;
// Private fields
private readonly Dictionary<GameObject, float> _tileHeights = new Dictionary<GameObject, float>();
private readonly List<GameObject> _activeTiles = new List<GameObject>();
private readonly Dictionary<int, int> _tileLastUsed = new Dictionary<int, int>();
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;
// Interval for velocity calculations (seconds)
[SerializeField] private float velocityCalculationInterval = 0.5f;
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;
private void Awake()
{
_mainCamera = Camera.main;
_baseMoveSpeed = moveSpeed; // Store the original base speed
// 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<Tile>() == 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<Tile>();
}
}
}
private void Start()
{
CalculateScreenBounds();
SpawnInitialTiles();
// Initialize velocity and start the velocity calculation coroutine
_currentVelocity = moveSpeed * Time.fixedDeltaTime;
StartCoroutine(VelocityCalculationRoutine());
}
private void Update()
{
HandleMovement();
HandleTileDestruction();
HandleTileSpawning();
HandleSpeedRamping();
}
/// <summary>
/// Gets the current velocity of the tiles
/// </summary>
/// <returns>The current upward velocity of the tiles</returns>
public float GetCurrentVelocity()
{
return _currentVelocity;
}
/// <summary>
/// Sets a custom velocity, overriding the calculated one
/// </summary>
/// <param name="velocity">The new velocity value</param>
public void SetVelocity(float velocity)
{
_currentVelocity = velocity;
}
/// <summary>
/// Coroutine that periodically calculates the velocity based on game state
/// </summary>
private IEnumerator VelocityCalculationRoutine()
{
while (true)
{
CalculateVelocity();
yield return new WaitForSeconds(velocityCalculationInterval);
}
}
/// <summary>
/// Calculates the current velocity based on move speed
/// </summary>
private void CalculateVelocity()
{
_currentVelocity = moveSpeed * Time.fixedDeltaTime;
}
/// <summary>
/// Calculate height values for all tile prefabs
/// </summary>
private void CalculateTileHeights()
{
foreach (var prefab in tilePrefabs)
{
if (prefab == null)
{
Debug.LogError("Null prefab found in tilePrefabs list!");
continue;
}
Renderer renderer = prefab.GetComponentInChildren<Renderer>();
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}.");
}
}
}
/// <summary>
/// Initialize the object pool system
/// </summary>
private void InitializeObjectPool()
{
// Create the tile pool
GameObject poolGO = new GameObject("TrenchTilePool");
poolGO.transform.SetParent(transform);
_tilePool = poolGO.AddComponent<TrenchTilePool>();
// Set up the pool configuration
_tilePool.maxPerPrefabPoolSize = maxPerPrefabPoolSize;
_tilePool.totalMaxPoolSize = totalMaxPoolSize;
// Convert the GameObject list to a Tile list
List<Tile> prefabTiles = new List<Tile>(tilePrefabs.Count);
foreach (var prefab in tilePrefabs)
{
if (prefab != null)
{
Tile tileComponent = prefab.GetComponent<Tile>();
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);
}
/// <summary>
/// Spawn the initial set of tiles
/// </summary>
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 < 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);
}
}
/// <summary>
/// Calculate the screen bounds in world space
/// </summary>
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;
}
/// <summary>
/// Called when the velocity factor changes from the DivingGameManager
/// </summary>
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
moveSpeed = _baseMoveSpeed * Mathf.Abs(_velocityFactor);
// Recalculate velocity immediately
CalculateVelocity();
Debug.Log($"[TrenchTileSpawner] Velocity factor updated to {_velocityFactor:F2}, moveSpeed: {moveSpeed:F2}");
}
/// <summary>
/// Reverses direction to start surfacing
/// </summary>
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");
}
/// <summary>
/// Stops spawning new tiles
/// </summary>
public void StopSpawning()
{
_stopSpawning = true;
}
/// <summary>
/// Handles the movement of all active tiles based on the current velocity
/// </summary>
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;
}
}
}
/// <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];
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 - tileSpawnBuffer;
}
else
{
// When descending, destroy tiles at the top
shouldDestroy = topTile.transform.position.y - tileHeight / 2 > _screenTop + tileSpawnBuffer;
}
if (shouldDestroy)
{
_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);
}
}
}
/// <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 + 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 - 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 + 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 - tileSpawnBuffer;
newY = bottomTile.transform.position.y - tileHeight;
}
if (shouldSpawn)
{
SpawnTileAtY(newY);
}
}
/// <summary>
/// Handle increasing the movement speed over time
/// </summary>
private void HandleSpeedRamping()
{
_speedUpTimer += Time.deltaTime;
if (_speedUpTimer >= speedUpInterval)
{
moveSpeed = Mathf.Min(moveSpeed + speedUpFactor, maxMoveSpeed);
_speedUpTimer = 0f;
}
}
/// <summary>
/// Spawn a new tile at the specified Y position
/// </summary>
/// <param name="y">The Y position to spawn at</param>
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);
}
/// <summary>
/// Gets a weighted random tile index, favoring tiles that haven't been used recently
/// </summary>
/// <returns>The selected prefab index</returns>
private int GetWeightedRandomTileIndex()
{
int prefabCount = tilePrefabs.Count;
List<float> weights = new List<float>(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
}
/// <summary>
/// Gets the height of a tile based on its prefab or renderer bounds
/// </summary>
/// <param name="tile">The tile to measure</param>
/// <returns>The height of the tile</returns>
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<Renderer>();
if (renderer != null)
{
return renderer.bounds.size.y;
}
// Fallback
return DefaultTileHeight;
}
/// <summary>
/// Gets the index of the prefab that was used to create this tile
/// </summary>
/// <param name="tile">The tile to check</param>
/// <returns>The index of the prefab or -1 if not found</returns>
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;
}
/// <summary>
/// Called periodically to trim excess pooled tiles
/// </summary>
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
}
}