Resolved the obstacle spawning issue

This commit is contained in:
Michal Pikulski
2025-09-18 15:51:18 +02:00
parent c5ce2ae1af
commit 9be8d1619b
4 changed files with 185 additions and 56 deletions

View File

@@ -155,18 +155,21 @@ namespace Minigames.DivingForPictures
/// </summary> /// </summary>
private void CheckIfOffScreen() private void CheckIfOffScreen()
{ {
if (_mainCamera == null) return; if (_mainCamera == null)
// Calculate screen top if not cached
if (_screenTop == 0f)
{ {
Vector3 topWorldPoint = _mainCamera.ViewportToWorldPoint(new Vector3(0.5f, 1f, _mainCamera.nearClipPlane)); _mainCamera = Camera.main;
_screenTop = topWorldPoint.y; if (_mainCamera == null) return;
} }
// Check if obstacle is above screen // Always recalculate screen bounds to ensure accuracy
if (transform.position.y > _screenTop + 2f) // Extra buffer for safety 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(); ReturnToPool();
} }
} }
@@ -176,14 +179,33 @@ namespace Minigames.DivingForPictures
/// </summary> /// </summary>
private void ReturnToPool() 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) if (spawner != null)
{ {
spawner.ReturnObstacleToPool(gameObject, prefabIndex); spawner.ReturnObstacleToPool(gameObject, prefabIndex);
} }
else else
{ {
Debug.LogWarning($"[FloatingObstacle] Cannot return {gameObject.name} to pool - missing spawner reference"); // Try to find the spawner instead of destroying the object
Destroy(gameObject); ObstacleSpawner foundSpawner = FindFirstObjectByType<ObstacleSpawner>();
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
/// </summary> /// </summary>
public void OnSpawn() public void OnSpawn()
{ {
// Reset all state first
_screenTop = 0f; // Reset cached screen bounds _screenTop = 0f; // Reset cached screen bounds
_mainCamera = Camera.main; // Refresh camera reference
// Re-enable the collider for reuse // Re-enable the collider for reuse
if (_collider != null) if (_collider != null)
@@ -209,10 +233,9 @@ namespace Minigames.DivingForPictures
_collider.enabled = true; _collider.enabled = true;
} }
// Ensure the obstacle is active and visible Debug.Log($"[FloatingObstacle] Obstacle {gameObject.name} spawned from pool");
gameObject.SetActive(true);
Debug.Log($"[FloatingObstacle] Obstacle {gameObject.name} spawned"); // Note: Don't start coroutines here - OnEnable() will handle that when SetActive(true) is called
} }
/// <summary> /// <summary>
@@ -220,13 +243,16 @@ namespace Minigames.DivingForPictures
/// </summary> /// </summary>
public void OnDespawn() public void OnDespawn()
{ {
// Stop all coroutines before returning to pool
StopObstacleBehavior();
// Re-enable collider for next use (in case it was disabled) // Re-enable collider for next use (in case it was disabled)
if (_collider != null) if (_collider != null)
{ {
_collider.enabled = true; _collider.enabled = true;
} }
Debug.Log($"[FloatingObstacle] Obstacle {gameObject.name} despawned"); Debug.Log($"[FloatingObstacle] Obstacle {gameObject.name} despawned to pool");
} }
/// <summary> /// <summary>

View File

@@ -21,6 +21,7 @@ namespace Minigames.DivingForPictures
FloatingObstacle obstacleComponent = obstacle.GetComponent<FloatingObstacle>(); FloatingObstacle obstacleComponent = obstacle.GetComponent<FloatingObstacle>();
if (obstacleComponent != null) if (obstacleComponent != null)
{ {
Debug.Log($"[ObstaclePool] Returning obstacle {obstacle.name} to pool");
Return(obstacleComponent, prefabIndex); Return(obstacleComponent, prefabIndex);
} }
else else
@@ -37,7 +38,16 @@ namespace Minigames.DivingForPictures
/// <returns>An obstacle instance ready to use</returns> /// <returns>An obstacle instance ready to use</returns>
public GameObject GetObstacle(int prefabIndex) public GameObject GetObstacle(int prefabIndex)
{ {
Debug.Log($"[ObstaclePool] GetObstacle called for prefab index {prefabIndex}");
FloatingObstacle obstacleComponent = Get(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; return obstacleComponent.gameObject;
} }
} }

View File

