453 lines
16 KiB
C#
453 lines
16 KiB
C#
|
|
using System.Collections;
|
|||
|
|
using System.Collections.Generic;
|
|||
|
|
using UnityEngine;
|
|||
|
|
using UnityEngine.Events;
|
|||
|
|
using Pooling;
|
|||
|
|
|
|||
|
|
namespace Minigames.DivingForPictures
|
|||
|
|
{
|
|||
|
|
/// <summary>
|
|||
|
|
/// Spawns and manages mobile obstacles for the diving minigame.
|
|||
|
|
/// Uses object pooling and validates spawn positions to avoid colliding with tiles.
|
|||
|
|
/// </summary>
|
|||
|
|
public class ObstacleSpawner : MonoBehaviour
|
|||
|
|
{
|
|||
|
|
[Header("Obstacle Prefabs")]
|
|||
|
|
[Tooltip("List of possible obstacle prefabs to spawn")]
|
|||
|
|
[SerializeField] private List<GameObject> obstaclePrefabs;
|
|||
|
|
|
|||
|
|
[Header("Spawn Settings")]
|
|||
|
|
[Tooltip("Time interval between spawn attempts (in seconds)")]
|
|||
|
|
[SerializeField] private float spawnInterval = 2f;
|
|||
|
|
|
|||
|
|
[Tooltip("Random variation in spawn timing (+/- seconds)")]
|
|||
|
|
[SerializeField] private float spawnIntervalVariation = 0.5f;
|
|||
|
|
|
|||
|
|
[Tooltip("Maximum number of spawn position attempts before skipping")]
|
|||
|
|
[SerializeField] private int maxSpawnAttempts = 10;
|
|||
|
|
|
|||
|
|
[Tooltip("Radius around spawn point to check for tile collisions")]
|
|||
|
|
[SerializeField] private float spawnCollisionRadius = 1f;
|
|||
|
|
|
|||
|
|
[Header("Spawn Position")]
|
|||
|
|
[Tooltip("How far below screen to spawn obstacles")]
|
|||
|
|
[SerializeField] private float spawnDistanceBelowScreen = 2f;
|
|||
|
|
|
|||
|
|
[Tooltip("Horizontal spawn range (distance from center)")]
|
|||
|
|
[SerializeField] private float spawnRangeX = 8f;
|
|||
|
|
|
|||
|
|
[Header("Obstacle Properties Randomization")]
|
|||
|
|
[Tooltip("Minimum movement speed for spawned obstacles")]
|
|||
|
|
[SerializeField] private float minMoveSpeed = 1f;
|
|||
|
|
|
|||
|
|
[Tooltip("Maximum movement speed for spawned obstacles")]
|
|||
|
|
[SerializeField] private float maxMoveSpeed = 4f;
|
|||
|
|
|
|||
|
|
[Tooltip("Minimum damage dealt by obstacles")]
|
|||
|
|
[SerializeField] private float minDamage = 0.5f;
|
|||
|
|
|
|||
|
|
[Tooltip("Maximum damage dealt by obstacles")]
|
|||
|
|
[SerializeField] private float maxDamage = 2f;
|
|||
|
|
|
|||
|
|
[Header("Object Pooling")]
|
|||
|
|
[Tooltip("Whether to use object pooling for obstacles")]
|
|||
|
|
[SerializeField] private bool useObjectPooling = true;
|
|||
|
|
|
|||
|
|
[Tooltip("Maximum objects per prefab type in pool")]
|
|||
|
|
[SerializeField] private int maxPerPrefabPoolSize = 3;
|
|||
|
|
|
|||
|
|
[Tooltip("Total maximum pool size across all prefab types")]
|
|||
|
|
[SerializeField] private int totalMaxPoolSize = 15;
|
|||
|
|
|
|||
|
|
[Header("Layer Settings")]
|
|||
|
|
[Tooltip("Layer mask for tile collision detection - should match WorldObstacle layer")]
|
|||
|
|
[SerializeField] private LayerMask tileLayerMask = 1 << 6; // WorldObstacle layer
|
|||
|
|
|
|||
|
|
[Header("Events")]
|
|||
|
|
[Tooltip("Called when an obstacle is spawned")]
|
|||
|
|
public UnityEvent<GameObject> onObstacleSpawned;
|
|||
|
|
|
|||
|
|
[Tooltip("Called when an obstacle is returned to pool")]
|
|||
|
|
public UnityEvent<GameObject> onObstacleDestroyed;
|
|||
|
|
|
|||
|
|
// Private fields
|
|||
|
|
private ObstaclePool _obstaclePool;
|
|||
|
|
private Camera _mainCamera;
|
|||
|
|
private float _screenBottom;
|
|||
|
|
private Coroutine _spawnCoroutine;
|
|||
|
|
private readonly List<GameObject> _activeObstacles = new List<GameObject>();
|
|||
|
|
|
|||
|
|
private void Awake()
|
|||
|
|
{
|
|||
|
|
_mainCamera = Camera.main;
|
|||
|
|
|
|||
|
|
// Validate obstacle prefabs
|
|||
|
|
ValidateObstaclePrefabs();
|
|||
|
|
|
|||
|
|
if (useObjectPooling)
|
|||
|
|
{
|
|||
|
|
InitializeObjectPool();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void Start()
|
|||
|
|
{
|
|||
|
|
CalculateScreenBounds();
|
|||
|
|
StartSpawning();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void OnDestroy()
|
|||
|
|
{
|
|||
|
|
StopSpawning();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Validates that all prefabs have required components
|
|||
|
|
/// </summary>
|
|||
|
|
private void ValidateObstaclePrefabs()
|
|||
|
|
{
|
|||
|
|
for (int i = 0; i < obstaclePrefabs.Count; i++)
|
|||
|
|
{
|
|||
|
|
if (obstaclePrefabs[i] == null) continue;
|
|||
|
|
|
|||
|
|
// Check if the prefab has a FloatingObstacle component
|
|||
|
|
if (obstaclePrefabs[i].GetComponent<FloatingObstacle>() == null)
|
|||
|
|
{
|
|||
|
|
Debug.LogWarning($"Obstacle prefab {obstaclePrefabs[i].name} does not have a FloatingObstacle component. Adding one automatically.");
|
|||
|
|
obstaclePrefabs[i].AddComponent<FloatingObstacle>();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Ensure the prefab is on the correct layer
|
|||
|
|
if (obstaclePrefabs[i].layer != 11) // QuarryObstacle layer
|
|||
|
|
{
|
|||
|
|
Debug.LogWarning($"Obstacle prefab {obstaclePrefabs[i].name} is not on QuarryObstacle layer (11). Setting layer automatically.");
|
|||
|
|
SetLayerRecursively(obstaclePrefabs[i], 11);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Sets the layer of a GameObject and all its children
|
|||
|
|
/// </summary>
|
|||
|
|
private void SetLayerRecursively(GameObject obj, int layer)
|
|||
|
|
{
|
|||
|
|
obj.layer = layer;
|
|||
|
|
foreach (Transform child in obj.transform)
|
|||
|
|
{
|
|||
|
|
SetLayerRecursively(child.gameObject, layer);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Initialize the object pool system
|
|||
|
|
/// </summary>
|
|||
|
|
private void InitializeObjectPool()
|
|||
|
|
{
|
|||
|
|
GameObject poolGO = new GameObject("ObstaclePool");
|
|||
|
|
poolGO.transform.SetParent(transform);
|
|||
|
|
_obstaclePool = poolGO.AddComponent<ObstaclePool>();
|
|||
|
|
|
|||
|
|
// Set up pool configuration
|
|||
|
|
_obstaclePool.maxPerPrefabPoolSize = maxPerPrefabPoolSize;
|
|||
|
|
_obstaclePool.totalMaxPoolSize = totalMaxPoolSize;
|
|||
|
|
|
|||
|
|
// Convert GameObject list to FloatingObstacle list
|
|||
|
|
List<FloatingObstacle> prefabObstacles = new List<FloatingObstacle>(obstaclePrefabs.Count);
|
|||
|
|
foreach (var prefab in obstaclePrefabs)
|
|||
|
|
{
|
|||
|
|
if (prefab != null)
|
|||
|
|
{
|
|||
|
|
FloatingObstacle obstacleComponent = prefab.GetComponent<FloatingObstacle>();
|
|||
|
|
if (obstacleComponent != null)
|
|||
|
|
{
|
|||
|
|
prefabObstacles.Add(obstacleComponent);
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
Debug.LogError($"Obstacle prefab {prefab.name} is missing a FloatingObstacle component!");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Initialize the pool
|
|||
|
|
_obstaclePool.Initialize(prefabObstacles);
|
|||
|
|
|
|||
|
|
// Periodically trim the pool
|
|||
|
|
InvokeRepeating(nameof(TrimExcessPooledObstacles), 15f, 30f);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Calculate screen bounds in world space
|
|||
|
|
/// </summary>
|
|||
|
|
private void CalculateScreenBounds()
|
|||
|
|
{
|
|||
|
|
if (_mainCamera == null)
|
|||
|
|
{
|
|||
|
|
_mainCamera = Camera.main;
|
|||
|
|
if (_mainCamera == null)
|
|||
|
|
{
|
|||
|
|
Debug.LogError("[ObstacleSpawner] No main camera found!");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Vector3 bottomWorldPoint = _mainCamera.ViewportToWorldPoint(new Vector3(0.5f, 0f, _mainCamera.nearClipPlane));
|
|||
|
|
_screenBottom = bottomWorldPoint.y;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Starts the obstacle spawning coroutine
|
|||
|
|
/// </summary>
|
|||
|
|
public void StartSpawning()
|
|||
|
|
{
|
|||
|
|
if (_spawnCoroutine == null)
|
|||
|
|
{
|
|||
|
|
_spawnCoroutine = StartCoroutine(SpawnObstaclesCoroutine());
|
|||
|
|
Debug.Log("[ObstacleSpawner] Started spawning obstacles");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Stops the obstacle spawning coroutine
|
|||
|
|
/// </summary>
|
|||
|
|
public void StopSpawning()
|
|||
|
|
{
|
|||
|
|
if (_spawnCoroutine != null)
|
|||
|
|
{
|
|||
|
|
StopCoroutine(_spawnCoroutine);
|
|||
|
|
_spawnCoroutine = null;
|
|||
|
|
Debug.Log("[ObstacleSpawner] Stopped spawning obstacles");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Main spawning coroutine that runs continuously
|
|||
|
|
/// </summary>
|
|||
|
|
private IEnumerator SpawnObstaclesCoroutine()
|
|||
|
|
{
|
|||
|
|
while (true)
|
|||
|
|
{
|
|||
|
|
// Calculate next spawn time with variation
|
|||
|
|
float nextSpawnTime = spawnInterval + Random.Range(-spawnIntervalVariation, spawnIntervalVariation);
|
|||
|
|
nextSpawnTime = Mathf.Max(0.1f, nextSpawnTime); // Ensure minimum interval
|
|||
|
|
|
|||
|
|
yield return new WaitForSeconds(nextSpawnTime);
|
|||
|
|
|
|||
|
|
// Attempt to spawn an obstacle
|
|||
|
|
TrySpawnObstacle();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Attempts to spawn an obstacle at a valid position
|
|||
|
|
/// </summary>
|
|||
|
|
private void TrySpawnObstacle()
|
|||
|
|
{
|
|||
|
|
if (obstaclePrefabs == null || obstaclePrefabs.Count == 0)
|
|||
|
|
{
|
|||
|
|
Debug.LogWarning("[ObstacleSpawner] No obstacle prefabs available for spawning!");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Vector3 spawnPosition;
|
|||
|
|
bool foundValidPosition = false;
|
|||
|
|
|
|||
|
|
// Try to find a valid spawn position
|
|||
|
|
for (int attempts = 0; attempts < maxSpawnAttempts; attempts++)
|
|||
|
|
{
|
|||
|
|
spawnPosition = GetRandomSpawnPosition();
|
|||
|
|
|
|||
|
|
if (IsValidSpawnPosition(spawnPosition))
|
|||
|
|
{
|
|||
|
|
SpawnObstacleAt(spawnPosition);
|
|||
|
|
foundValidPosition = true;
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!foundValidPosition)
|
|||
|
|
{
|
|||
|
|
Debug.Log($"[ObstacleSpawner] Could not find valid spawn position after {maxSpawnAttempts} attempts");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Gets a random spawn position below the screen
|
|||
|
|
/// </summary>
|
|||
|
|
private Vector3 GetRandomSpawnPosition()
|
|||
|
|
{
|
|||
|
|
float randomX = Random.Range(-spawnRangeX, spawnRangeX);
|
|||
|
|
float spawnY = _screenBottom - spawnDistanceBelowScreen;
|
|||
|
|
|
|||
|
|
return new Vector3(randomX, spawnY, 0f);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Checks if a spawn position is valid (not colliding with tiles)
|
|||
|
|
/// </summary>
|
|||
|
|
private bool IsValidSpawnPosition(Vector3 position)
|
|||
|
|
{
|
|||
|
|
// Use OverlapCircle to check for collisions with tiles
|
|||
|
|
Collider2D collision = Physics2D.OverlapCircle(position, spawnCollisionRadius, tileLayerMask);
|
|||
|
|
return collision == null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Spawns an obstacle at the specified position
|
|||
|
|
/// </summary>
|
|||
|
|
private void SpawnObstacleAt(Vector3 position)
|
|||
|
|
{
|
|||
|
|
// Select random prefab
|
|||
|
|
int prefabIndex = Random.Range(0, obstaclePrefabs.Count);
|
|||
|
|
GameObject prefab = obstaclePrefabs[prefabIndex];
|
|||
|
|
|
|||
|
|
if (prefab == null)
|
|||
|
|
{
|
|||
|
|
Debug.LogError($"[ObstacleSpawner] Obstacle prefab at index {prefabIndex} is null!");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
GameObject obstacle;
|
|||
|
|
|
|||
|
|
// Spawn using pool or instantiate directly
|
|||
|
|
if (useObjectPooling && _obstaclePool != null)
|
|||
|
|
{
|
|||
|
|
obstacle = _obstaclePool.GetObstacle(prefabIndex);
|
|||
|
|
if (obstacle == null)
|
|||
|
|
{
|
|||
|
|
Debug.LogError("[ObstacleSpawner] Failed to get obstacle from pool!");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
obstacle.transform.position = position;
|
|||
|
|
obstacle.transform.rotation = prefab.transform.rotation;
|
|||
|
|
obstacle.transform.SetParent(transform);
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
obstacle = Instantiate(prefab, position, prefab.transform.rotation, transform);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Configure the obstacle
|
|||
|
|
ConfigureObstacle(obstacle, prefabIndex);
|
|||
|
|
|
|||
|
|
// Track active obstacles
|
|||
|
|
_activeObstacles.Add(obstacle);
|
|||
|
|
|
|||
|
|
// Invoke events
|
|||
|
|
onObstacleSpawned?.Invoke(obstacle);
|
|||
|
|
|
|||
|
|
Debug.Log($"[ObstacleSpawner] Spawned obstacle {obstacle.name} at {position}");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Configures an obstacle with randomized properties
|
|||
|
|
/// </summary>
|
|||
|
|
private void ConfigureObstacle(GameObject obstacle, int prefabIndex)
|
|||
|
|
{
|
|||
|
|
FloatingObstacle obstacleComponent = obstacle.GetComponent<FloatingObstacle>();
|
|||
|
|
if (obstacleComponent != null)
|
|||
|
|
{
|
|||
|
|
// Set prefab index
|
|||
|
|
obstacleComponent.PrefabIndex = prefabIndex;
|
|||
|
|
|
|||
|
|
// Randomize properties
|
|||
|
|
obstacleComponent.MoveSpeed = Random.Range(minMoveSpeed, maxMoveSpeed);
|
|||
|
|
obstacleComponent.Damage = Random.Range(minDamage, maxDamage);
|
|||
|
|
|
|||
|
|
// Set spawner reference (since FloatingObstacle has this built-in now)
|
|||
|
|
obstacleComponent.SetSpawner(this);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Returns an obstacle to the pool (called by FloatingObstacle)
|
|||
|
|
/// </summary>
|
|||
|
|
public void ReturnObstacleToPool(GameObject obstacle, int prefabIndex)
|
|||
|
|
{
|
|||
|
|
if (obstacle == null) return;
|
|||
|
|
|
|||
|
|
// Remove from active list
|
|||
|
|
_activeObstacles.Remove(obstacle);
|
|||
|
|
|
|||
|
|
// Invoke events
|
|||
|
|
onObstacleDestroyed?.Invoke(obstacle);
|
|||
|
|
|
|||
|
|
// Return to pool or destroy
|
|||
|
|
if (useObjectPooling && _obstaclePool != null)
|
|||
|
|
{
|
|||
|
|
_obstaclePool.ReturnObstacle(obstacle, prefabIndex);
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
Destroy(obstacle);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Called periodically to trim excess pooled obstacles
|
|||
|
|
/// </summary>
|
|||
|
|
private void TrimExcessPooledObstacles()
|
|||
|
|
{
|
|||
|
|
if (_obstaclePool != null)
|
|||
|
|
{
|
|||
|
|
_obstaclePool.TrimExcess();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Public method to change spawn interval at runtime
|
|||
|
|
/// </summary>
|
|||
|
|
public void SetSpawnInterval(float interval)
|
|||
|
|
{
|
|||
|
|
spawnInterval = interval;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Public method to change spawn range at runtime
|
|||
|
|
/// </summary>
|
|||
|
|
public void SetSpawnRange(float range)
|
|||
|
|
{
|
|||
|
|
spawnRangeX = range;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Public method to set speed range at runtime
|
|||
|
|
/// </summary>
|
|||
|
|
public void SetSpeedRange(float min, float max)
|
|||
|
|
{
|
|||
|
|
minMoveSpeed = min;
|
|||
|
|
maxMoveSpeed = max;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Public method to set damage range at runtime
|
|||
|
|
/// </summary>
|
|||
|
|
public void SetDamageRange(float min, float max)
|
|||
|
|
{
|
|||
|
|
minDamage = min;
|
|||
|
|
maxDamage = max;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Gets the count of currently active obstacles
|
|||
|
|
/// </summary>
|
|||
|
|
public int ActiveObstacleCount => _activeObstacles.Count;
|
|||
|
|
|
|||
|
|
#if UNITY_EDITOR
|
|||
|
|
private void OnDrawGizmosSelected()
|
|||
|
|
{
|
|||
|
|
// Draw spawn area
|
|||
|
|
Gizmos.color = Color.yellow;
|
|||
|
|
Vector3 center = new Vector3(0f, _screenBottom - spawnDistanceBelowScreen, 0f);
|
|||
|
|
Vector3 size = new Vector3(spawnRangeX * 2f, 1f, 1f);
|
|||
|
|
Gizmos.DrawWireCube(center, size);
|
|||
|
|
|
|||
|
|
// Draw collision radius at spawn point
|
|||
|
|
Gizmos.color = Color.red;
|
|||
|
|
Gizmos.DrawWireSphere(center, spawnCollisionRadius);
|
|||
|
|
}
|
|||
|
|
#endif
|
|||
|
|
}
|
|||
|
|
}
|