- A Settings Provider system to utilize addressables for loading settings at runtime - An editor UI for easy modifications of the settings objects - A split out developer settings functionality to keep gameplay and nitty-gritty details separately - Most settings migrated out of game objects and into the new system - An additional Editor utility for fetching the settings at editor runtime, for gizmos, visualization etc Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com> Co-authored-by: AlexanderT <alexander@foolhardyhorizons.com> Reviewed-on: #7
553 lines
22 KiB
C#
553 lines
22 KiB
C#
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using UnityEngine.Events;
|
|
using Pooling;
|
|
using AppleHills.Core.Settings;
|
|
|
|
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("Events")]
|
|
[Tooltip("Invoked when a new obstacle is spawned")]
|
|
public UnityEvent<GameObject> onObstacleSpawned;
|
|
|
|
[Tooltip("Invoked when an obstacle is destroyed or returned to pool")]
|
|
public UnityEvent<GameObject> onObstacleDestroyed;
|
|
|
|
// Settings references
|
|
private IDivingMinigameSettings _settings;
|
|
private DivingDeveloperSettings _devSettings;
|
|
|
|
// 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;
|
|
|
|
// Get settings from GameManager
|
|
_settings = GameManager.GetSettingsObject<IDivingMinigameSettings>();
|
|
_devSettings = GameManager.GetDeveloperSettings<DivingDeveloperSettings>();
|
|
|
|
if (_settings == null)
|
|
{
|
|
Debug.LogError("[ObstacleSpawner] Failed to load diving minigame settings!");
|
|
}
|
|
|
|
if (_devSettings == null)
|
|
{
|
|
Debug.LogError("[ObstacleSpawner] Failed to load diving developer settings!");
|
|
}
|
|
|
|
// Validate obstacle prefabs
|
|
ValidateObstaclePrefabs();
|
|
|
|
if (_devSettings?.ObstacleUseObjectPooling ?? false)
|
|
{
|
|
InitializeObjectPool();
|
|
}
|
|
|
|
// Initialize events if null
|
|
if (onObstacleSpawned == null)
|
|
onObstacleSpawned = new UnityEvent<GameObject>();
|
|
|
|
if (onObstacleDestroyed == null)
|
|
onObstacleDestroyed = new UnityEvent<GameObject>();
|
|
}
|
|
|
|
private void Start()
|
|
{
|
|
CalculateScreenBounds();
|
|
StartSpawning();
|
|
}
|
|
|
|
private void OnDestroy()
|
|
{
|
|
StopSpawning();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates that all prefabs have required components
|
|
/// </summary>
|
|
private void ValidateObstaclePrefabs()
|
|
{
|
|
if (_devSettings == null) return;
|
|
|
|
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 != _devSettings.ObstacleLayer)
|
|
{
|
|
Debug.LogWarning($"Obstacle prefab {obstaclePrefabs[i].name} is not on the configured obstacle layer ({_devSettings.ObstacleLayer}). Setting layer automatically.");
|
|
SetLayerRecursively(obstaclePrefabs[i], _devSettings.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 = _devSettings.ObstacleMaxPerPrefabPoolSize;
|
|
_obstaclePool.totalMaxPoolSize = _devSettings.ObstacleTotalMaxPoolSize;
|
|
|
|
// 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 = _settings.ObstacleSpawnInterval +
|
|
Random.Range(-_settings.ObstacleSpawnIntervalVariation,
|
|
_settings.ObstacleSpawnIntervalVariation);
|
|
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 < _settings.ObstacleMaxSpawnAttempts; 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}/{_settings.ObstacleMaxSpawnAttempts})");
|
|
}
|
|
}
|
|
|
|
if (!foundValidPosition)
|
|
{
|
|
Debug.LogWarning($"[ObstacleSpawner] SPAWN MISSED: Could not find valid spawn position after {_settings.ObstacleMaxSpawnAttempts} 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 using just the layer
|
|
// Convert the single layer to a layer mask inline (1 << layerNumber)
|
|
Collider2D collision = Physics2D.OverlapCircle(position, _settings.ObstacleSpawnCollisionRadius, 1 << _devSettings.TrenchTileLayer);
|
|
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 (_devSettings.ObstacleUseObjectPooling && _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 using settings
|
|
obstacleComponent.MoveSpeed = Random.Range(
|
|
_settings.ObstacleMinMoveSpeed,
|
|
_settings.ObstacleMaxMoveSpeed);
|
|
|
|
// 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 (_devSettings.ObstacleUseObjectPooling && _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)
|
|
{
|
|
// This method can no longer directly modify the settings
|
|
// Consider implementing a runtime settings override system if needed
|
|
Debug.LogWarning("[ObstacleSpawner] SetSpawnInterval no longer modifies settings directly. Settings are now centralized.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Public method to set speed range at runtime
|
|
/// </summary>
|
|
public void SetSpeedRange(float min, float max)
|
|
{
|
|
// This method can no longer directly modify the settings
|
|
// Consider implementing a runtime settings override system if needed
|
|
Debug.LogWarning("[ObstacleSpawner] SetSpeedRange no longer modifies settings directly. Settings are now centralized.");
|
|
}
|
|
|
|
/// <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 and settings are available
|
|
if (_spawnRangeX > 0f && _settings != null)
|
|
{
|
|
// 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, _settings.ObstacleSpawnCollisionRadius);
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
}
|