Time-based difficulty scaling with object pools and bird pooper restart improvements to the minigame
This commit is contained in:
@@ -15,11 +15,12 @@ namespace Minigames.BirdPooper
|
||||
public UnityEngine.Events.UnityEvent OnFlap;
|
||||
public UnityEngine.Events.UnityEvent OnPlayerDamaged;
|
||||
|
||||
private Rigidbody2D rb;
|
||||
private IBirdPooperSettings settings;
|
||||
private float verticalVelocity = 0f;
|
||||
private bool isDead = false;
|
||||
private float fixedXPosition; // Store the initial X position from the scene
|
||||
private Rigidbody2D _rb;
|
||||
private IBirdPooperSettings _settings;
|
||||
private float _verticalVelocity;
|
||||
private bool _isDead;
|
||||
private float _fixedXPosition; // Store the initial X position from the scene
|
||||
private bool _isInitialized; // Flag to control when physics/input are active
|
||||
|
||||
internal override void OnManagedAwake()
|
||||
{
|
||||
@@ -31,33 +32,49 @@ namespace Minigames.BirdPooper
|
||||
if (OnPlayerDamaged == null)
|
||||
OnPlayerDamaged = new UnityEngine.Events.UnityEvent();
|
||||
|
||||
// Load settings
|
||||
settings = GameManager.GetSettingsObject<IBirdPooperSettings>();
|
||||
if (settings == null)
|
||||
{
|
||||
Debug.LogError("[BirdPlayerController] BirdPooperSettings not found!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get Rigidbody2D component (Dynamic with gravityScale = 0)
|
||||
rb = GetComponent<Rigidbody2D>();
|
||||
if (rb != null)
|
||||
{
|
||||
rb.gravityScale = 0f; // Disable Unity physics gravity
|
||||
rb.bodyType = RigidbodyType2D.Kinematic; // Kinematic = manual movement, no physics forces
|
||||
|
||||
// Store the initial X position from the scene
|
||||
fixedXPosition = rb.position.x;
|
||||
Debug.Log($"[BirdPlayerController] Fixed X position set to: {fixedXPosition}");
|
||||
}
|
||||
else
|
||||
// Only cache component references - NO setup yet
|
||||
_rb = GetComponent<Rigidbody2D>();
|
||||
if (_rb == null)
|
||||
{
|
||||
Debug.LogError("[BirdPlayerController] Rigidbody2D component not found!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Register as default consumer (gets input if nothing else consumes it)
|
||||
// This allows UI buttons to work while still flapping when tapping empty space
|
||||
// Load settings
|
||||
_settings = GameManager.GetSettingsObject<IBirdPooperSettings>();
|
||||
if (_settings == null)
|
||||
{
|
||||
Debug.LogError("[BirdPlayerController] BirdPooperSettings not found!");
|
||||
}
|
||||
|
||||
Debug.Log("[BirdPlayerController] References cached, waiting for initialization...");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the player controller - enables physics and input.
|
||||
/// Should be called by BirdPooperGameManager when ready to start the game.
|
||||
/// </summary>
|
||||
public void Initialize()
|
||||
{
|
||||
if (_isInitialized)
|
||||
{
|
||||
Debug.LogWarning("[BirdPlayerController] Already initialized!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_rb == null || _settings == null)
|
||||
{
|
||||
Debug.LogError("[BirdPlayerController] Cannot initialize - missing references!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup physics
|
||||
_rb.gravityScale = 0f; // Disable Unity physics gravity
|
||||
_rb.bodyType = RigidbodyType2D.Kinematic; // Kinematic = manual movement, no physics forces
|
||||
|
||||
// Store the initial X position from the scene
|
||||
_fixedXPosition = _rb.position.x;
|
||||
|
||||
// Register as default input consumer
|
||||
if (Input.InputManager.Instance != null)
|
||||
{
|
||||
Input.InputManager.Instance.SetDefaultConsumer(this);
|
||||
@@ -67,52 +84,74 @@ namespace Minigames.BirdPooper
|
||||
{
|
||||
Debug.LogError("[BirdPlayerController] InputManager instance not found!");
|
||||
}
|
||||
|
||||
_isInitialized = true;
|
||||
Debug.Log($"[BirdPlayerController] Initialized! Fixed X position: {_fixedXPosition}");
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!isDead && settings != null && rb != null)
|
||||
{
|
||||
// Apply manual gravity
|
||||
verticalVelocity -= settings.Gravity * Time.deltaTime;
|
||||
|
||||
// Cap fall speed (terminal velocity)
|
||||
if (verticalVelocity < -settings.MaxFallSpeed)
|
||||
verticalVelocity = -settings.MaxFallSpeed;
|
||||
|
||||
// Update position manually
|
||||
Vector2 newPosition = rb.position;
|
||||
newPosition.y += verticalVelocity * Time.deltaTime;
|
||||
newPosition.x = fixedXPosition; // Keep X fixed at scene-configured position
|
||||
|
||||
// Clamp Y position to bounds
|
||||
newPosition.y = Mathf.Clamp(newPosition.y, settings.MinY, settings.MaxY);
|
||||
|
||||
rb.MovePosition(newPosition);
|
||||
|
||||
// Update rotation based on velocity
|
||||
UpdateRotation();
|
||||
}
|
||||
// Only run physics/movement if initialized
|
||||
if (!_isInitialized || _isDead || _settings == null || _rb == null)
|
||||
return;
|
||||
|
||||
// Apply manual gravity
|
||||
_verticalVelocity -= _settings.Gravity * Time.deltaTime;
|
||||
|
||||
// Cap fall speed (terminal velocity)
|
||||
if (_verticalVelocity < -_settings.MaxFallSpeed)
|
||||
_verticalVelocity = -_settings.MaxFallSpeed;
|
||||
|
||||
// Update position manually
|
||||
Vector2 newPosition = _rb.position;
|
||||
newPosition.y += _verticalVelocity * Time.deltaTime;
|
||||
newPosition.x = _fixedXPosition; // Keep X fixed at scene-configured position
|
||||
|
||||
// Clamp Y position to bounds
|
||||
newPosition.y = Mathf.Clamp(newPosition.y, _settings.MinY, _settings.MaxY);
|
||||
|
||||
_rb.MovePosition(newPosition);
|
||||
|
||||
// Update rotation based on velocity
|
||||
UpdateRotation();
|
||||
}
|
||||
|
||||
#region ITouchInputConsumer Implementation
|
||||
|
||||
public void OnTap(Vector2 tapPosition)
|
||||
{
|
||||
if (!isDead && settings != null)
|
||||
{
|
||||
verticalVelocity = settings.FlapForce;
|
||||
Debug.Log($"[BirdPlayerController] Flap! velocity = {verticalVelocity}");
|
||||
|
||||
// Emit flap event
|
||||
OnFlap?.Invoke();
|
||||
}
|
||||
// Only respond to input if initialized and alive
|
||||
if (!_isInitialized || _isDead || _settings == null)
|
||||
return;
|
||||
|
||||
Flap();
|
||||
}
|
||||
|
||||
public void OnHoldStart(Vector2 position) { }
|
||||
public void OnHoldMove(Vector2 position) { }
|
||||
public void OnHoldEnd(Vector2 position) { }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Player Actions
|
||||
|
||||
/// <summary>
|
||||
/// Makes the bird flap, applying upward velocity.
|
||||
/// Can be called by input system or externally (e.g., for first tap).
|
||||
/// </summary>
|
||||
public void Flap()
|
||||
{
|
||||
if (!_isInitialized || _isDead || _settings == null)
|
||||
return;
|
||||
|
||||
_verticalVelocity = _settings.FlapForce;
|
||||
Debug.Log($"[BirdPlayerController] Flap! velocity = {_verticalVelocity}");
|
||||
|
||||
// Emit flap event
|
||||
OnFlap?.Invoke();
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region Rotation
|
||||
@@ -123,19 +162,19 @@ namespace Minigames.BirdPooper
|
||||
/// </summary>
|
||||
private void UpdateRotation()
|
||||
{
|
||||
if (settings == null) return;
|
||||
if (_settings == null) return;
|
||||
|
||||
// Map velocity to rotation angle
|
||||
// When falling at max speed (-MaxFallSpeed): -MaxRotationAngle (down)
|
||||
// When at flap velocity (+FlapForce): +MaxRotationAngle (up)
|
||||
float velocityPercent = Mathf.InverseLerp(
|
||||
-settings.MaxFallSpeed,
|
||||
settings.FlapForce,
|
||||
verticalVelocity
|
||||
-_settings.MaxFallSpeed,
|
||||
_settings.FlapForce,
|
||||
_verticalVelocity
|
||||
);
|
||||
float targetAngle = Mathf.Lerp(
|
||||
-settings.MaxRotationAngle,
|
||||
settings.MaxRotationAngle,
|
||||
-_settings.MaxRotationAngle,
|
||||
_settings.MaxRotationAngle,
|
||||
velocityPercent
|
||||
);
|
||||
|
||||
@@ -148,7 +187,7 @@ namespace Minigames.BirdPooper
|
||||
float smoothedAngle = Mathf.Lerp(
|
||||
currentAngle,
|
||||
targetAngle,
|
||||
settings.RotationSpeed * Time.deltaTime
|
||||
_settings.RotationSpeed * Time.deltaTime
|
||||
);
|
||||
|
||||
// Apply rotation to Z axis only (2D rotation)
|
||||
@@ -175,10 +214,10 @@ namespace Minigames.BirdPooper
|
||||
private void HandleDeath()
|
||||
{
|
||||
// Only process death once
|
||||
if (isDead) return;
|
||||
if (_isDead) return;
|
||||
|
||||
isDead = true;
|
||||
verticalVelocity = 0f;
|
||||
_isDead = true;
|
||||
_verticalVelocity = 0f;
|
||||
Debug.Log("[BirdPlayerController] Bird died!");
|
||||
|
||||
// Emit damage event - let the game manager handle UI
|
||||
@@ -187,9 +226,9 @@ namespace Minigames.BirdPooper
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Properties
|
||||
#region Public Accessors
|
||||
|
||||
public bool IsDead => isDead;
|
||||
public bool IsDead => _isDead;
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -17,12 +17,13 @@ namespace Minigames.BirdPooper
|
||||
[SerializeField] private BirdPlayerController player;
|
||||
[SerializeField] private ObstacleSpawner obstacleSpawner;
|
||||
[SerializeField] private TargetSpawner targetSpawner;
|
||||
[SerializeField] private TapToStartController tapToStartController;
|
||||
[SerializeField] private GameOverScreen gameOverScreen;
|
||||
[SerializeField] private GameObject poopPrefab;
|
||||
|
||||
[Header("Game State")]
|
||||
private int targetsHit;
|
||||
private bool isGameOver;
|
||||
private int _targetsHit;
|
||||
private bool _isGameOver;
|
||||
|
||||
[Header("Input")]
|
||||
[Tooltip("Minimum seconds between consecutive poop spawns")]
|
||||
@@ -60,6 +61,11 @@ namespace Minigames.BirdPooper
|
||||
Debug.LogWarning("[BirdPooperGameManager] TargetSpawner reference not assigned! Targets will not spawn.");
|
||||
}
|
||||
|
||||
if (tapToStartController == null)
|
||||
{
|
||||
Debug.LogError("[BirdPooperGameManager] TapToStartController reference not assigned!");
|
||||
}
|
||||
|
||||
if (gameOverScreen == null)
|
||||
{
|
||||
Debug.LogError("[BirdPooperGameManager] GameOverScreen reference not assigned!");
|
||||
@@ -76,34 +82,76 @@ namespace Minigames.BirdPooper
|
||||
}
|
||||
}
|
||||
|
||||
internal override void OnManagedStart()
|
||||
/// <summary>
|
||||
/// Called after scene is fully loaded and any save data is restored.
|
||||
/// Activates tap-to-start UI instead of starting immediately.
|
||||
/// </summary>
|
||||
internal override void OnSceneRestoreCompleted()
|
||||
{
|
||||
base.OnManagedStart();
|
||||
base.OnSceneRestoreCompleted();
|
||||
|
||||
Debug.Log("[BirdPooperGameManager] Scene fully loaded, activating tap-to-start...");
|
||||
|
||||
if (tapToStartController != null)
|
||||
{
|
||||
tapToStartController.Activate();
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("[BirdPooperGameManager] TapToStartController missing! Starting game immediately as fallback.");
|
||||
BeginMinigame();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Central method to begin the minigame.
|
||||
/// Initializes player, starts spawners, and sets up game state.
|
||||
/// </summary>
|
||||
public void BeginMinigame()
|
||||
{
|
||||
// Initialize game state
|
||||
isGameOver = false;
|
||||
targetsHit = 0;
|
||||
_isGameOver = false;
|
||||
_targetsHit = 0;
|
||||
|
||||
// Subscribe to player events
|
||||
// Initialize and enable player
|
||||
if (player != null)
|
||||
{
|
||||
player.Initialize();
|
||||
player.OnPlayerDamaged.AddListener(HandlePlayerDamaged);
|
||||
Debug.Log("[BirdPooperGameManager] Subscribed to player damaged event");
|
||||
|
||||
// Make bird do initial flap so first tap feels responsive
|
||||
player.Flap();
|
||||
|
||||
Debug.Log("[BirdPooperGameManager] Player initialized and event subscribed");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("[BirdPooperGameManager] Cannot begin minigame - player reference missing!");
|
||||
}
|
||||
|
||||
// Start obstacle spawning
|
||||
if (obstacleSpawner != null)
|
||||
{
|
||||
obstacleSpawner.StartSpawning();
|
||||
Debug.Log("[BirdPooperGameManager] Started obstacle spawning");
|
||||
Debug.Log("[BirdPooperGameManager] Obstacle spawner started");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("[BirdPooperGameManager] Cannot begin minigame - obstacle spawner reference missing!");
|
||||
}
|
||||
|
||||
// Start target spawning
|
||||
if (targetSpawner != null)
|
||||
{
|
||||
targetSpawner.StartSpawning();
|
||||
Debug.Log("[BirdPooperGameManager] Started target spawning");
|
||||
Debug.Log("[BirdPooperGameManager] Target spawner started");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("[BirdPooperGameManager] Target spawner reference missing - targets will not spawn");
|
||||
}
|
||||
|
||||
Debug.Log("[BirdPooperGameManager] ✅ Minigame started successfully!");
|
||||
}
|
||||
|
||||
internal override void OnManagedDestroy()
|
||||
@@ -129,10 +177,10 @@ namespace Minigames.BirdPooper
|
||||
/// </summary>
|
||||
private void HandlePlayerDamaged()
|
||||
{
|
||||
if (isGameOver) return;
|
||||
if (_isGameOver) return;
|
||||
|
||||
isGameOver = true;
|
||||
Debug.Log($"[BirdPooperGameManager] Player damaged - Game Over! Targets Hit: {targetsHit}");
|
||||
_isGameOver = true;
|
||||
Debug.Log($"[BirdPooperGameManager] Player damaged - Game Over! Targets Hit: {_targetsHit}");
|
||||
|
||||
// Stop spawning obstacles
|
||||
if (obstacleSpawner != null)
|
||||
@@ -167,7 +215,7 @@ namespace Minigames.BirdPooper
|
||||
if (Time.time < _lastPoopTime + poopCooldown)
|
||||
return;
|
||||
|
||||
if (isGameOver || player == null || poopPrefab == null)
|
||||
if (_isGameOver || player == null || poopPrefab == null)
|
||||
return;
|
||||
|
||||
Vector3 spawnPosition = player.transform.position;
|
||||
@@ -183,16 +231,17 @@ namespace Minigames.BirdPooper
|
||||
/// </summary>
|
||||
public void OnTargetHit()
|
||||
{
|
||||
if (isGameOver) return;
|
||||
if (_isGameOver) return;
|
||||
|
||||
targetsHit++;
|
||||
Debug.Log($"[BirdPooperGameManager] Target Hit! Total: {targetsHit}");
|
||||
_targetsHit++;
|
||||
Debug.Log($"[BirdPooperGameManager] Target Hit! Total: {_targetsHit}");
|
||||
}
|
||||
|
||||
#region Public Accessors
|
||||
|
||||
public bool IsGameOver => isGameOver;
|
||||
public int TargetsHit => targetsHit;
|
||||
public bool IsGameOver => _isGameOver;
|
||||
public int TargetsHit => _targetsHit;
|
||||
public BirdPlayerController Player => player;
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
111
Assets/Scripts/Minigames/BirdPooper/ObstacleSpawnConfig.cs
Normal file
111
Assets/Scripts/Minigames/BirdPooper/ObstacleSpawnConfig.cs
Normal file
@@ -0,0 +1,111 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Minigames.BirdPooper
|
||||
{
|
||||
/// <summary>
|
||||
/// Container for a pool of obstacle prefabs at a specific difficulty tier.
|
||||
/// Pools are ordered by difficulty, with pool[0] being the easiest.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class ObstaclePool
|
||||
{
|
||||
[Tooltip("Obstacles in this difficulty tier")]
|
||||
public GameObject[] obstacles;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for obstacle spawning in Bird Pooper minigame.
|
||||
/// Includes difficulty pools, spawn timing, and diversity settings.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class ObstacleSpawnConfig
|
||||
{
|
||||
[Header("Difficulty Pools")]
|
||||
[Tooltip("Obstacle pools ordered by difficulty (pool[0] = easiest, always active)")]
|
||||
public ObstaclePool[] obstaclePools;
|
||||
|
||||
[Tooltip("Times (in seconds) when each additional pool unlocks. Length should be obstaclePools.Length - 1. At poolUnlockTimes[i], pool[i+1] becomes available.")]
|
||||
public float[] poolUnlockTimes;
|
||||
|
||||
[Header("Spawn Timing")]
|
||||
[Tooltip("Minimum interval between spawns (seconds) - represents high difficulty")]
|
||||
public float minSpawnInterval = 1f;
|
||||
|
||||
[Tooltip("Maximum interval between spawns (seconds) - represents low difficulty")]
|
||||
public float maxSpawnInterval = 2f;
|
||||
|
||||
[Header("Difficulty Scaling")]
|
||||
[Tooltip("Time in seconds for difficulty to ramp from 0 to 1")]
|
||||
public float difficultyRampDuration = 60f;
|
||||
|
||||
[Tooltip("Curve mapping normalized time (0..1) to difficulty (0..1). Default is linear.")]
|
||||
public AnimationCurve difficultyCurve = AnimationCurve.Linear(0f, 0f, 1f, 1f);
|
||||
|
||||
[Tooltip("Maximum jitter applied to computed interval (fractional). 0.1 = +/-10% jitter")]
|
||||
public float intervalJitter = 0.05f;
|
||||
|
||||
[Header("Recency / Diversity")]
|
||||
[Tooltip("Time in seconds it takes for a recently-used prefab to recover back to full weight")]
|
||||
public float recentDecayDuration = 10f;
|
||||
|
||||
[Tooltip("Minimum weight (0..1) applied to a just-used prefab so it can still appear occasionally")]
|
||||
[Range(0f, 1f)]
|
||||
public float minRecentWeight = 0.05f;
|
||||
|
||||
/// <summary>
|
||||
/// Validates the configuration and logs warnings for invalid settings.
|
||||
/// </summary>
|
||||
public void Validate()
|
||||
{
|
||||
// Validate pools
|
||||
if (obstaclePools == null || obstaclePools.Length == 0)
|
||||
{
|
||||
Debug.LogError("[ObstacleSpawnConfig] No obstacle pools defined!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate pool unlock times
|
||||
int expectedUnlockTimes = obstaclePools.Length - 1;
|
||||
if (poolUnlockTimes == null)
|
||||
{
|
||||
Debug.LogWarning($"[ObstacleSpawnConfig] poolUnlockTimes is null. Expected {expectedUnlockTimes} entries. Only pool[0] will be available.");
|
||||
}
|
||||
else if (poolUnlockTimes.Length != expectedUnlockTimes)
|
||||
{
|
||||
Debug.LogWarning($"[ObstacleSpawnConfig] poolUnlockTimes.Length ({poolUnlockTimes.Length}) does not match expected value ({expectedUnlockTimes}). Should be obstaclePools.Length - 1.");
|
||||
}
|
||||
|
||||
// Validate spawn intervals
|
||||
if (minSpawnInterval < 0f)
|
||||
{
|
||||
Debug.LogWarning("[ObstacleSpawnConfig] minSpawnInterval is negative. Clamping to 0.");
|
||||
}
|
||||
if (maxSpawnInterval < 0f)
|
||||
{
|
||||
Debug.LogWarning("[ObstacleSpawnConfig] maxSpawnInterval is negative. Clamping to 0.");
|
||||
}
|
||||
if (minSpawnInterval > maxSpawnInterval)
|
||||
{
|
||||
Debug.LogWarning("[ObstacleSpawnConfig] minSpawnInterval is greater than maxSpawnInterval. Values should be swapped.");
|
||||
}
|
||||
|
||||
// Validate difficulty ramp
|
||||
if (difficultyRampDuration < 0.01f)
|
||||
{
|
||||
Debug.LogWarning("[ObstacleSpawnConfig] difficultyRampDuration is too small. Should be at least 0.01.");
|
||||
}
|
||||
|
||||
// Validate recency settings
|
||||
if (recentDecayDuration < 0.01f)
|
||||
{
|
||||
Debug.LogWarning("[ObstacleSpawnConfig] recentDecayDuration is too small. Should be at least 0.01.");
|
||||
}
|
||||
if (minRecentWeight < 0f || minRecentWeight > 1f)
|
||||
{
|
||||
Debug.LogWarning("[ObstacleSpawnConfig] minRecentWeight should be between 0 and 1.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 74f4387c76774225afa2d02d590d5ad4
|
||||
timeCreated: 1765918010
|
||||
@@ -4,6 +4,7 @@ using Core.Settings;
|
||||
using Core.Lifecycle;
|
||||
using AppleHillsCamera;
|
||||
using System.Text;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Minigames.BirdPooper
|
||||
{
|
||||
@@ -11,6 +12,7 @@ namespace Minigames.BirdPooper
|
||||
/// Spawns obstacles at regular intervals for Bird Pooper minigame.
|
||||
/// Uses Transform references for spawn and despawn positions instead of hardcoded values.
|
||||
/// All obstacles are spawned at Y = 0 (prefabs should be authored accordingly).
|
||||
/// Supports dynamic difficulty pools that unlock over time.
|
||||
/// </summary>
|
||||
public class ObstacleSpawner : ManagedBehaviour
|
||||
{
|
||||
@@ -20,6 +22,9 @@ namespace Minigames.BirdPooper
|
||||
|
||||
[Tooltip("Transform marking where obstacles despawn (off-screen left)")]
|
||||
[SerializeField] private Transform despawnPoint;
|
||||
|
||||
[Tooltip("Optional parent transform for spawned obstacles (for scene organization)")]
|
||||
[SerializeField] private Transform obstacleContainer;
|
||||
|
||||
[Header("EdgeAnchor References")]
|
||||
[Tooltip("ScreenReferenceMarker to pass to spawned obstacles")]
|
||||
@@ -28,55 +33,45 @@ namespace Minigames.BirdPooper
|
||||
[Tooltip("CameraScreenAdapter to pass to spawned obstacles")]
|
||||
[SerializeField] private CameraScreenAdapter cameraAdapter;
|
||||
|
||||
[Header("Obstacle Prefabs")]
|
||||
[Tooltip("Array of obstacle prefabs to spawn randomly")]
|
||||
[SerializeField] private GameObject[] obstaclePrefabs;
|
||||
|
||||
[Header("Spawn Timing")]
|
||||
[Tooltip("Minimum interval between spawns (seconds)")]
|
||||
[SerializeField] private float minSpawnInterval = 1f;
|
||||
[Tooltip("Maximum interval between spawns (seconds)")]
|
||||
[SerializeField] private float maxSpawnInterval = 2f;
|
||||
|
||||
[Header("Difficulty")]
|
||||
[Tooltip("Time in seconds for difficulty to ramp from 0 to 1")]
|
||||
[SerializeField] private float difficultyRampDuration = 60f;
|
||||
[Tooltip("Curve mapping normalized time (0..1) to difficulty (0..1). Default is linear.")]
|
||||
[SerializeField] private AnimationCurve difficultyCurve = AnimationCurve.Linear(0f, 0f, 1f, 1f);
|
||||
[Tooltip("Maximum jitter applied to computed interval (fractional). 0.1 = +/-10% jitter")]
|
||||
[SerializeField] private float intervalJitter = 0.05f;
|
||||
|
||||
[Header("Recency / Diversity")]
|
||||
[Tooltip("Time in seconds it takes for a recently-used prefab to recover back to full weight")]
|
||||
[SerializeField] private float recentDecayDuration = 10f;
|
||||
[Tooltip("Minimum weight (0..1) applied to a just-used prefab so it can still appear occasionally")]
|
||||
[Range(0f, 1f)]
|
||||
[SerializeField] private float minRecentWeight = 0.05f;
|
||||
|
||||
private IBirdPooperSettings settings;
|
||||
private float spawnTimer;
|
||||
private bool isSpawning;
|
||||
private IBirdPooperSettings _settings;
|
||||
private ObstacleSpawnConfig _spawnConfig;
|
||||
private float _spawnTimer;
|
||||
private bool _isSpawning;
|
||||
private float _currentSpawnInterval = 1f;
|
||||
|
||||
// difficulty tracking
|
||||
private float _elapsedTime = 0f;
|
||||
// Difficulty tracking
|
||||
private float _elapsedTime;
|
||||
|
||||
// recency tracking
|
||||
// Master obstacle list for recency tracking
|
||||
private List<GameObject> _allObstacles;
|
||||
private Dictionary<GameObject, int> _obstacleToGlobalIndex;
|
||||
private float[] _lastUsedTimes;
|
||||
|
||||
internal override void OnManagedAwake()
|
||||
/// <summary>
|
||||
/// Initializes the obstacle spawner by loading settings, validating references, and building obstacle pools.
|
||||
/// Should be called once before spawning begins.
|
||||
/// </summary>
|
||||
private void Initialize()
|
||||
{
|
||||
base.OnManagedAwake();
|
||||
|
||||
// Load settings
|
||||
settings = GameManager.GetSettingsObject<IBirdPooperSettings>();
|
||||
if (settings == null)
|
||||
_settings = GameManager.GetSettingsObject<IBirdPooperSettings>();
|
||||
if (_settings == null)
|
||||
{
|
||||
Debug.LogError("[ObstacleSpawner] BirdPooperSettings not found!");
|
||||
// continue — we now use min/max interval fields instead of relying on settings
|
||||
Debug.LogError("[ObstacleSpawner] BirdPooperSettings not found! Cannot initialize.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate references
|
||||
_spawnConfig = _settings.ObstacleSpawnConfiguration;
|
||||
if (_spawnConfig == null)
|
||||
{
|
||||
Debug.LogError("[ObstacleSpawner] ObstacleSpawnConfiguration not found in settings! Cannot initialize.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate spawn configuration
|
||||
_spawnConfig.Validate();
|
||||
|
||||
// Validate scene references
|
||||
if (spawnPoint == null)
|
||||
{
|
||||
Debug.LogError("[ObstacleSpawner] Spawn Point not assigned! Please assign a Transform in the Inspector.");
|
||||
@@ -87,11 +82,6 @@ namespace Minigames.BirdPooper
|
||||
Debug.LogError("[ObstacleSpawner] Despawn Point not assigned! Please assign a Transform in the Inspector.");
|
||||
}
|
||||
|
||||
if (obstaclePrefabs == null || obstaclePrefabs.Length == 0)
|
||||
{
|
||||
Debug.LogError("[ObstacleSpawner] No obstacle prefabs assigned! Please assign at least one prefab in the Inspector.");
|
||||
}
|
||||
|
||||
if (referenceMarker == null)
|
||||
{
|
||||
Debug.LogError("[ObstacleSpawner] ScreenReferenceMarker not assigned! Obstacles need this for EdgeAnchor positioning.");
|
||||
@@ -102,58 +92,100 @@ namespace Minigames.BirdPooper
|
||||
Debug.LogWarning("[ObstacleSpawner] CameraScreenAdapter not assigned. EdgeAnchor will attempt to auto-find camera.");
|
||||
}
|
||||
|
||||
// Validate interval range
|
||||
if (minSpawnInterval < 0f) minSpawnInterval = 0f;
|
||||
if (maxSpawnInterval < 0f) maxSpawnInterval = 0f;
|
||||
if (minSpawnInterval > maxSpawnInterval)
|
||||
// Build master obstacle list from all pools
|
||||
BuildMasterObstacleList();
|
||||
|
||||
Debug.Log("[ObstacleSpawner] Initialized successfully with pool-based difficulty scaling");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a master list of all obstacles across all pools and creates index mappings for recency tracking.
|
||||
/// </summary>
|
||||
private void BuildMasterObstacleList()
|
||||
{
|
||||
_allObstacles = new List<GameObject>();
|
||||
_obstacleToGlobalIndex = new Dictionary<GameObject, int>();
|
||||
|
||||
if (_spawnConfig.obstaclePools == null || _spawnConfig.obstaclePools.Length == 0)
|
||||
{
|
||||
float tmp = minSpawnInterval;
|
||||
minSpawnInterval = maxSpawnInterval;
|
||||
maxSpawnInterval = tmp;
|
||||
Debug.LogWarning("[ObstacleSpawner] minSpawnInterval was greater than maxSpawnInterval. Values were swapped.");
|
||||
Debug.LogError("[ObstacleSpawner] No obstacle pools defined in configuration!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Clamp ramp duration
|
||||
if (difficultyRampDuration < 0.01f) difficultyRampDuration = 0.01f;
|
||||
int globalIndex = 0;
|
||||
for (int poolIdx = 0; poolIdx < _spawnConfig.obstaclePools.Length; poolIdx++)
|
||||
{
|
||||
ObstaclePool pool = _spawnConfig.obstaclePools[poolIdx];
|
||||
if (pool == null || pool.obstacles == null || pool.obstacles.Length == 0)
|
||||
{
|
||||
Debug.LogWarning($"[ObstacleSpawner] Pool[{poolIdx}] is empty or null!");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Clamp recency
|
||||
if (recentDecayDuration < 0.01f) recentDecayDuration = 0.01f;
|
||||
if (minRecentWeight < 0f) minRecentWeight = 0f;
|
||||
if (minRecentWeight > 1f) minRecentWeight = 1f;
|
||||
foreach (GameObject prefab in pool.obstacles)
|
||||
{
|
||||
if (prefab == null)
|
||||
{
|
||||
Debug.LogWarning($"[ObstacleSpawner] Null prefab found in pool[{poolIdx}]");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Initialize last-used timestamps so prefabs start available (set to sufficiently negative so they appear with full weight)
|
||||
int n = obstaclePrefabs != null ? obstaclePrefabs.Length : 0;
|
||||
_lastUsedTimes = new float[n];
|
||||
float initTime = -recentDecayDuration - 1f;
|
||||
for (int i = 0; i < n; i++) _lastUsedTimes[i] = initTime;
|
||||
// Allow duplicates - same prefab can appear in multiple pools
|
||||
if (!_obstacleToGlobalIndex.ContainsKey(prefab))
|
||||
{
|
||||
_obstacleToGlobalIndex[prefab] = globalIndex;
|
||||
_allObstacles.Add(prefab);
|
||||
globalIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Log("[ObstacleSpawner] Initialized successfully");
|
||||
// Initialize recency tracking
|
||||
int totalObstacles = _allObstacles.Count;
|
||||
_lastUsedTimes = new float[totalObstacles];
|
||||
float initTime = Time.time - _spawnConfig.recentDecayDuration - 1f;
|
||||
for (int i = 0; i < totalObstacles; i++)
|
||||
{
|
||||
_lastUsedTimes[i] = initTime;
|
||||
}
|
||||
|
||||
// Log pool configuration
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.AppendLine($"[ObstacleSpawner] Loaded {_spawnConfig.obstaclePools.Length} obstacle pools with {totalObstacles} unique obstacles:");
|
||||
for (int i = 0; i < _spawnConfig.obstaclePools.Length; i++)
|
||||
{
|
||||
ObstaclePool pool = _spawnConfig.obstaclePools[i];
|
||||
int obstacleCount = pool != null && pool.obstacles != null ? pool.obstacles.Length : 0;
|
||||
float unlockTime = (i == 0) ? 0f : (_spawnConfig.poolUnlockTimes != null && i - 1 < _spawnConfig.poolUnlockTimes.Length ? _spawnConfig.poolUnlockTimes[i - 1] : -1f);
|
||||
sb.AppendLine($" Pool[{i}]: {obstacleCount} obstacles, unlocks at {unlockTime}s");
|
||||
}
|
||||
Debug.Log(sb.ToString());
|
||||
}
|
||||
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!isSpawning || spawnPoint == null) return;
|
||||
if (!_isSpawning || spawnPoint == null || _spawnConfig == null) return;
|
||||
|
||||
spawnTimer += Time.deltaTime;
|
||||
_spawnTimer += Time.deltaTime;
|
||||
_elapsedTime += Time.deltaTime;
|
||||
|
||||
if (spawnTimer >= _currentSpawnInterval)
|
||||
if (_spawnTimer >= _currentSpawnInterval)
|
||||
{
|
||||
SpawnObstacle();
|
||||
spawnTimer = 0f;
|
||||
_spawnTimer = 0f;
|
||||
|
||||
// pick next interval based on difficulty ramp
|
||||
float t = Mathf.Clamp01(_elapsedTime / difficultyRampDuration);
|
||||
float difficulty = difficultyCurve.Evaluate(t); // 0..1
|
||||
// Pick next interval based on difficulty ramp
|
||||
float t = Mathf.Clamp01(_elapsedTime / _spawnConfig.difficultyRampDuration);
|
||||
float difficulty = _spawnConfig.difficultyCurve.Evaluate(t); // 0..1
|
||||
|
||||
// map difficulty to interval: difficulty 0 -> max interval (easy), 1 -> min interval (hard)
|
||||
float baseInterval = Mathf.Lerp(maxSpawnInterval, minSpawnInterval, difficulty);
|
||||
// Map difficulty to interval: difficulty 0 -> max interval (easy), 1 -> min interval (hard)
|
||||
float baseInterval = Mathf.Lerp(_spawnConfig.maxSpawnInterval, _spawnConfig.minSpawnInterval, difficulty);
|
||||
|
||||
// apply small jitter
|
||||
if (intervalJitter > 0f)
|
||||
// Apply small jitter
|
||||
if (_spawnConfig.intervalJitter > 0f)
|
||||
{
|
||||
float jitter = Random.Range(-intervalJitter, intervalJitter);
|
||||
float jitter = Random.Range(-_spawnConfig.intervalJitter, _spawnConfig.intervalJitter);
|
||||
_currentSpawnInterval = Mathf.Max(0f, baseInterval * (1f + jitter));
|
||||
}
|
||||
else
|
||||
@@ -167,14 +199,14 @@ namespace Minigames.BirdPooper
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawn a random obstacle at the spawn point position (Y = 0).
|
||||
/// Spawn a random obstacle from currently unlocked pools at the spawn point position (Y = 0).
|
||||
/// Uses timestamp/decay weighting so prefabs used recently are less likely.
|
||||
/// </summary>
|
||||
private void SpawnObstacle()
|
||||
{
|
||||
if (obstaclePrefabs == null || obstaclePrefabs.Length == 0)
|
||||
if (_spawnConfig == null || _spawnConfig.obstaclePools == null || _spawnConfig.obstaclePools.Length == 0)
|
||||
{
|
||||
Debug.LogWarning("[ObstacleSpawner] No obstacle prefabs to spawn!");
|
||||
Debug.LogWarning("[ObstacleSpawner] No obstacle pools configured!");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -184,53 +216,133 @@ namespace Minigames.BirdPooper
|
||||
return;
|
||||
}
|
||||
|
||||
int count = obstaclePrefabs.Length;
|
||||
|
||||
// Defensive: ensure _lastUsedTimes is initialized and matches prefab count
|
||||
if (_lastUsedTimes == null || _lastUsedTimes.Length != count)
|
||||
// Determine which pools are currently unlocked based on elapsed time
|
||||
int unlockedPoolCount = 1; // Pool[0] is always unlocked
|
||||
if (_spawnConfig.poolUnlockTimes != null)
|
||||
{
|
||||
_lastUsedTimes = new float[count];
|
||||
float initTime = Time.time - recentDecayDuration - 1f;
|
||||
for (int i = 0; i < count; i++) _lastUsedTimes[i] = initTime;
|
||||
for (int i = 0; i < _spawnConfig.poolUnlockTimes.Length; i++)
|
||||
{
|
||||
if (_elapsedTime >= _spawnConfig.poolUnlockTimes[i])
|
||||
{
|
||||
unlockedPoolCount = i + 2; // +2 because we're unlocking pool[i+1]
|
||||
}
|
||||
else
|
||||
{
|
||||
break; // Times should be in order, so stop when we hit a future unlock
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// compute weights based on recency (newer = lower weight)
|
||||
float[] weights = new float[count];
|
||||
// Clamp to available pools
|
||||
unlockedPoolCount = Mathf.Min(unlockedPoolCount, _spawnConfig.obstaclePools.Length);
|
||||
|
||||
// Build list of available obstacles from unlocked pools
|
||||
List<GameObject> availableObstacles = new List<GameObject>();
|
||||
List<int> availableGlobalIndices = new List<int>();
|
||||
|
||||
for (int poolIdx = 0; poolIdx < unlockedPoolCount; poolIdx++)
|
||||
{
|
||||
ObstaclePool pool = _spawnConfig.obstaclePools[poolIdx];
|
||||
if (pool == null || pool.obstacles == null) continue;
|
||||
|
||||
foreach (GameObject prefab in pool.obstacles)
|
||||
{
|
||||
if (prefab == null) continue;
|
||||
|
||||
// Add to available list (duplicates allowed if same prefab is in multiple pools)
|
||||
availableObstacles.Add(prefab);
|
||||
|
||||
// Look up global index for recency tracking
|
||||
if (_obstacleToGlobalIndex.TryGetValue(prefab, out int globalIdx))
|
||||
{
|
||||
availableGlobalIndices.Add(globalIdx);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"[ObstacleSpawner] Prefab '{prefab.name}' not found in global index!");
|
||||
availableGlobalIndices.Add(-1); // Invalid index
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (availableObstacles.Count == 0)
|
||||
{
|
||||
Debug.LogWarning($"[ObstacleSpawner] No obstacles available in unlocked pools (0..{unlockedPoolCount-1})");
|
||||
return;
|
||||
}
|
||||
|
||||
// Compute weights based on recency
|
||||
float[] weights = new float[availableObstacles.Count];
|
||||
float now = Time.time;
|
||||
for (int i = 0; i < count; i++)
|
||||
|
||||
for (int i = 0; i < availableObstacles.Count; i++)
|
||||
{
|
||||
float age = now - _lastUsedTimes[i];
|
||||
float normalized = Mathf.Clamp01(age / recentDecayDuration); // 0 = just used, 1 = fully recovered
|
||||
float weight = Mathf.Max(minRecentWeight, normalized); // ensure minimum probability
|
||||
weights[i] = weight; // base weight = 1.0, could be extended to per-prefab weights
|
||||
int globalIdx = availableGlobalIndices[i];
|
||||
if (globalIdx < 0 || globalIdx >= _lastUsedTimes.Length)
|
||||
{
|
||||
weights[i] = 1f; // Default weight for invalid indices
|
||||
continue;
|
||||
}
|
||||
|
||||
float age = now - _lastUsedTimes[globalIdx];
|
||||
float normalized = Mathf.Clamp01(age / _spawnConfig.recentDecayDuration); // 0 = just used, 1 = fully recovered
|
||||
float weight = Mathf.Max(_spawnConfig.minRecentWeight, normalized);
|
||||
weights[i] = weight;
|
||||
}
|
||||
|
||||
// compute probabilities for logging
|
||||
// Compute and log probabilities for debugging
|
||||
float totalW = 0f;
|
||||
for (int i = 0; i < count; i++) totalW += Mathf.Max(0f, weights[i]);
|
||||
for (int i = 0; i < availableObstacles.Count; i++)
|
||||
{
|
||||
totalW += Mathf.Max(0f, weights[i]);
|
||||
}
|
||||
|
||||
if (totalW > 0f)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append("[ObstacleSpawner] Prefab pick probabilities: ");
|
||||
for (int i = 0; i < count; i++)
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.Append($"[ObstacleSpawner] Spawning from pools 0-{unlockedPoolCount-1}. Probabilities: ");
|
||||
for (int i = 0; i < availableObstacles.Count; i++)
|
||||
{
|
||||
float p = weights[i] / totalW;
|
||||
string name = obstaclePrefabs[i] != null ? obstaclePrefabs[i].name : i.ToString();
|
||||
sb.AppendFormat("{0}:{1:P1}", name, p);
|
||||
if (i < count - 1) sb.Append(", ");
|
||||
string prefabName = availableObstacles[i] != null ? availableObstacles[i].name : i.ToString();
|
||||
sb.AppendFormat("{0}:{1:P1}", prefabName, p);
|
||||
if (i < availableObstacles.Count - 1) sb.Append(", ");
|
||||
}
|
||||
Debug.Log(sb.ToString());
|
||||
}
|
||||
|
||||
// Select obstacle using weighted random
|
||||
int chosenIndex = WeightedPickIndex(weights);
|
||||
GameObject selectedPrefab = obstaclePrefabs[chosenIndex];
|
||||
GameObject selectedPrefab = availableObstacles[chosenIndex];
|
||||
int selectedGlobalIndex = availableGlobalIndices[chosenIndex];
|
||||
|
||||
// record usage timestamp
|
||||
_lastUsedTimes[chosenIndex] = Time.time;
|
||||
// Record usage timestamp for recency tracking
|
||||
if (selectedGlobalIndex >= 0 && selectedGlobalIndex < _lastUsedTimes.Length)
|
||||
{
|
||||
_lastUsedTimes[selectedGlobalIndex] = Time.time;
|
||||
}
|
||||
|
||||
// Determine which pool this obstacle came from (for logging)
|
||||
int sourcePool = -1;
|
||||
for (int poolIdx = 0; poolIdx < unlockedPoolCount; poolIdx++)
|
||||
{
|
||||
ObstaclePool pool = _spawnConfig.obstaclePools[poolIdx];
|
||||
if (pool != null && pool.obstacles != null && System.Array.IndexOf(pool.obstacles, selectedPrefab) >= 0)
|
||||
{
|
||||
sourcePool = poolIdx;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Spawn at spawn point position with Y = 0
|
||||
Vector3 spawnPosition = new Vector3(spawnPoint.position.x, 0f, 0f);
|
||||
GameObject obstacleObj = Instantiate(selectedPrefab, spawnPosition, Quaternion.identity);
|
||||
|
||||
// Parent to container if provided
|
||||
if (obstacleContainer != null)
|
||||
{
|
||||
obstacleObj.transform.SetParent(obstacleContainer, true);
|
||||
}
|
||||
|
||||
// Initialize obstacle with despawn X position and EdgeAnchor references
|
||||
Obstacle obstacle = obstacleObj.GetComponent<Obstacle>();
|
||||
@@ -244,7 +356,7 @@ namespace Minigames.BirdPooper
|
||||
Destroy(obstacleObj);
|
||||
}
|
||||
|
||||
Debug.Log($"[ObstacleSpawner] Spawned obstacle '{selectedPrefab.name}' at position {spawnPosition}");
|
||||
Debug.Log($"[ObstacleSpawner] Spawned '{selectedPrefab.name}' from pool[{sourcePool}] at {spawnPosition}");
|
||||
}
|
||||
|
||||
private int WeightedPickIndex(float[] weights)
|
||||
@@ -275,20 +387,45 @@ namespace Minigames.BirdPooper
|
||||
|
||||
/// <summary>
|
||||
/// Start spawning obstacles.
|
||||
/// Initializes the spawner if not already initialized, then begins spawning logic.
|
||||
/// Spawns the first obstacle immediately, then continues with interval-based spawning.
|
||||
/// </summary>
|
||||
public void StartSpawning()
|
||||
{
|
||||
isSpawning = true;
|
||||
spawnTimer = 0f;
|
||||
// Initialize if not already done
|
||||
if (_spawnConfig == null)
|
||||
{
|
||||
Initialize();
|
||||
}
|
||||
|
||||
// Ensure initialization was successful
|
||||
if (_spawnConfig == null)
|
||||
{
|
||||
Debug.LogError("[ObstacleSpawner] Cannot start spawning - initialization failed!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Begin the spawning process
|
||||
BeginSpawningObstacles();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal method that handles the actual spawning startup logic.
|
||||
/// Sets initial state, computes first interval, and spawns the first obstacle.
|
||||
/// </summary>
|
||||
private void BeginSpawningObstacles()
|
||||
{
|
||||
_isSpawning = true;
|
||||
_spawnTimer = 0f;
|
||||
_elapsedTime = 0f;
|
||||
|
||||
// choose initial interval based on difficulty (at time 0)
|
||||
float initialDifficulty = difficultyCurve.Evaluate(0f);
|
||||
float initialBase = Mathf.Lerp(maxSpawnInterval, minSpawnInterval, initialDifficulty);
|
||||
if (intervalJitter > 0f)
|
||||
// Choose initial interval based on difficulty (at time 0)
|
||||
float initialDifficulty = _spawnConfig.difficultyCurve.Evaluate(0f);
|
||||
float initialBase = Mathf.Lerp(_spawnConfig.maxSpawnInterval, _spawnConfig.minSpawnInterval, initialDifficulty);
|
||||
|
||||
if (_spawnConfig.intervalJitter > 0f)
|
||||
{
|
||||
float jitter = Random.Range(-intervalJitter, intervalJitter);
|
||||
float jitter = Random.Range(-_spawnConfig.intervalJitter, _spawnConfig.intervalJitter);
|
||||
_currentSpawnInterval = Mathf.Max(0f, initialBase * (1f + jitter));
|
||||
}
|
||||
else
|
||||
@@ -310,14 +447,14 @@ namespace Minigames.BirdPooper
|
||||
/// </summary>
|
||||
public void StopSpawning()
|
||||
{
|
||||
isSpawning = false;
|
||||
_isSpawning = false;
|
||||
Debug.Log("[ObstacleSpawner] Stopped spawning");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if spawner is currently active.
|
||||
/// </summary>
|
||||
public bool IsSpawning => isSpawning;
|
||||
public bool IsSpawning => _isSpawning;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
/// <summary>
|
||||
|
||||
153
Assets/Scripts/Minigames/BirdPooper/TapToStartController.cs
Normal file
153
Assets/Scripts/Minigames/BirdPooper/TapToStartController.cs
Normal file
@@ -0,0 +1,153 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using Core.Lifecycle;
|
||||
using Pixelplacement.TweenSystem;
|
||||
using Utils;
|
||||
|
||||
namespace Minigames.BirdPooper
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages "tap to start" flow for Bird Pooper minigame.
|
||||
/// Shows blinking finger UI, waits for first tap, then starts the game.
|
||||
/// </summary>
|
||||
public class TapToStartController : ManagedBehaviour, ITouchInputConsumer
|
||||
{
|
||||
[Header("UI References")]
|
||||
[SerializeField] private GameObject fingerContainer;
|
||||
[SerializeField] private Image fingerImage;
|
||||
|
||||
[Header("Animation Settings")]
|
||||
[Tooltip("Duration for one complete fade in/out cycle")]
|
||||
[SerializeField] private float blinkDuration = 1.5f;
|
||||
[Tooltip("Minimum alpha value during blink")]
|
||||
[SerializeField] private float minAlpha = 0.3f;
|
||||
[Tooltip("Maximum alpha value during blink")]
|
||||
[SerializeField] private float maxAlpha = 1f;
|
||||
|
||||
private bool _isWaitingForTap;
|
||||
private TweenBase _blinkTween;
|
||||
|
||||
internal override void OnManagedAwake()
|
||||
{
|
||||
base.OnManagedAwake();
|
||||
|
||||
// Validate references
|
||||
if (fingerContainer == null)
|
||||
{
|
||||
Debug.LogError("[TapToStartController] Finger container not assigned!");
|
||||
}
|
||||
|
||||
if (fingerImage == null)
|
||||
{
|
||||
Debug.LogError("[TapToStartController] Finger image not assigned!");
|
||||
}
|
||||
|
||||
// Start hidden
|
||||
if (fingerContainer != null)
|
||||
{
|
||||
fingerContainer.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Activates the tap-to-start UI and begins waiting for player input.
|
||||
/// Called by BirdPooperGameManager when scene is ready.
|
||||
/// </summary>
|
||||
public void Activate()
|
||||
{
|
||||
if (_isWaitingForTap)
|
||||
{
|
||||
Debug.LogWarning("[TapToStartController] Already waiting for tap!");
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Log("[TapToStartController] Activating tap-to-start...");
|
||||
|
||||
_isWaitingForTap = true;
|
||||
|
||||
// Show finger UI
|
||||
if (fingerContainer != null)
|
||||
{
|
||||
fingerContainer.SetActive(true);
|
||||
}
|
||||
|
||||
// Start blinking animation using tween utility
|
||||
if (fingerImage != null)
|
||||
{
|
||||
_blinkTween = TweenAnimationUtility.StartBlinkImage(fingerImage, minAlpha, maxAlpha, blinkDuration);
|
||||
}
|
||||
|
||||
// Register as high-priority input consumer to catch first tap
|
||||
if (Input.InputManager.Instance != null)
|
||||
{
|
||||
Input.InputManager.Instance.SetDefaultConsumer(this);
|
||||
Debug.Log("[TapToStartController] Registered as input consumer");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("[TapToStartController] InputManager instance not found!");
|
||||
}
|
||||
}
|
||||
|
||||
#region ITouchInputConsumer Implementation
|
||||
|
||||
public void OnTap(Vector2 tapPosition)
|
||||
{
|
||||
if (!_isWaitingForTap) return;
|
||||
|
||||
Debug.Log("[TapToStartController] First tap received! Starting game...");
|
||||
|
||||
// Stop waiting for tap
|
||||
_isWaitingForTap = false;
|
||||
|
||||
// Stop blinking animation
|
||||
if (_blinkTween != null)
|
||||
{
|
||||
_blinkTween.Stop();
|
||||
_blinkTween = null;
|
||||
}
|
||||
|
||||
// Hide finger UI
|
||||
if (fingerContainer != null)
|
||||
{
|
||||
fingerContainer.SetActive(false);
|
||||
}
|
||||
|
||||
// Unregister from input system
|
||||
if (Input.InputManager.Instance != null)
|
||||
{
|
||||
Input.InputManager.Instance.SetDefaultConsumer(null);
|
||||
Debug.Log("[TapToStartController] Unregistered from input");
|
||||
}
|
||||
|
||||
// Tell game manager to start the game (it will handle the initial flap)
|
||||
BirdPooperGameManager.Instance.BeginMinigame();
|
||||
|
||||
}
|
||||
|
||||
public void OnHoldStart(Vector2 position) { }
|
||||
public void OnHoldMove(Vector2 position) { }
|
||||
public void OnHoldEnd(Vector2 position) { }
|
||||
|
||||
#endregion
|
||||
|
||||
internal override void OnManagedDestroy()
|
||||
{
|
||||
// Stop blinking animation if active
|
||||
if (_blinkTween != null)
|
||||
{
|
||||
_blinkTween.Stop();
|
||||
_blinkTween = null;
|
||||
}
|
||||
|
||||
// Unregister from input if still registered
|
||||
if (_isWaitingForTap && Input.InputManager.Instance != null)
|
||||
{
|
||||
Input.InputManager.Instance.SetDefaultConsumer(null);
|
||||
}
|
||||
|
||||
base.OnManagedDestroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2a6ee5aca3ca423c82b57e16c0b2cca3
|
||||
timeCreated: 1765922092
|
||||
@@ -31,9 +31,9 @@ namespace Minigames.BirdPooper
|
||||
[Tooltip("Array of target prefabs to spawn randomly")]
|
||||
[SerializeField] private GameObject[] targetPrefabs;
|
||||
|
||||
private IBirdPooperSettings settings;
|
||||
private float spawnTimer;
|
||||
private bool isSpawning;
|
||||
private IBirdPooperSettings _settings;
|
||||
private float _spawnTimer;
|
||||
private bool _isSpawning;
|
||||
private float _currentTargetInterval = 1f;
|
||||
|
||||
[Header("Spawn Timing")]
|
||||
@@ -42,16 +42,17 @@ namespace Minigames.BirdPooper
|
||||
[Tooltip("Maximum interval between target spawns (seconds)")]
|
||||
[SerializeField] private float maxTargetSpawnInterval = 2f;
|
||||
|
||||
internal override void OnManagedAwake()
|
||||
/// <summary>
|
||||
/// Initializes the target spawner by loading settings and validating references.
|
||||
/// Should be called once before spawning begins.
|
||||
/// </summary>
|
||||
private void Initialize()
|
||||
{
|
||||
base.OnManagedAwake();
|
||||
|
||||
// Load settings
|
||||
settings = GameManager.GetSettingsObject<IBirdPooperSettings>();
|
||||
if (settings == null)
|
||||
_settings = GameManager.GetSettingsObject<IBirdPooperSettings>();
|
||||
if (_settings == null)
|
||||
{
|
||||
Debug.LogError("[TargetSpawner] BirdPooperSettings not found!");
|
||||
// continue – we'll use inspector intervals
|
||||
Debug.LogWarning("[TargetSpawner] BirdPooperSettings not found! Using inspector intervals.");
|
||||
}
|
||||
|
||||
// Validate interval range
|
||||
@@ -96,15 +97,15 @@ namespace Minigames.BirdPooper
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!isSpawning)
|
||||
if (!_isSpawning)
|
||||
return;
|
||||
|
||||
spawnTimer += Time.deltaTime;
|
||||
_spawnTimer += Time.deltaTime;
|
||||
|
||||
if (spawnTimer >= _currentTargetInterval)
|
||||
if (_spawnTimer >= _currentTargetInterval)
|
||||
{
|
||||
SpawnTarget();
|
||||
spawnTimer = 0f;
|
||||
_spawnTimer = 0f;
|
||||
// pick next random interval
|
||||
_currentTargetInterval = Random.Range(minTargetSpawnInterval, maxTargetSpawnInterval);
|
||||
}
|
||||
@@ -167,15 +168,34 @@ namespace Minigames.BirdPooper
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start spawning targets at regular intervals.
|
||||
/// Start spawning targets.
|
||||
/// Initializes the spawner if not already initialized, then begins spawning logic.
|
||||
/// </summary>
|
||||
public void StartSpawning()
|
||||
{
|
||||
isSpawning = true;
|
||||
spawnTimer = 0f;
|
||||
// choose initial interval
|
||||
// Initialize if not already done
|
||||
if (_settings == null)
|
||||
{
|
||||
Initialize();
|
||||
}
|
||||
|
||||
// Begin the spawning process
|
||||
BeginSpawningTargets();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal method that handles the actual spawning startup logic.
|
||||
/// Sets initial state and computes first interval.
|
||||
/// </summary>
|
||||
private void BeginSpawningTargets()
|
||||
{
|
||||
_isSpawning = true;
|
||||
_spawnTimer = 0f;
|
||||
|
||||
// Choose initial interval
|
||||
_currentTargetInterval = Random.Range(minTargetSpawnInterval, maxTargetSpawnInterval);
|
||||
Debug.Log("[TargetSpawner] Started spawning targets");
|
||||
|
||||
Debug.Log($"[TargetSpawner] Started spawning targets with interval {_currentTargetInterval:F2}s");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -183,14 +203,14 @@ namespace Minigames.BirdPooper
|
||||
/// </summary>
|
||||
public void StopSpawning()
|
||||
{
|
||||
isSpawning = false;
|
||||
_isSpawning = false;
|
||||
Debug.Log("[TargetSpawner] Stopped spawning targets");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if spawner is currently spawning.
|
||||
/// </summary>
|
||||
public bool IsSpawning => isSpawning;
|
||||
public bool IsSpawning => _isSpawning;
|
||||
|
||||
/// <summary>
|
||||
/// Draw gizmos to visualize spawn and despawn points in the editor.
|
||||
|
||||
Reference in New Issue
Block a user