255 lines
7.8 KiB
C#
255 lines
7.8 KiB
C#
using System;
|
|
using FlappyBird.Core;
|
|
using UnityEngine;
|
|
using Zenject;
|
|
|
|
namespace FlappyBird.Bird
|
|
{
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Spawns bird in a frozen state waiting for first input.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Applies upward impulse and un-freezes physics on first tap.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clamps fall velocity and rotates the bird based on vertical speed.
|
|
/// Called from BirdView.FixedUpdate ? contains NO input polling.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|
|
}
|