Move around scripts to cleanup the Minigame structure
This commit is contained in:
@@ -0,0 +1,553 @@
|
||||
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("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;
|
||||
|
||||
[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 during spawn position validation")]
|
||||
[SerializeField] private LayerMask tileLayerMask = -1; // Let user configure which layers to avoid
|
||||
|
||||
[Tooltip("Target layer for spawned obstacles - obstacles will be placed on this layer")]
|
||||
[SerializeField] private int obstacleLayer = 11; // Default to layer 11, but configurable
|
||||
|
||||
[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 float _spawnRangeX;
|
||||
private Coroutine _spawnCoroutine;
|
||||
private readonly List<GameObject> _activeObstacles = new List<GameObject>();
|
||||
private int _obstacleCounter = 0; // Counter for unique obstacle naming
|
||||
private bool _isSurfacing = false; // Flag to track surfacing state
|
||||
private float _velocityFactor = 1.0f; // Current velocity factor from the game manager
|
||||
|
||||
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 (using configurable obstacleLayer)
|
||||
if (obstaclePrefabs[i].layer != obstacleLayer)
|
||||
{
|
||||
Debug.LogWarning($"Obstacle prefab {obstaclePrefabs[i].name} is not on the configured obstacle layer ({obstacleLayer}). Setting layer automatically.");
|
||||
SetLayerRecursively(obstaclePrefabs[i], obstacleLayer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculate screen bounds in world space dynamically
|
||||
/// </summary>
|
||||
private void CalculateScreenBounds()
|
||||
{
|
||||
if (_mainCamera == null)
|
||||
{
|
||||
_mainCamera = Camera.main;
|
||||
if (_mainCamera == null)
|
||||
{
|
||||
Debug.LogError("[ObstacleSpawner] No main camera found!");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate screen bottom (Y spawn position will be 2 units below this)
|
||||
Vector3 bottomWorldPoint = _mainCamera.ViewportToWorldPoint(new Vector3(0.5f, 0f, _mainCamera.nearClipPlane));
|
||||
_screenBottom = bottomWorldPoint.y;
|
||||
|
||||
// Calculate screen width in world units
|
||||
Vector3 leftWorldPoint = _mainCamera.ViewportToWorldPoint(new Vector3(0f, 0.5f, _mainCamera.nearClipPlane));
|
||||
Vector3 rightWorldPoint = _mainCamera.ViewportToWorldPoint(new Vector3(1f, 0.5f, _mainCamera.nearClipPlane));
|
||||
float screenWidth = rightWorldPoint.x - leftWorldPoint.x;
|
||||
|
||||
// Calculate spawn range based on 80% of screen width (40% on each side from center)
|
||||
_spawnRangeX = (screenWidth * 0.8f) / 2f;
|
||||
|
||||
Debug.Log($"[ObstacleSpawner] Screen calculated - Width: {screenWidth:F2}, Bottom: {_screenBottom:F2}, Spawn Range X: ±{_spawnRangeX:F2}");
|
||||
}
|
||||
|
||||
/// <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()
|
||||
{
|
||||
// Don't spawn new obstacles when surfacing
|
||||
if (_isSurfacing)
|
||||
{
|
||||
Debug.Log("[ObstacleSpawner] Skipping obstacle spawn - currently surfacing");
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Log($"[ObstacleSpawner] TrySpawnObstacle called at {Time.time:F2}");
|
||||
|
||||
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))
|
||||
{
|
||||
Debug.Log($"[ObstacleSpawner] Found valid position at {spawnPosition} after {attempts + 1} attempts");
|
||||
SpawnObstacleAt(spawnPosition);
|
||||
foundValidPosition = true;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log($"[ObstacleSpawner] Position {spawnPosition} invalid (attempt {attempts + 1}/{maxSpawnAttempts})");
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundValidPosition)
|
||||
{
|
||||
Debug.LogWarning($"[ObstacleSpawner] SPAWN MISSED: Could not find valid spawn position after {maxSpawnAttempts} attempts at {Time.time:F2}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a random spawn position below the screen
|
||||
/// </summary>
|
||||
private Vector3 GetRandomSpawnPosition()
|
||||
{
|
||||
// Use dynamically calculated spawn range (80% of screen width)
|
||||
float randomX = Random.Range(-_spawnRangeX, _spawnRangeX);
|
||||
// Spawn 2 units below screen bottom
|
||||
float spawnY = _screenBottom - 2f;
|
||||
|
||||
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)
|
||||
{
|
||||
Debug.Log($"[ObstacleSpawner] SpawnObstacleAt called for position {position}");
|
||||
|
||||
// Select random prefab
|
||||
int prefabIndex = Random.Range(0, obstaclePrefabs.Count);
|
||||
GameObject prefab = obstaclePrefabs[prefabIndex];
|
||||
|
||||
if (prefab == null)
|
||||
{
|
||||
Debug.LogError($"[ObstacleSpawner] SPAWN FAILED: Obstacle prefab at index {prefabIndex} is null!");
|
||||
return;
|
||||
}
|
||||
|
||||
GameObject obstacle;
|
||||
|
||||
// Spawn using pool or instantiate directly
|
||||
if (useObjectPooling && _obstaclePool != null)
|
||||
{
|
||||
Debug.Log($"[ObstacleSpawner] Requesting obstacle from pool (prefab index {prefabIndex})");
|
||||
obstacle = _obstaclePool.GetObstacle(prefabIndex);
|
||||
if (obstacle == null)
|
||||
{
|
||||
Debug.LogError($"[ObstacleSpawner] SPAWN FAILED: Failed to get obstacle from pool for prefab index {prefabIndex}!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Important: Set position/parent/rotation BEFORE activation to avoid visual glitches
|
||||
obstacle.transform.position = position;
|
||||
obstacle.transform.rotation = prefab.transform.rotation;
|
||||
obstacle.transform.SetParent(transform);
|
||||
|
||||
Debug.Log($"[ObstacleSpawner] Got obstacle {obstacle.name} from pool, active state: {obstacle.activeInHierarchy}");
|
||||
|
||||
// ENHANCED FORCE ACTIVATION - more robust approach
|
||||
if (!obstacle.activeInHierarchy)
|
||||
{
|
||||
Debug.LogWarning($"[ObstacleSpawner] Pool returned inactive object {obstacle.name}, force activating!");
|
||||
|
||||
// Configure obstacle BEFORE activation
|
||||
ConfigureObstacle(obstacle, prefabIndex);
|
||||
|
||||
// Force activate the obstacle
|
||||
obstacle.SetActive(true);
|
||||
|
||||
// Double-check activation status
|
||||
if (!obstacle.activeInHierarchy)
|
||||
{
|
||||
Debug.LogError($"[ObstacleSpawner] CRITICAL ERROR: Failed to activate {obstacle.name} after multiple attempts!");
|
||||
|
||||
// Last resort: try to instantiate a new one instead
|
||||
GameObject newObstacle = Instantiate(prefab, position, prefab.transform.rotation, transform);
|
||||
if (newObstacle != null)
|
||||
{
|
||||
obstacle = newObstacle;
|
||||
ConfigureObstacle(obstacle, prefabIndex);
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Log($"[ObstacleSpawner] After force activation, {obstacle.name} active state: {obstacle.activeInHierarchy}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Still configure if already active
|
||||
ConfigureObstacle(obstacle, prefabIndex);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log($"[ObstacleSpawner] Instantiating new obstacle (pooling disabled)");
|
||||
obstacle = Instantiate(prefab, position, prefab.transform.rotation, transform);
|
||||
|
||||
// Configure the newly instantiated obstacle
|
||||
ConfigureObstacle(obstacle, prefabIndex);
|
||||
}
|
||||
|
||||
// Assign unique name with counter
|
||||
_obstacleCounter++;
|
||||
string oldName = obstacle.name;
|
||||
obstacle.name = $"Obstacle{_obstacleCounter:D3}";
|
||||
Debug.Log($"[ObstacleSpawner] Renamed obstacle from '{oldName}' to '{obstacle.name}', active state: {obstacle.activeInHierarchy}");
|
||||
|
||||
// Track active obstacles
|
||||
_activeObstacles.Add(obstacle);
|
||||
|
||||
// Invoke events
|
||||
onObstacleSpawned?.Invoke(obstacle);
|
||||
Debug.Log($"[ObstacleSpawner] After events, obstacle {obstacle.name} active state: {obstacle.activeInHierarchy}");
|
||||
|
||||
Debug.Log($"[ObstacleSpawner] Successfully spawned obstacle {obstacle.name} at {position}. Active count: {_activeObstacles.Count}, Final active state: {obstacle.activeInHierarchy}");
|
||||
}
|
||||
|
||||
/// <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);
|
||||
|
||||
// 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;
|
||||
|
||||
Debug.Log($"[ObstacleSpawner] ReturnObstacleToPool called for {obstacle.name}, active state: {obstacle.activeInHierarchy}");
|
||||
|
||||
// Remove from active list
|
||||
_activeObstacles.Remove(obstacle);
|
||||
|
||||
// Invoke events
|
||||
onObstacleDestroyed?.Invoke(obstacle);
|
||||
|
||||
// Return to pool or destroy
|
||||
if (useObjectPooling && _obstaclePool != null)
|
||||
{
|
||||
Debug.Log($"[ObstacleSpawner] Returning {obstacle.name} to pool");
|
||||
_obstaclePool.ReturnObstacle(obstacle, prefabIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log($"[ObstacleSpawner] Destroying {obstacle.name} (pooling disabled)");
|
||||
Destroy(obstacle);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Public method to change spawn interval at runtime
|
||||
/// </summary>
|
||||
public void SetSpawnInterval(float interval)
|
||||
{
|
||||
spawnInterval = interval;
|
||||
}
|
||||
|
||||
/// <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 recalculate screen bounds (useful if camera changes)
|
||||
/// </summary>
|
||||
public void RecalculateScreenBounds()
|
||||
{
|
||||
CalculateScreenBounds();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the velocity factor changes from the DivingGameManager
|
||||
/// </summary>
|
||||
public void OnVelocityFactorChanged(float velocityFactor)
|
||||
{
|
||||
_velocityFactor = velocityFactor;
|
||||
|
||||
// Update all active obstacles with the new velocity factor
|
||||
foreach (GameObject obstacle in _activeObstacles)
|
||||
{
|
||||
if (obstacle != null)
|
||||
{
|
||||
FloatingObstacle obstacleComponent = obstacle.GetComponent<FloatingObstacle>();
|
||||
if (obstacleComponent != null)
|
||||
{
|
||||
obstacleComponent.OnVelocityFactorChanged(velocityFactor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Log($"[ObstacleSpawner] Velocity factor updated to {_velocityFactor:F2}, propagated to {_activeObstacles.Count} active obstacles");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start surfacing mode - reverse direction of existing obstacles and stop spawning new ones
|
||||
/// </summary>
|
||||
public void StartSurfacing()
|
||||
{
|
||||
if (_isSurfacing) return; // Already surfacing
|
||||
|
||||
_isSurfacing = true;
|
||||
|
||||
// Notify obstacles about surfacing state (for direction-based logic)
|
||||
foreach (GameObject obstacle in _activeObstacles)
|
||||
{
|
||||
if (obstacle != null)
|
||||
{
|
||||
FloatingObstacle obstacleComponent = obstacle.GetComponent<FloatingObstacle>();
|
||||
if (obstacleComponent != null)
|
||||
{
|
||||
// Call StartSurfacing on the obstacle component itself
|
||||
obstacleComponent.StartSurfacing();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Log($"[ObstacleSpawner] Started surfacing mode for {_activeObstacles.Count} active obstacles");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the count of currently active obstacles
|
||||
/// </summary>
|
||||
public int ActiveObstacleCount => _activeObstacles.Count;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
// Only draw if screen bounds have been calculated
|
||||
if (_spawnRangeX > 0f)
|
||||
{
|
||||
// Draw spawn area using dynamic calculations
|
||||
Gizmos.color = Color.yellow;
|
||||
Vector3 center = new Vector3(0f, _screenBottom - 2f, 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user