Initial Commit
This commit is contained in:
@@ -0,0 +1,254 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b5b67107af99c5a4f900c6901da708db
|
||||
@@ -0,0 +1,55 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace FlappyBird.Bird
|
||||
{
|
||||
/// <summary>
|
||||
/// Configurable physics and rotation settings for the bird.
|
||||
/// Create via Assets > Create > FlappyBird > Bird Settings.
|
||||
/// </summary>
|
||||
[CreateAssetMenu(fileName = "BirdSettings", menuName = "FlappyBird/Bird Settings")]
|
||||
public class BirdSettings : ScriptableObject
|
||||
{
|
||||
[Header("Jump")]
|
||||
[Tooltip("Upward force applied on each tap.")]
|
||||
public float jumpForce = 5f;
|
||||
|
||||
[Tooltip("Vertical velocity applied before jump force. Useful for snappier flaps.")]
|
||||
public float jumpStartVelocity = 0f;
|
||||
|
||||
[Tooltip("Maximum upward speed after a jump.")]
|
||||
public float maxRiseVelocity = 8f;
|
||||
|
||||
[Tooltip("How much current falling speed is preserved when jumping. 0 = reset fall fully.")]
|
||||
[Range(0f, 1f)]
|
||||
public float fallVelocityCarryover = 0f;
|
||||
|
||||
[Header("Movement")]
|
||||
[Tooltip("Gravity scale while the bird is active.")]
|
||||
public float activeGravityScale = 1f;
|
||||
|
||||
[Tooltip("Extra gravity applied while the bird is falling.")]
|
||||
public float fallGravityMultiplier = 1.5f;
|
||||
|
||||
[Tooltip("Extra gravity applied while the bird is rising.")]
|
||||
public float riseGravityMultiplier = 1f;
|
||||
|
||||
[Tooltip("Terminal fall velocity clamp (negative value).")]
|
||||
public float maxFallVelocity = -10f;
|
||||
|
||||
[Header("Rotation")]
|
||||
[Tooltip("Maximum rotation angle when moving upward (degrees).")]
|
||||
public float maxUpAngle = 30f;
|
||||
|
||||
[Tooltip("Maximum rotation angle when falling (degrees, negative = nose-down).")]
|
||||
public float maxDownAngle = -90f;
|
||||
|
||||
[Tooltip("Vertical speed used to reach max upward angle.")]
|
||||
public float upAngleVelocity = 5f;
|
||||
|
||||
[Tooltip("Vertical speed used to reach max downward angle.")]
|
||||
public float downAngleVelocity = -10f;
|
||||
|
||||
[Tooltip("Degrees per second for rotation lerp towards target angle.")]
|
||||
public float rotationSpeed = 360f;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 25db2a2dd51cc6f4fadb316f81d7b382
|
||||
@@ -0,0 +1,103 @@
|
||||
using FlappyBird.Pipes;
|
||||
using UnityEngine;
|
||||
using Zenject;
|
||||
using FlappyBird.Ground;
|
||||
|
||||
namespace FlappyBird.Bird
|
||||
{
|
||||
/// <summary>
|
||||
/// MonoBehaviour facade for the Bird prefab.
|
||||
/// Responsibilities (AI_RULES.md rule #1):
|
||||
/// - Holds component references
|
||||
/// - Forwards Unity lifecycle events to BirdController
|
||||
/// - Detects input in Update only ? all logic lives in BirdController
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(Rigidbody2D))]
|
||||
public class BirdView : MonoBehaviour
|
||||
{
|
||||
public Rigidbody2D Rigidbody { get; private set; }
|
||||
|
||||
private IBirdController _controller;
|
||||
|
||||
[Inject]
|
||||
public void Construct(IBirdController controller)
|
||||
{
|
||||
_controller = controller;
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
Rigidbody = GetComponent<Rigidbody2D>();
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
_controller.Bind(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Input detection only ? no logic.
|
||||
/// Delegates to IBirdController on tap/click/space.
|
||||
/// </summary>
|
||||
private void Update()
|
||||
{
|
||||
if (_controller == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Input.GetMouseButtonDown(0) || Input.GetKeyDown(KeyCode.Space))
|
||||
{
|
||||
_controller.Jump();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Physics-tick delegation ? no input here.
|
||||
/// </summary>
|
||||
private void FixedUpdate()
|
||||
{
|
||||
if (_controller == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_controller.FixedTick();
|
||||
}
|
||||
|
||||
private void OnCollisionEnter2D(Collision2D collision)
|
||||
{
|
||||
if (_controller == null || collision == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsPipeCollision(collision.collider) && !IsGroundCollision(collision.collider))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_controller.HandleCollision();
|
||||
}
|
||||
|
||||
private void OnTriggerEnter2D(Collider2D other)
|
||||
{
|
||||
if (_controller == null || other == null || !IsGroundCollision(other))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_controller.HandleCollision();
|
||||
}
|
||||
|
||||
private static bool IsPipeCollision(Collider2D collider)
|
||||
{
|
||||
return collider.GetComponentInParent<PipeView>() != null;
|
||||
}
|
||||
|
||||
private static bool IsGroundCollision(Collider2D collider)
|
||||
{
|
||||
return collider.GetComponentInParent<GroundView>() != null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 128f85bdbe34ee14992703c62de302d0
|
||||
@@ -0,0 +1,30 @@
|
||||
namespace FlappyBird.Bird
|
||||
{
|
||||
/// <summary>
|
||||
/// Contract for the bird physics controller service.
|
||||
/// Separates logic from the MonoBehaviour view layer.
|
||||
/// </summary>
|
||||
public interface IBirdController
|
||||
{
|
||||
/// <summary>Attaches the spawned bird view to the controller.</summary>
|
||||
void Bind(BirdView view);
|
||||
|
||||
/// <summary>Initializes the bird at the given spawn position and freezes physics.</summary>
|
||||
void Initialize();
|
||||
|
||||
/// <summary>Applies an upward impulse and enables physics simulation.</summary>
|
||||
void Jump();
|
||||
|
||||
/// <summary>Reports a lethal collision from the view layer.</summary>
|
||||
void HandleCollision();
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables input processing.
|
||||
/// Pass false to freeze the bird before the first tap.
|
||||
/// </summary>
|
||||
void SetInputEnabled(bool enabled);
|
||||
|
||||
/// <summary>Called every fixed-update tick to apply rotation.</summary>
|
||||
void FixedTick();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1713e69de72bd3843ac048392b2483b9
|
||||
Reference in New Issue
Block a user