using Core; using Core.Lifecycle; using Core.Settings; using UnityEngine; namespace Minigames.BirdPooper { /// /// Bird player controller with Flappy Bird-style flight mechanics. /// Responds to tap input to flap, with manual gravity simulation. /// public class BirdPlayerController : ManagedBehaviour, ITouchInputConsumer { [Header("Events")] public UnityEngine.Events.UnityEvent OnFlap; public UnityEngine.Events.UnityEvent OnPlayerDamaged; [Header("Flap Cooldown")] [Tooltip("Minimum seconds between flaps to prevent multi-tap issues on mobile")] [SerializeField] private float flapCooldown = 0.15f; 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 private float _lastFlapTime = -Mathf.Infinity; // Track last flap time for cooldown // Diagnostic tracking private int _updateFrameCount = 0; private float _maxDeltaTime = 0f; private float _minDeltaTime = float.MaxValue; internal override void OnManagedAwake() { base.OnManagedAwake(); // Initialize events if (OnFlap == null) OnFlap = new UnityEngine.Events.UnityEvent(); if (OnPlayerDamaged == null) OnPlayerDamaged = new UnityEngine.Events.UnityEvent(); // Only cache component references - NO setup yet _rb = GetComponent(); if (_rb == null) { Debug.LogError("[BirdPlayerController] Rigidbody2D component not found!"); } // Load settings _settings = GameManager.GetSettingsObject(); if (_settings == null) { Debug.LogError("[BirdPlayerController] BirdPooperSettings not found!"); } Debug.Log("[BirdPlayerController] References cached, waiting for initialization..."); } /// /// Initializes the player controller - enables physics and input. /// Should be called by BirdPooperGameManager when ready to start the game. /// 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); Debug.Log("[BirdPlayerController] Registered as default input consumer"); } else { Debug.LogError("[BirdPlayerController] InputManager instance not found!"); } _isInitialized = true; // DIAGNOSTIC: Log all critical values that might differ between platforms Debug.Log($"[BirdPlayerController] ===== INITIALIZATION DIAGNOSTICS ====="); Debug.Log($"[BirdPlayerController] Fixed X position: {_fixedXPosition}"); Debug.Log($"[BirdPlayerController] Time.timeScale: {Time.timeScale}"); Debug.Log($"[BirdPlayerController] Application.targetFrameRate: {Application.targetFrameRate}"); Debug.Log($"[BirdPlayerController] Settings.Gravity: {_settings.Gravity}"); Debug.Log($"[BirdPlayerController] Settings.FlapForce: {_settings.FlapForce}"); Debug.Log($"[BirdPlayerController] Settings.MaxFallSpeed: {_settings.MaxFallSpeed}"); Debug.Log($"[BirdPlayerController] Settings.MinY: {_settings.MinY}"); Debug.Log($"[BirdPlayerController] Settings.MaxY: {_settings.MaxY}"); Debug.Log($"[BirdPlayerController] =========================================="); } /// /// Using FixedUpdate for physics to ensure frame-rate independence. /// FixedUpdate runs at a fixed timestep (default 0.02s = 50fps) regardless of actual frame rate. /// private void FixedUpdate() { // Only run physics/movement if initialized if (!_isInitialized || _isDead || _settings == null || _rb == null) return; // DIAGNOSTIC: Track fixedDeltaTime (should be consistent) _updateFrameCount++; float dt = Time.fixedDeltaTime; if (dt > _maxDeltaTime) _maxDeltaTime = dt; if (dt < _minDeltaTime) _minDeltaTime = dt; // Log diagnostics every 50 fixed frames (every second at 50fps fixed timestep) if (_updateFrameCount % 50 == 0) { Debug.Log($"[BirdPlayerController] FixedFrame {_updateFrameCount}: fixedDeltaTime={dt:F4}, min={_minDeltaTime:F4}, max={_maxDeltaTime:F4}, velocity={_verticalVelocity:F2}, pos.y={_rb.position.y:F2}"); } // Apply manual gravity using fixedDeltaTime for consistent physics _verticalVelocity -= _settings.Gravity * dt; // Cap fall speed (terminal velocity) if (_verticalVelocity < -_settings.MaxFallSpeed) _verticalVelocity = -_settings.MaxFallSpeed; // Update position manually Vector2 newPosition = _rb.position; newPosition.y += _verticalVelocity * dt; 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 runs at actual frame rate for smooth visual updates like rotation. /// Physics calculations moved to FixedUpdate for frame-rate independence. /// private void Update() { // Only update visuals if initialized if (!_isInitialized || _isDead || _settings == null) return; // Update rotation based on velocity (visual only, runs at frame rate for smoothness) UpdateRotation(); } #region ITouchInputConsumer Implementation public void OnTap(Vector2 tapPosition) { // 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 /// /// Makes the bird flap, applying upward velocity. /// Can be called by input system or externally (e.g., for first tap). /// Includes cooldown to prevent multi-tap issues on mobile devices. /// public void Flap() { if (!_isInitialized || _isDead || _settings == null) return; // Cooldown check to prevent multi-tap issues (especially on mobile touchscreens) if (Time.time < _lastFlapTime + flapCooldown) { Debug.Log($"[BirdPlayerController] Flap rejected - on cooldown ({Time.time - _lastFlapTime:F3}s since last flap)"); return; } _verticalVelocity = _settings.FlapForce; _lastFlapTime = Time.time; // DIAGNOSTIC: Log flap details including time values Debug.Log($"[BirdPlayerController] FLAP! velocity={_verticalVelocity}, Time.time={Time.time}, Time.deltaTime={Time.deltaTime}, Time.timeScale={Time.timeScale}"); // Emit flap event OnFlap?.Invoke(); } #endregion #region Rotation /// /// Updates the bird's rotation based on vertical velocity. /// Bird tilts up when flapping, down when falling. /// private void UpdateRotation() { 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 ); float targetAngle = Mathf.Lerp( -_settings.MaxRotationAngle, _settings.MaxRotationAngle, velocityPercent ); // Get current angle (handle 0-360 wrapping to -180-180) float currentAngle = transform.rotation.eulerAngles.z; if (currentAngle > 180f) currentAngle -= 360f; // Smooth interpolation to target float smoothedAngle = Mathf.Lerp( currentAngle, targetAngle, _settings.RotationSpeed * Time.deltaTime ); // Apply rotation to Z axis only (2D rotation) transform.rotation = Quaternion.Euler(0, 0, smoothedAngle); } #endregion #region Trigger-Based Collision Detection /// /// Called when a trigger collider enters this object's trigger. /// Used for detecting obstacles and targets without physics interactions. /// private void OnTriggerEnter2D(Collider2D other) { // Check if the colliding object is tagged as an obstacle or target if (other.CompareTag("Obstacle") || other.CompareTag("Target")) { HandleDeath(); } } private void HandleDeath() { // Only process death once if (_isDead) return; _isDead = true; _verticalVelocity = 0f; Debug.Log("[BirdPlayerController] Bird died!"); // Emit damage event - let the game manager handle UI OnPlayerDamaged?.Invoke(); } #endregion #region Public Accessors public bool IsDead => _isDead; #endregion } }