From 9be8d1619b77687ce9c245ca76f77e97da4fe66a Mon Sep 17 00:00:00 2001 From: Michal Pikulski Date: Thu, 18 Sep 2025 15:51:18 +0200 Subject: [PATCH] Resolved the obstacle spawning issue --- .../DivingForPictures/FloatingObstacle.cs | 54 ++++++-- .../DivingForPictures/ObstaclePool.cs | 10 ++ .../DivingForPictures/ObstacleSpawner.cs | 57 ++++++--- Assets/Scripts/Pooling/MultiPrefabPool.cs | 120 ++++++++++++++---- 4 files changed, 185 insertions(+), 56 deletions(-) diff --git a/Assets/Scripts/Minigames/DivingForPictures/FloatingObstacle.cs b/Assets/Scripts/Minigames/DivingForPictures/FloatingObstacle.cs index 11a58e49..52338634 100644 --- a/Assets/Scripts/Minigames/DivingForPictures/FloatingObstacle.cs +++ b/Assets/Scripts/Minigames/DivingForPictures/FloatingObstacle.cs @@ -155,18 +155,21 @@ namespace Minigames.DivingForPictures /// private void CheckIfOffScreen() { - if (_mainCamera == null) return; - - // Calculate screen top if not cached - if (_screenTop == 0f) + if (_mainCamera == null) { - Vector3 topWorldPoint = _mainCamera.ViewportToWorldPoint(new Vector3(0.5f, 1f, _mainCamera.nearClipPlane)); - _screenTop = topWorldPoint.y; + _mainCamera = Camera.main; + if (_mainCamera == null) return; } - // Check if obstacle is above screen - if (transform.position.y > _screenTop + 2f) // Extra buffer for safety + // Always recalculate screen bounds to ensure accuracy + Vector3 topWorldPoint = _mainCamera.ViewportToWorldPoint(new Vector3(0.5f, 1f, _mainCamera.transform.position.z)); + _screenTop = topWorldPoint.y; + + // Check if obstacle is significantly above screen top (obstacles move upward) + // Use a larger buffer to ensure obstacles are truly off-screen before returning to pool + if (transform.position.y > _screenTop + 5f) { + Debug.Log($"[FloatingObstacle] {gameObject.name} off-screen at Y:{transform.position.y:F2}, screen top:{_screenTop:F2}"); ReturnToPool(); } } @@ -176,14 +179,33 @@ namespace Minigames.DivingForPictures /// private void ReturnToPool() { + // CRITICAL: Stop all behavior first to prevent race conditions + // This ensures no more off-screen checks or movement happen during pool return + StopObstacleBehavior(); + if (spawner != null) { spawner.ReturnObstacleToPool(gameObject, prefabIndex); } else { - Debug.LogWarning($"[FloatingObstacle] Cannot return {gameObject.name} to pool - missing spawner reference"); - Destroy(gameObject); + // Try to find the spawner instead of destroying the object + ObstacleSpawner foundSpawner = FindFirstObjectByType(); + if (foundSpawner != null) + { + Debug.LogWarning($"[FloatingObstacle] Obstacle {gameObject.name} lost spawner reference, found replacement spawner"); + spawner = foundSpawner; + spawner.ReturnObstacleToPool(gameObject, prefabIndex); + } + else + { + // No spawner found - just deactivate the object instead of destroying it + Debug.LogWarning($"[FloatingObstacle] No spawner found for {gameObject.name}, deactivating safely"); + gameObject.SetActive(false); + + // Move to a safe location to avoid interference + transform.position = new Vector3(1000f, 1000f, 0f); + } } } @@ -201,7 +223,9 @@ namespace Minigames.DivingForPictures /// public void OnSpawn() { + // Reset all state first _screenTop = 0f; // Reset cached screen bounds + _mainCamera = Camera.main; // Refresh camera reference // Re-enable the collider for reuse if (_collider != null) @@ -209,10 +233,9 @@ namespace Minigames.DivingForPictures _collider.enabled = true; } - // Ensure the obstacle is active and visible - gameObject.SetActive(true); + Debug.Log($"[FloatingObstacle] Obstacle {gameObject.name} spawned from pool"); - Debug.Log($"[FloatingObstacle] Obstacle {gameObject.name} spawned"); + // Note: Don't start coroutines here - OnEnable() will handle that when SetActive(true) is called } /// @@ -220,13 +243,16 @@ namespace Minigames.DivingForPictures /// public void OnDespawn() { + // Stop all coroutines before returning to pool + StopObstacleBehavior(); + // Re-enable collider for next use (in case it was disabled) if (_collider != null) { _collider.enabled = true; } - Debug.Log($"[FloatingObstacle] Obstacle {gameObject.name} despawned"); + Debug.Log($"[FloatingObstacle] Obstacle {gameObject.name} despawned to pool"); } /// diff --git a/Assets/Scripts/Minigames/DivingForPictures/ObstaclePool.cs b/Assets/Scripts/Minigames/DivingForPictures/ObstaclePool.cs index 25b8edf8..d11a77fa 100644 --- a/Assets/Scripts/Minigames/DivingForPictures/ObstaclePool.cs +++ b/Assets/Scripts/Minigames/DivingForPictures/ObstaclePool.cs @@ -21,6 +21,7 @@ namespace Minigames.DivingForPictures FloatingObstacle obstacleComponent = obstacle.GetComponent(); if (obstacleComponent != null) { + Debug.Log($"[ObstaclePool] Returning obstacle {obstacle.name} to pool"); Return(obstacleComponent, prefabIndex); } else @@ -37,7 +38,16 @@ namespace Minigames.DivingForPictures /// An obstacle instance ready to use public GameObject GetObstacle(int prefabIndex) { + Debug.Log($"[ObstaclePool] GetObstacle called for prefab index {prefabIndex}"); FloatingObstacle obstacleComponent = Get(prefabIndex); + + if (obstacleComponent == null) + { + Debug.LogError($"[ObstaclePool] Get() returned null for prefab index {prefabIndex}"); + return null; + } + + Debug.Log($"[ObstaclePool] Get() returned obstacle {obstacleComponent.name}, active state: {obstacleComponent.gameObject.activeInHierarchy}"); return obstacleComponent.gameObject; } } diff --git a/Assets/Scripts/Minigames/DivingForPictures/ObstacleSpawner.cs b/Assets/Scripts/Minigames/DivingForPictures/ObstacleSpawner.cs index 0c9031d1..67f0cffd 100644 --- a/Assets/Scripts/Minigames/DivingForPictures/ObstacleSpawner.cs +++ b/Assets/Scripts/Minigames/DivingForPictures/ObstacleSpawner.cs @@ -74,6 +74,7 @@ namespace Minigames.DivingForPictures private float _spawnRangeX; private Coroutine _spawnCoroutine; private readonly List _activeObstacles = new List(); + private int _obstacleCounter = 0; // Counter for unique obstacle naming private void Awake() { @@ -169,9 +170,6 @@ namespace Minigames.DivingForPictures // Initialize the pool _obstaclePool.Initialize(prefabObstacles); - - // Periodically trim the pool - InvokeRepeating(nameof(TrimExcessPooledObstacles), 15f, 30f); } /// @@ -252,6 +250,8 @@ namespace Minigames.DivingForPictures /// private void TrySpawnObstacle() { + Debug.Log($"[ObstacleSpawner] TrySpawnObstacle called at {Time.time:F2}"); + if (obstaclePrefabs == null || obstaclePrefabs.Count == 0) { Debug.LogWarning("[ObstacleSpawner] No obstacle prefabs available for spawning!"); @@ -268,15 +268,20 @@ namespace Minigames.DivingForPictures 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.Log($"[ObstacleSpawner] Could not find valid spawn position after {maxSpawnAttempts} attempts"); + Debug.LogWarning($"[ObstacleSpawner] SPAWN MISSED: Could not find valid spawn position after {maxSpawnAttempts} attempts at {Time.time:F2}"); } } @@ -308,13 +313,15 @@ namespace Minigames.DivingForPictures /// 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] Obstacle prefab at index {prefabIndex} is null!"); + Debug.LogError($"[ObstacleSpawner] SPAWN FAILED: Obstacle prefab at index {prefabIndex} is null!"); return; } @@ -323,32 +330,53 @@ namespace Minigames.DivingForPictures // 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] Failed to get obstacle from pool!"); + Debug.LogError($"[ObstacleSpawner] SPAWN FAILED: Failed to get obstacle from pool for prefab index {prefabIndex}!"); return; } + Debug.Log($"[ObstacleSpawner] Got obstacle {obstacle.name} from pool, active state: {obstacle.activeInHierarchy}"); + + // FORCE ACTIVATION - bypass pool issues + if (!obstacle.activeInHierarchy) + { + Debug.LogWarning($"[ObstacleSpawner] Pool returned inactive object {obstacle.name}, force activating!"); + obstacle.SetActive(true); + Debug.Log($"[ObstacleSpawner] After force activation, {obstacle.name} active state: {obstacle.activeInHierarchy}"); + } + obstacle.transform.position = position; obstacle.transform.rotation = prefab.transform.rotation; obstacle.transform.SetParent(transform); + Debug.Log($"[ObstacleSpawner] After positioning, obstacle {obstacle.name} active state: {obstacle.activeInHierarchy}"); } else { + Debug.Log($"[ObstacleSpawner] Instantiating new obstacle (pooling disabled)"); obstacle = Instantiate(prefab, position, prefab.transform.rotation, transform); } + // 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}"); + // Configure the obstacle ConfigureObstacle(obstacle, prefabIndex); + Debug.Log($"[ObstacleSpawner] After configuration, obstacle {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] Spawned obstacle {obstacle.name} at {position}"); + Debug.Log($"[ObstacleSpawner] Successfully spawned obstacle {obstacle.name} at {position}. Active count: {_activeObstacles.Count}, Final active state: {obstacle.activeInHierarchy}"); } /// @@ -377,6 +405,8 @@ namespace Minigames.DivingForPictures { if (obstacle == null) return; + Debug.Log($"[ObstacleSpawner] ReturnObstacleToPool called for {obstacle.name}, active state: {obstacle.activeInHierarchy}"); + // Remove from active list _activeObstacles.Remove(obstacle); @@ -386,25 +416,16 @@ namespace Minigames.DivingForPictures // 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); } } - /// - /// Called periodically to trim excess pooled obstacles - /// - private void TrimExcessPooledObstacles() - { - if (_obstaclePool != null) - { - _obstaclePool.TrimExcess(); - } - } - /// /// Public method to change spawn interval at runtime /// diff --git a/Assets/Scripts/Pooling/MultiPrefabPool.cs b/Assets/Scripts/Pooling/MultiPrefabPool.cs index 978d1131..75e2d0da 100644 --- a/Assets/Scripts/Pooling/MultiPrefabPool.cs +++ b/Assets/Scripts/Pooling/MultiPrefabPool.cs @@ -100,7 +100,7 @@ namespace Pooling /// An object ready to use public virtual T Get(int prefabIndex) { - T obj; + T obj = null; // Track usage frequency if (prefabUsageCount.ContainsKey(prefabIndex)) @@ -112,27 +112,71 @@ namespace Pooling prefabUsageCount[prefabIndex] = 1; } + // Try to get a valid object from the pool, cleaning up any destroyed objects if (pooledObjects.ContainsKey(prefabIndex) && pooledObjects[prefabIndex].Count > 0) { - obj = pooledObjects[prefabIndex].Pop(); - totalPooledCount--; + Debug.Log($"[{GetType().Name}] Found {pooledObjects[prefabIndex].Count} objects in pool for prefab index {prefabIndex}"); + + // Keep trying until we find a valid object or the pool is empty + while (pooledObjects[prefabIndex].Count > 0) + { + obj = pooledObjects[prefabIndex].Pop(); + totalPooledCount--; + + // Check if the object is still valid (not destroyed) + if (obj != null && obj.gameObject != null) + { + Debug.Log($"[{GetType().Name}] Retrieved valid object {obj.name} from pool, current active state: {obj.gameObject.activeInHierarchy}"); + break; // Found a valid object + } + else + { + // Object was destroyed, continue looking + Debug.LogWarning($"[{GetType().Name}] Found destroyed object in pool, removing it"); + obj = null; + } + } } else { - // Create new object without adding to pool - T prefab = prefabs[prefabIndex]; - obj = Instantiate(prefab, transform); + Debug.Log($"[{GetType().Name}] No objects in pool for prefab index {prefabIndex}, creating new one"); } + // If we couldn't find a valid object in the pool, create a new one + if (obj == null) + { + T prefab = prefabs[prefabIndex]; + obj = Instantiate(prefab, transform); + Debug.Log($"[{GetType().Name}] Created new object {obj.name} from prefab, active state: {obj.gameObject.activeInHierarchy}"); + } + + // Ensure the object is valid before proceeding + if (obj == null || obj.gameObject == null) + { + Debug.LogError($"[{GetType().Name}] Failed to create valid object for prefab index {prefabIndex}"); + return null; + } + + // CRITICAL FIX: Reset position to safe location BEFORE activation + // This prevents off-screen checks from triggering during spawn process + Vector3 originalPosition = obj.transform.position; + obj.transform.position = new Vector3(0f, -1000f, 0f); + Debug.Log($"[{GetType().Name}] Moved object {obj.name} from {originalPosition} to safe position before activation"); + + Debug.Log($"[{GetType().Name}] About to activate object {obj.name}, current state: {obj.gameObject.activeInHierarchy}"); obj.gameObject.SetActive(true); + Debug.Log($"[{GetType().Name}] After SetActive(true), object {obj.name} state: {obj.gameObject.activeInHierarchy}"); // Call OnSpawn for IPoolable components IPoolable poolable = obj.GetComponent(); if (poolable != null) { + Debug.Log($"[{GetType().Name}] Calling OnSpawn for object {obj.name}"); poolable.OnSpawn(); + Debug.Log($"[{GetType().Name}] After OnSpawn, object {obj.name} state: {obj.gameObject.activeInHierarchy}"); } + Debug.Log($"[{GetType().Name}] Returning object {obj.name} with final state: {obj.gameObject.activeInHierarchy}"); return obj; } @@ -152,33 +196,61 @@ namespace Pooling poolable.OnDespawn(); } - // Check if we're under the maximum pool size for this prefab type - bool keepObject = totalPooledCount < totalMaxPoolSize; + // Always deactivate and parent the object + obj.gameObject.SetActive(false); + obj.transform.SetParent(transform); - // Additional constraint: don't keep too many of any single prefab type - if (pooledObjects.ContainsKey(prefabIndex) && - pooledObjects[prefabIndex].Count >= maxPerPrefabPoolSize) + // Initialize stack if it doesn't exist + if (!pooledObjects.ContainsKey(prefabIndex)) { - keepObject = false; + pooledObjects[prefabIndex] = new Stack(); } - if (keepObject) + // Check if we need to trim this specific prefab type's pool + if (pooledObjects[prefabIndex].Count >= maxPerPrefabPoolSize) { - obj.gameObject.SetActive(false); - obj.transform.SetParent(transform); - - if (!pooledObjects.ContainsKey(prefabIndex)) + // Remove the oldest object from this prefab's pool to make room + if (pooledObjects[prefabIndex].Count > 0) { - pooledObjects[prefabIndex] = new Stack(); + T oldestObj = pooledObjects[prefabIndex].Pop(); + if (oldestObj != null && oldestObj.gameObject != null) + { + Destroy(oldestObj.gameObject); + totalPooledCount--; + } + } + } + + // Check global pool limit + if (totalPooledCount >= totalMaxPoolSize) + { + // Find the prefab type with the most pooled objects and remove one + int maxCount = 0; + int prefabToTrim = -1; + + foreach (var kvp in pooledObjects) + { + if (kvp.Value.Count > maxCount) + { + maxCount = kvp.Value.Count; + prefabToTrim = kvp.Key; + } } - pooledObjects[prefabIndex].Push(obj); - totalPooledCount++; - } - else - { - Destroy(obj.gameObject); + if (prefabToTrim >= 0 && pooledObjects[prefabToTrim].Count > 0) + { + T oldestObj = pooledObjects[prefabToTrim].Pop(); + if (oldestObj != null && oldestObj.gameObject != null) + { + Destroy(oldestObj.gameObject); + totalPooledCount--; + } + } } + + // Now add the current object to the pool + pooledObjects[prefabIndex].Push(obj); + totalPooledCount++; } ///