using System; using FlappyBird.Core; using UnityEngine; using Zenject; namespace FlappyBird.Bird { /// /// Pure service class that owns all bird physics logic. /// No MonoBehaviour ? follows AI_RULES.md architecture rule #1. /// Registered via Zenject and driven by BirdView. /// public class BirdController : IBirdController, IDisposable { private readonly BirdSettings _settings; private readonly IGameManager _gameManager; private readonly IDeathManager _deathManager; private BirdView _view; private bool _inputEnabled; private float _rotationVelocity; private Vector3 _startPosition; [Inject] public BirdController(BirdSettings settings, IGameManager gameManager, IDeathManager deathManager) { _settings = settings; _gameManager = gameManager; _deathManager = deathManager; _gameManager.OnGameStateChanged += HandleGameStateChanged; _deathManager.OnDied += HandleDied; } public void Bind(BirdView view) { _view = view; _startPosition = _view.transform.position; ApplyState(_gameManager.CurrentState); } /// /// Spawns bird in a frozen state waiting for first input. /// public void Initialize() { if (_view == null) { _inputEnabled = false; _rotationVelocity = 0f; return; } _inputEnabled = false; _rotationVelocity = 0f; _view.transform.position = _startPosition; _view.Rigidbody.constraints = RigidbodyConstraints2D.None; _view.Rigidbody.gravityScale = 0f; _view.Rigidbody.linearVelocity = Vector2.zero; _view.Rigidbody.angularVelocity = 0f; _view.transform.rotation = Quaternion.identity; } /// /// Applies upward impulse and un-freezes physics on first tap. /// public void Jump() { if (_view == null) { return; } if (_gameManager.CurrentState == GameState.GameOver) { RestartRun(); return; } if (_deathManager.IsDead) { return; } if (_gameManager.CurrentState == GameState.Menu) { _gameManager.StartGame(); } JumpInternal(); } public void HandleCollision() { _deathManager.Die(); } public void SetInputEnabled(bool enabled) { _inputEnabled = enabled; if (_view == null) { return; } if (!enabled) { _view.Rigidbody.gravityScale = 0f; _view.Rigidbody.linearVelocity = Vector2.zero; _view.Rigidbody.angularVelocity = 0f; return; } _view.Rigidbody.gravityScale = _settings.activeGravityScale; } /// /// Clamps fall velocity and rotates the bird based on vertical speed. /// Called from BirdView.FixedUpdate ? contains NO input polling. /// public void FixedTick() { if (!_inputEnabled || _view == null) { return; } ApplyGravityScale(); ClampRiseVelocity(); ClampFallVelocity(); ApplyRotation(); } public void Dispose() { _gameManager.OnGameStateChanged -= HandleGameStateChanged; _deathManager.OnDied -= HandleDied; } private void HandleGameStateChanged(GameState state) { ApplyState(state); } private void HandleDied() { if (_view == null) { _inputEnabled = false; _rotationVelocity = 0f; return; } _inputEnabled = false; _rotationVelocity = 0f; _view.Rigidbody.constraints = RigidbodyConstraints2D.None; _view.Rigidbody.gravityScale = _settings.activeGravityScale * _settings.fallGravityMultiplier; _view.Rigidbody.linearVelocity = new Vector2( _view.Rigidbody.linearVelocity.x, Mathf.Min(_view.Rigidbody.linearVelocity.y, 0f)); _view.Rigidbody.angularVelocity = -Mathf.Abs(_settings.rotationSpeed); } private void ApplyState(GameState state) { if (state == GameState.Menu) { Initialize(); return; } if (state == GameState.GameOver && !_deathManager.IsDead) { SetInputEnabled(false); } } private void RestartRun() { _gameManager.ReturnToMenu(); _gameManager.StartGame(); JumpInternal(); } private void JumpInternal() { if (!_inputEnabled) { SetInputEnabled(true); } Vector2 velocity = _view.Rigidbody.linearVelocity; float carriedFallVelocity = Mathf.Min(velocity.y, 0f) * _settings.fallVelocityCarryover; _view.Rigidbody.gravityScale = _settings.activeGravityScale * _settings.riseGravityMultiplier; _view.Rigidbody.linearVelocity = new Vector2(velocity.x, _settings.jumpStartVelocity + carriedFallVelocity); _view.Rigidbody.AddForce(Vector2.up * _settings.jumpForce, ForceMode2D.Impulse); ClampRiseVelocity(); } private void ApplyGravityScale() { float verticalVelocity = _view.Rigidbody.linearVelocity.y; float gravityMultiplier = verticalVelocity >= 0f ? _settings.riseGravityMultiplier : _settings.fallGravityMultiplier; _view.Rigidbody.gravityScale = _settings.activeGravityScale * gravityMultiplier; } private void ClampRiseVelocity() { Vector2 velocity = _view.Rigidbody.linearVelocity; if (velocity.y > _settings.maxRiseVelocity) { _view.Rigidbody.linearVelocity = new Vector2(velocity.x, _settings.maxRiseVelocity); } } private void ClampFallVelocity() { Vector2 velocity = _view.Rigidbody.linearVelocity; if (velocity.y < _settings.maxFallVelocity) { _view.Rigidbody.linearVelocity = new Vector2(velocity.x, _settings.maxFallVelocity); } } private void ApplyRotation() { float verticalVelocity = _view.Rigidbody.linearVelocity.y; float targetAngle = verticalVelocity >= 0f ? Mathf.Lerp(0f, _settings.maxUpAngle, Mathf.InverseLerp(0f, _settings.upAngleVelocity, verticalVelocity)) : Mathf.Lerp(0f, _settings.maxDownAngle, Mathf.InverseLerp(0f, Mathf.Abs(_settings.downAngleVelocity), Mathf.Abs(verticalVelocity))); float currentAngle = _view.transform.eulerAngles.z; if (currentAngle > 180f) { currentAngle -= 360f; } float smoothTime = Mathf.Max(0.02f, 180f / Mathf.Max(1f, _settings.rotationSpeed) * 0.1f); float newAngle = Mathf.SmoothDampAngle( currentAngle, targetAngle, ref _rotationVelocity, smoothTime, Mathf.Max(1f, _settings.rotationSpeed), Time.fixedDeltaTime); _view.transform.rotation = Quaternion.Euler(0f, 0f, newAngle); } } }