@@ -74,6 +74,7 @@ namespace Minigames.DivingForPictures
private float _spawnRangeX; private float _spawnRangeX;
private Coroutine _spawnCoroutine; private Coroutine _spawnCoroutine;
private readonly List<GameObject> _activeObstacles = new List<GameObject>(); private readonly List<GameObject> _activeObstacles = new List<GameObject>();
private int _obstacleCounter = 0; // Counter for unique obstacle naming
private void Awake() private void Awake()
{ {
@@ -169,9 +170,6 @@ namespace Minigames.DivingForPictures
// Initialize the pool // Initialize the pool
_obstaclePool.Initialize(prefabObstacles); _obstaclePool.Initialize(prefabObstacles);
// Periodically trim the pool
InvokeRepeating(nameof(TrimExcessPooledObstacles), 15f, 30f);
} }
/// <summary> /// <summary>
@@ -252,6 +250,8 @@ namespace Minigames.DivingForPictures
/// </summary> /// </summary>
private void TrySpawnObstacle() private void TrySpawnObstacle()
{ {
Debug.Log($"[ObstacleSpawner] TrySpawnObstacle called at {Time.time:F2}");
if (obstaclePrefabs == null || obstaclePrefabs.Count == 0) if (obstaclePrefabs == null || obstaclePrefabs.Count == 0)
{ {
Debug.LogWarning("[ObstacleSpawner] No obstacle prefabs available for spawning!"); Debug.LogWarning("[ObstacleSpawner] No obstacle prefabs available for spawning!");
@@ -268,15 +268,20 @@ namespace Minigames.DivingForPictures
if (IsValidSpawnPosition(spawnPosition)) if (IsValidSpawnPosition(spawnPosition))
{ {
Debug.Log($"[ObstacleSpawner] Found valid position at {spawnPosition} after {attempts + 1} attempts");
SpawnObstacleAt(spawnPosition); SpawnObstacleAt(spawnPosition);
foundValidPosition = true; foundValidPosition = true;
break; break;
} }
else
{
Debug.Log($"[ObstacleSpawner] Position {spawnPosition} invalid (attempt {attempts + 1}/{maxSpawnAttempts})");
}
} }
if (!foundValidPosition) 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
/// </summary> /// </summary>
private void SpawnObstacleAt(Vector3 position) private void SpawnObstacleAt(Vector3 position)
{ {
Debug.Log($"[ObstacleSpawner] SpawnObstacleAt called for position {position}");
// Select random prefab // Select random prefab
int prefabIndex = Random.Range(0, obstaclePrefabs.Count); int prefabIndex = Random.Range(0, obstaclePrefabs.Count);
GameObject prefab = obstaclePrefabs[prefabIndex]; GameObject prefab = obstaclePrefabs[prefabIndex];
if (prefab == null) 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; return;
} }
@@ -323,32 +330,53 @@ namespace Minigames.DivingForPictures
// Spawn using pool or instantiate directly // Spawn using pool or instantiate directly
if (useObjectPooling && _obstaclePool != null) if (useObjectPooling && _obstaclePool != null)
{ {
Debug.Log($"[ObstacleSpawner] Requesting obstacle from pool (prefab index {prefabIndex})");
obstacle = _obstaclePool.GetObstacle(prefabIndex); obstacle = _obstaclePool.GetObstacle(prefabIndex);
if (obstacle == null) 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; 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.position = position;
obstacle.transform.rotation = prefab.transform.rotation; obstacle.transform.rotation = prefab.transform.rotation;
obstacle.transform.SetParent(transform); obstacle.transform.SetParent(transform);
Debug.Log($"[ObstacleSpawner] After positioning, obstacle {obstacle.name} active state: {obstacle.activeInHierarchy}");
} }
else else
{ {
Debug.Log($"[ObstacleSpawner] Instantiating new obstacle (pooling disabled)");
obstacle = Instantiate(prefab, position, prefab.transform.rotation, transform); 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 // Configure the obstacle
ConfigureObstacle(obstacle, prefabIndex); ConfigureObstacle(obstacle, prefabIndex);
Debug.Log($"[ObstacleSpawner] After configuration, obstacle {obstacle.name} active state: {obstacle.activeInHierarchy}");
// Track active obstacles // Track active obstacles
_activeObstacles.Add(obstacle); _activeObstacles.Add(obstacle);
// Invoke events // Invoke events
onObstacleSpawned?.Invoke(obstacle); 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}");
} }
/// <summary> /// <summary>
@@ -377,6 +405,8 @@ namespace Minigames.DivingForPictures
{ {
if (obstacle == null) return; if (obstacle == null) return;
Debug.Log($"[ObstacleSpawner] ReturnObstacleToPool called for {obstacle.name}, active state: {obstacle.activeInHierarchy}");
// Remove from active list // Remove from active list
_activeObstacles.Remove(obstacle); _activeObstacles.Remove(obstacle);
@@ -386,25 +416,16 @@ namespace Minigames.DivingForPictures
// Return to pool or destroy // Return to pool or destroy
if (useObjectPooling && _obstaclePool != null) if (useObjectPooling && _obstaclePool != null)
{ {
Debug.Log($"[ObstacleSpawner] Returning {obstacle.name} to pool");
_obstaclePool.ReturnObstacle(obstacle, prefabIndex); _obstaclePool.ReturnObstacle(obstacle, prefabIndex);
} }
else else
{ {
Debug.Log($"[ObstacleSpawner] Destroying {obstacle.name} (pooling disabled)");
Destroy(obstacle); Destroy(obstacle);
} }
} }
/// <summary>
/// Called periodically to trim excess pooled obstacles
/// </summary>
private void TrimExcessPooledObstacles()
{
if (_obstaclePool != null)
{
_obstaclePool.TrimExcess();
}
}
/// <summary> /// <summary>
/// Public method to change spawn interval at runtime /// Public method to change spawn interval at runtime
/// </summary> /// </summary>

View File

@@ -100,7 +100,7 @@ namespace Pooling
/// <returns>An object ready to use</returns> /// <returns>An object ready to use</returns>
public virtual T Get(int prefabIndex) public virtual T Get(int prefabIndex)
{ {
T obj; T obj = null;
// Track usage frequency // Track usage frequency
if (prefabUsageCount.ContainsKey(prefabIndex)) if (prefabUsageCount.ContainsKey(prefabIndex))
@@ -112,27 +112,71 @@ namespace Pooling
prefabUsageCount[prefabIndex] = 1; 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) if (pooledObjects.ContainsKey(prefabIndex) && pooledObjects[prefabIndex].Count > 0)
{ {
obj = pooledObjects[prefabIndex].Pop(); Debug.Log($"[{GetType().Name}] Found {pooledObjects[prefabIndex].Count} objects in pool for prefab index {prefabIndex}");
totalPooledCount--;
// 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 else
{ {
// Create new object without adding to pool Debug.Log($"[{GetType().Name}] No objects in pool for prefab index {prefabIndex}, creating new one");
T prefab = prefabs[prefabIndex];
obj = Instantiate(prefab, transform);
} }
// 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); obj.gameObject.SetActive(true);
Debug.Log($"[{GetType().Name}] After SetActive(true), object {obj.name} state: {obj.gameObject.activeInHierarchy}");
// Call OnSpawn for IPoolable components // Call OnSpawn for IPoolable components
IPoolable poolable = obj.GetComponent<IPoolable>(); IPoolable poolable = obj.GetComponent<IPoolable>();
if (poolable != null) if (poolable != null)
{ {
Debug.Log($"[{GetType().Name}] Calling OnSpawn for object {obj.name}");
poolable.OnSpawn(); 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; return obj;
} }
@@ -152,33 +196,61 @@ namespace Pooling
poolable.OnDespawn(); poolable.OnDespawn();
} }
// Check if we're under the maximum pool size for this prefab type // Always deactivate and parent the object
bool keepObject = totalPooledCount < totalMaxPoolSize; obj.gameObject.SetActive(false);
obj.transform.SetParent(transform);
// Additional constraint: don't keep too many of any single prefab type // Initialize stack if it doesn't exist
if (pooledObjects.ContainsKey(prefabIndex) && if (!pooledObjects.ContainsKey(prefabIndex))
pooledObjects[prefabIndex].Count >= maxPerPrefabPoolSize)
{ {
keepObject = false; pooledObjects[prefabIndex] = new Stack<T>();
} }
if (keepObject) // Check if we need to trim this specific prefab type's pool
if (pooledObjects[prefabIndex].Count >= maxPerPrefabPoolSize)
{ {
obj.gameObject.SetActive(false); // Remove the oldest object from this prefab's pool to make room
obj.transform.SetParent(transform); if (pooledObjects[prefabIndex].Count > 0)
if (!pooledObjects.ContainsKey(prefabIndex))
{ {
pooledObjects[prefabIndex] = new Stack<T>(); 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); if (prefabToTrim >= 0 && pooledObjects[prefabToTrim].Count > 0)
totalPooledCount++; {
} T oldestObj = pooledObjects[prefabToTrim].Pop();
else if (oldestObj != null && oldestObj.gameObject != null)
{ {
Destroy(obj.gameObject); Destroy(oldestObj.gameObject);
totalPooledCount--;
}
}
} }
// Now add the current object to the pool
pooledObjects[prefabIndex].Push(obj);
totalPooledCount++;
} }
/// <summary> /// <summary>