Update Project

This commit is contained in:
2026-03-20 13:04:43 +02:00
parent 9b587e6cba
commit beae2dea89
2295 changed files with 251259 additions and 33 deletions
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a512bdb0c66789b4bb58dc64c433aef5
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+64
View File
@@ -0,0 +1,64 @@
using UnityEngine;
using Zenject;
using FlappyBird.Gameplay.Pipes;
namespace FlappyBird.Core
{
/// <summary>
/// Pure C# service — owns all death side-effects.
/// Subscribes to IGameStateManager.OnStateChanged at construction time
/// and reacts when the state becomes GameOver.
///
/// Side-effects on death:
/// 1. Freeze the bird Rigidbody2D (velocity + gravity)
/// 2. Reset the pipe spawner timer so pipes start cleanly on restart
/// </summary>
public sealed class DeathHandler : IDeathHandler, IInitializable
{
private readonly Rigidbody2D _birdRigidbody;
private readonly IGameStateManager _gameStateManager;
private readonly IPipeSpawner _pipeSpawner;
public DeathHandler(
Rigidbody2D birdRigidbody,
IGameStateManager gameStateManager,
IPipeSpawner pipeSpawner)
{
_birdRigidbody = birdRigidbody;
_gameStateManager = gameStateManager;
_pipeSpawner = pipeSpawner;
}
// ── IInitializable ─────────────────────────────────────────────────
public void Initialize()
{
_gameStateManager.OnStateChanged += OnStateChanged;
}
// ── IDeathHandler ──────────────────────────────────────────────────
/// <inheritdoc/>
public void HandleDeath()
{
FreezeBird();
_pipeSpawner.ResetTimer();
}
// ── Private helpers ────────────────────────────────────────────────
private void OnStateChanged(GameState newState)
{
if (newState == GameState.Playing)
UnfreezeBird();
}
/// <summary>
/// Kills velocity and disables gravity so the bird hangs in place
/// during the GameOver freeze (Time.timeScale is 0, but we set this
/// explicitly so the bird does not drop when timeScale resumes on restart).
/// </summary>
private void FreezeBird()
{
_birdRigidbody.linearVelocity = Vector2.zero;
_birdRigidbody.angularVelocity = 0f;
_birdRigidbody.gravityScale = 0f;
}
/// <summary>Restore gravity when a new game begins.</summary>
private void UnfreezeBird()
{
_birdRigidbody.gravityScale = 1f;
}
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d023d969944224b458f29abc062d3c07
+10
View File
@@ -0,0 +1,10 @@
namespace FlappyBird.Core
{
/// <summary>All possible game states.</summary>
public enum GameState
{
Menu,
Playing,
GameOver
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b946714168a978e49b501c41b0c81379
+45
View File
@@ -0,0 +1,45 @@
using UnityEngine;
namespace FlappyBird.Core
{
/// <summary>
/// Pure C# service — central authority for all game-state transitions.
/// No MonoBehaviour, no Unity lifecycle. Injected via Zenject.
///
/// Transition side-effects:
/// Playing -> Time.timeScale = 1 (unfreeze)
/// GameOver -> Time.timeScale = 0 (freeze everything)
/// Menu -> Time.timeScale = 0 (frozen until game starts)
/// </summary>
public sealed class GameStateManager : IGameStateManager
{
// ── IGameStateManager ──────────────────────────────────────────────
/// <inheritdoc/>
public GameState CurrentState { get; private set; } = GameState.Menu;
/// <inheritdoc/>
public event System.Action<GameState> OnStateChanged;
// ── Constructor ────────────────────────────────────────────────────
public GameStateManager()
{
// Start frozen; gameplay begins only when ChangeState(Playing) is called.
Time.timeScale = 0f;
}
// ── IGameStateManager ──────────────────────────────────────────────
/// <inheritdoc/>
public void ChangeState(GameState newState)
{
if (CurrentState == newState) return;
CurrentState = newState;
ApplyTimeScale(newState);
OnStateChanged?.Invoke(newState);
}
// ── Private helpers ────────────────────────────────────────────────
/// <summary>
/// Only Playing runs at normal speed.
/// Menu and GameOver both freeze Time so no physics or Update logic runs.
/// </summary>
private static void ApplyTimeScale(GameState state)
{
Time.timeScale = state == GameState.Playing ? 1f : 0f;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 05899132f4565a94590c4e2a093e54ba
+16
View File
@@ -0,0 +1,16 @@
namespace FlappyBird.Core
{
/// <summary>
/// Contract for the death-handling service.
/// Implementations react to a GameOver state transition and apply
/// all necessary side-effects (freeze bird, clear pipes, etc.).
/// </summary>
public interface IDeathHandler
{
/// <summary>
/// Executes all death side-effects.
/// Called by the subscriber wired up in the installer.
/// </summary>
void HandleDeath();
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d145c7d22d47c16479e3be770cfc1cbe
+22
View File
@@ -0,0 +1,22 @@
namespace FlappyBird.Core
{
/// <summary>
/// Contract for the central game-state service.
/// Matches the interface defined in AI_RULES.md.
/// </summary>
public interface IGameStateManager
{
/// <summary>The currently active game state.</summary>
GameState CurrentState { get; }
/// <summary>
/// Transitions to <paramref name="newState"/>.
/// Fires <see cref="OnStateChanged"/> after the transition completes.
/// </summary>
void ChangeState(GameState newState);
/// <summary>
/// Fired every time the game state changes.
/// Passes the new <see cref="GameState"/> value.
/// </summary>
event System.Action<GameState> OnStateChanged;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4002324bd9e6d054ea59da95339ab344
+13
View File
@@ -0,0 +1,13 @@
namespace FlappyBird.Core
{
/// <summary>
/// Implement on any Component that is managed by the object pool.
/// </summary>
public interface IPoolable
{
/// <summary>Called by the pool just before the object is handed out.</summary>
void OnSpawn();
/// <summary>Called by the pool just after the object is returned.</summary>
void OnDespawn();
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7ea87bd7742f91e4a9550011fb8354fc
+22
View File
@@ -0,0 +1,22 @@
namespace FlappyBird.Core
{
/// <summary>
/// Contract for the score management service.
/// Matches the interface defined in AI_RULES.md.
/// </summary>
public interface IScoreManager
{
/// <summary>Current score for this session.</summary>
int CurrentScore { get; }
/// <summary>All-time high score (persisted via PlayerPrefs).</summary>
int HighScore { get; }
/// <summary>Adds <paramref name="points"/> to the current score.</summary>
void AddScore(int points);
/// <summary>Resets the current score to zero (call on game restart).</summary>
void ResetScore();
/// <summary>Fired whenever the current score changes. Passes the new score value.</summary>
event System.Action<int> OnScoreChanged;
/// <summary>Fired when a new high score is set. Passes the new high score value.</summary>
event System.Action<int> OnHighScoreBeaten;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c6a5d584113c9e644892894fabe9970b
+48
View File
@@ -0,0 +1,48 @@
using UnityEngine;
namespace FlappyBird.Core
{
/// <summary>
/// Pure C# service — all scoring logic lives here.
/// No MonoBehaviour, no Unity lifecycle. Injected via Zenject.
/// High score is persisted with PlayerPrefs.
/// </summary>
public sealed class ScoreManager : IScoreManager
{
private const string HIGH_SCORE_KEY = "HighScore";
// ── IScoreManager ──────────────────────────────────────────────────
/// <inheritdoc/>
public int CurrentScore { get; private set; }
/// <inheritdoc/>
public int HighScore { get; private set; }
/// <inheritdoc/>
public event System.Action<int> OnScoreChanged;
/// <inheritdoc/>
public event System.Action<int> OnHighScoreBeaten;
// ── Constructor ────────────────────────────────────────────────────
public ScoreManager()
{
HighScore = PlayerPrefs.GetInt(HIGH_SCORE_KEY, 0);
}
// ── IScoreManager ──────────────────────────────────────────────────
/// <inheritdoc/>
public void AddScore(int points)
{
if (points <= 0) return;
CurrentScore += points;
OnScoreChanged?.Invoke(CurrentScore);
if (CurrentScore > HighScore)
{
HighScore = CurrentScore;
PlayerPrefs.SetInt(HIGH_SCORE_KEY, HighScore);
PlayerPrefs.Save();
OnHighScoreBeaten?.Invoke(HighScore);
}
}
/// <inheritdoc/>
public void ResetScore()
{
CurrentScore = 0;
OnScoreChanged?.Invoke(CurrentScore);
}
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8cb16782bb35543409bdbf67b4f6c2d6
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 83f6be400e1444819b170acf1f7d4a9e
timeCreated: 1773995371
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c3318d99ec74437982dcf36b799293bd
timeCreated: 1773995371
@@ -0,0 +1,83 @@
using UnityEngine;
using Zenject;
using FlappyBird.Core;
namespace FlappyBird.Gameplay.Bird
{
/// <summary>
/// Thin MonoBehaviour on the bird GameObject.
/// Responsibilities:
/// - Detect solid pipe collisions (OnCollisionEnter2D)
/// - Detect trigger-based pipe collisions (OnTriggerEnter2D, tag "Obstacle")
/// - Check vertical screen bounds every frame
/// On any death condition: calls IDeathHandler then notifies GameManagerView.
/// NO death logic lives here.
/// </summary>
[RequireComponent(typeof(Collider2D))]
public sealed class BirdCollisionView : MonoBehaviour
{
private GameManagerView _gameManagerView;
private BirdSettings _birdSettings;
private IGameStateManager _gameStateManager;
private IDeathHandler _deathHandler;
[Inject]
private void Construct(
GameManagerView gameManagerView,
BirdSettings birdSettings,
IGameStateManager gameStateManager,
IDeathHandler deathHandler)
{
_gameManagerView = gameManagerView;
_birdSettings = birdSettings;
_gameStateManager = gameStateManager;
_deathHandler = deathHandler;
}
// ── Unity lifecycle ────────────────────────────────────────────────
private void Update()
{
// Bounds check only runs while playing — prevents re-triggering death.
if (_gameStateManager.CurrentState != GameState.Playing) return;
CheckBounds();
}
private void OnCollisionEnter2D(Collision2D other)
{
Die();
}
private void OnTriggerEnter2D(Collider2D other)
{
// Ignore the score trigger that sits in the gap centre.
if (other.CompareTag("Bird")) return;
if (other.CompareTag("Obstacle"))
Die();
}
// ── Private helpers ────────────────────────────────────────────────
/// <summary>
/// Position-based ceiling/floor check.
/// No boundary collider objects are required in the scene.
/// </summary>
private void CheckBounds()
{
float y = transform.position.y;
if (y > _birdSettings.MaxY || y < _birdSettings.MinY)
Die();
}
/// <summary>
/// Single death entry-point — applies side-effects then triggers GameOver.
/// Guard inside NotifyBirdDied prevents double-firing.
/// </summary>
private void Die()
{
_deathHandler.HandleDeath();
_gameManagerView.NotifyBirdDied();
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 47e600501c07d4045a3c51d0d03b1bfc
@@ -0,0 +1,68 @@
using UnityEngine;
namespace FlappyBird.Gameplay.Bird
{
/// <summary>
/// Pure C# service — all bird physics and rotation logic lives here.
/// No MonoBehaviour, no Unity lifecycle. Injected via Zenject.
/// </summary>
public sealed class BirdController : IBirdController
{
// ── Dependencies ────────────────────────────────────────────────────
private readonly Rigidbody2D _rigidbody;
private readonly BirdSettings _settings;
/// <summary>
/// Constructor injection (Zenject will call this).
/// </summary>
/// <param name="rigidbody">The bird's Rigidbody2D component.</param>
/// <param name="settings">Configurable physics parameters.</param>
public BirdController(Rigidbody2D rigidbody, BirdSettings settings)
{
_rigidbody = rigidbody;
_settings = settings;
// Lock X position so the bird never drifts horizontally.
// The illusion of forward movement comes from pipes scrolling left.
_rigidbody.constraints = RigidbodyConstraints2D.FreezePositionX;
}
// ── IBirdController ─────────────────────────────────────────────────
/// <inheritdoc/>
public void Tick()
{
ApplyRotation();
}
/// <inheritdoc/>
public void Jump()
{
// Reset vertical velocity so each tap feels consistent regardless of fall speed.
// X is frozen via constraints, but we explicitly keep it zero for safety.
_rigidbody.linearVelocity = Vector2.zero;
_rigidbody.AddForce(Vector2.up * _settings.JumpForce, ForceMode2D.Impulse);
}
// ── Private helpers ─────────────────────────────────────────────────
/// <summary>
/// Maps vertical velocity to a tilt angle and smoothly rotates the bird.
/// Velocity range [rotationUpVelocity .. rotationDownVelocity] maps to
/// [maxUpRotation .. maxDownRotation] via an inverse-lerp → lerp.
/// </summary>
private void ApplyRotation()
{
float velocityY = _rigidbody.linearVelocity.y;
// Normalise velocity to [0, 1] (0 = falling fast, 1 = rising fast).
float t = Mathf.InverseLerp(
_settings.RotationDownVelocity,
_settings.RotationUpVelocity,
velocityY);
float targetAngle = Mathf.Lerp(
_settings.MaxDownRotation,
_settings.MaxUpRotation,
t);
// Read current Z rotation from the transform via the rigidbody.
float currentAngle = _rigidbody.rotation;
float smoothedAngle = Mathf.LerpAngle(
currentAngle,
targetAngle,
_settings.RotationSpeed * Time.deltaTime);
_rigidbody.MoveRotation(smoothedAngle);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e7a2477aa06913f429b73104a5697ca3
@@ -0,0 +1,49 @@
using UnityEngine;
namespace FlappyBird.Gameplay.Bird
{
/// <summary>
/// ScriptableObject that holds all configurable bird physics parameters.
/// Create via: Assets → Create → FlappyBird → Bird Settings
/// </summary>
[CreateAssetMenu(fileName = "BirdSettings", menuName = "FlappyBird/Bird Settings")]
public sealed class BirdSettings : ScriptableObject
{
[Header("Jump")]
[Tooltip("Upward force applied on each tap (Impulse mode).")]
[SerializeField, Min(0f)] private float jumpForce = 5f;
[Header("Rotation")]
[Tooltip("Maximum upward tilt angle in degrees (positive = nose up).")]
[SerializeField] private float maxUpRotation = 30f;
[Tooltip("Maximum downward tilt angle in degrees (positive = nose down).")]
[SerializeField] private float maxDownRotation = -90f;
[Tooltip("Velocity at which the bird reaches full upward rotation.")]
[SerializeField] private float rotationUpVelocity = 5f;
[Tooltip("Velocity at which the bird reaches full downward rotation.")]
[SerializeField] private float rotationDownVelocity = -10f;
[Tooltip("Speed at which the rotation visually lerps to the target angle.")]
[SerializeField, Min(0f)] private float rotationSpeed = 10f;
[Header("Boundaries")]
[Tooltip("Y position of the ceiling. Bird dies if it rises above this value.")]
[SerializeField] private float maxY = 5f;
[Tooltip("Y position of the ground. Bird dies if it falls below this value.")]
[SerializeField] private float minY = -5f;
// ── Public read-only accessors ──────────────────────────────────────
public float JumpForce => jumpForce;
public float MaxUpRotation => maxUpRotation;
public float MaxDownRotation => maxDownRotation;
public float RotationUpVelocity => rotationUpVelocity;
public float RotationDownVelocity => rotationDownVelocity;
public float RotationSpeed => rotationSpeed;
public float MaxY => maxY;
public float MinY => minY;
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 895930ede4bb4906be7651fd00905f76
timeCreated: 1773995571
+56
View File
@@ -0,0 +1,56 @@
using UnityEngine;
using Zenject;
using FlappyBird.Core;
namespace FlappyBird.Gameplay.Bird
{
/// <summary>
/// Thin MonoBehaviour view for the bird.
/// Responsibilities:
/// - Hold component references (Rigidbody2D)
/// - Forward Unity lifecycle events to the service
/// - Detect input in Update and delegate to IBirdController
/// NO game logic lives here.
/// </summary>
[RequireComponent(typeof(Rigidbody2D))]
public sealed class BirdView : MonoBehaviour
{
private IBirdController _birdController;
private IGameStateManager _gameStateManager;
[Inject]
private void Construct(IBirdController birdController, IGameStateManager gameStateManager)
{
_birdController = birdController;
_gameStateManager = gameStateManager;
}
// Unity lifecycle
/// <summary>Update: ONLY input detection, no logic.</summary>
private void Update()
{
if (_gameStateManager.CurrentState != GameState.Playing) return;
if (IsJumpInputDetected())
_birdController.Jump();
}
/// <summary>FixedUpdate: physics tick forwarded to the service.</summary>
private void FixedUpdate()
{
_birdController.Tick();
}
// Private helpers
/// <summary>
/// Returns true when the player taps (touch) or clicks (mouse / Space).
/// Supports mobile touch and desktop mouse / keyboard for editor testing.
/// </summary>
private static bool IsJumpInputDetected()
{
if (Input.touchCount > 0 && Input.GetTouch(0).phase == TouchPhase.Began)
return true;
if (Input.GetMouseButtonDown(0))
return true;
if (Input.GetKeyDown(KeyCode.Space))
return true;
return false;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a837c412868f0ff48af1351eb1d3ca12
@@ -0,0 +1,16 @@
namespace FlappyBird.Gameplay.Bird
{
/// <summary>
/// Contract for the bird's physics and rotation logic.
/// Implementations must NOT be MonoBehaviours.
/// </summary>
public interface IBirdController
{
/// <summary>Called once per frame to update rotation based on current velocity.</summary>
void Tick();
/// <summary>Applies a configurable upward force impulse to the bird.</summary>
void Jump();
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f8720fa707d34ebabf662d71943b80a9
timeCreated: 1773995371
@@ -0,0 +1,78 @@
using UnityEngine;
using Zenject;
using FlappyBird.Core;
namespace FlappyBird.Gameplay
{
/// <summary>
/// Thin MonoBehaviour — the single scene entry-point for game-state transitions.
/// Responsibilities:
/// - Start the game (Menu -> Playing) on first tap
/// - Detect bird collision with pipes or boundaries -> GameOver
/// - Restart (GameOver -> Playing) on tap after death
/// NO state logic lives here; all calls are delegated to IGameStateManager.
/// </summary>
public sealed class GameManagerView : MonoBehaviour
{
private IGameStateManager _gameStateManager;
private IScoreManager _scoreManager;
[Inject]
private void Construct(IGameStateManager gameStateManager, IScoreManager scoreManager)
{
_gameStateManager = gameStateManager;
_scoreManager = scoreManager;
}
// ── Unity lifecycle ────────────────────────────────────────────────
private void Update()
{
HandleInput();
}
// ── Public API (called by BirdCollisionView) ───────────────────────
/// <summary>
/// Called when the bird hits a pipe or boundary collider.
/// Transitions Playing -> GameOver.
/// </summary>
public void NotifyBirdDied()
{
if (_gameStateManager.CurrentState != GameState.Playing) return;
_gameStateManager.ChangeState(GameState.GameOver);
}
// ── Private helpers ────────────────────────────────────────────────
/// <summary>
/// Menu + tap -> Playing (start game, reset score)
/// GameOver + tap -> Playing (restart game, reset score)
/// </summary>
private void HandleInput()
{
if (!IsTapDetected()) return;
switch (_gameStateManager.CurrentState)
{
case GameState.Menu:
StartGame();
break;
case GameState.GameOver:
RestartGame();
break;
}
}
private void StartGame()
{
_scoreManager.ResetScore();
_gameStateManager.ChangeState(GameState.Playing);
}
private void RestartGame()
{
_scoreManager.ResetScore();
_gameStateManager.ChangeState(GameState.Playing);
}
private static bool IsTapDetected()
{
if (Input.touchCount > 0 && Input.GetTouch(0).phase == TouchPhase.Began)
return true;
if (Input.GetMouseButtonDown(0))
return true;
if (Input.GetKeyDown(KeyCode.Space))
return true;
return false;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5b54ab392d818d847800e7e5fa535364
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: df3987a264e7ddf4b96d4df6f741dbe2
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,20 @@
namespace FlappyBird.Gameplay.Pipes
{
/// <summary>
/// Contract for the pipe-pair pool service.
/// </summary>
public interface IPipePool
{
/// <summary>
/// Pre-warms the pool. Must be called after the container is fully built
/// (i.e. from IInitializable.Initialize) to avoid circular dependency.
/// </summary>
void Initialize();
/// <summary>Returns a PipeView from the pool, expanding it if necessary.</summary>
PipeView Get();
/// <summary>Returns a PipeView back to the pool.</summary>
void Return(PipeView pipe);
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8a4286c16c2bdec4fbe71cf821265ffd
@@ -0,0 +1,17 @@
namespace FlappyBird.Gameplay.Pipes
{
/// <summary>
/// Contract for the pipe-spawning service.
/// </summary>
public interface IPipeSpawner
{
/// <summary>
/// Called every frame (from a MonoBehaviour Tick).
/// Accumulates delta time and triggers a spawn when the interval elapses.
/// </summary>
/// <param name="deltaTime">Time.deltaTime supplied by the caller.</param>
void Tick(float deltaTime);
/// <summary>Resets the internal spawn timer.</summary>
void ResetTimer();
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6c750205b671e1d41b9b591718065c14
+68
View File
@@ -0,0 +1,68 @@
using System.Collections.Generic;
using Zenject;
namespace FlappyBird.Gameplay.Pipes
{
/// <summary>
/// Pure C# object pool for PipeView prefab instances.
/// Pre-warming is deferred to Initialize() (called by Zenject after the
/// container is fully built) to avoid a circular dependency:
/// PipePool → Factory → PipeView → IPipePool → PipePool
/// </summary>
public sealed class PipePool : IPipePool, IInitializable
{
private readonly Stack<PipeView> _inactive = new Stack<PipeView>();
private readonly PipeSettings _settings;
private readonly PipeView.Factory _factory;
/// <summary>Constructor injection — no instantiation here.</summary>
public PipePool(PipeSettings settings, PipeView.Factory factory)
{
_settings = settings;
_factory = factory;
}
// ── IInitializable ─────────────────────────────────────────────────
/// <summary>
/// Called by Zenject after all bindings are resolved.
/// Safe to call Factory.Create() here because IPipePool is already registered.
/// </summary>
public void Initialize()
{
Prewarm(_settings.InitialPoolSize);
}
// ── IPipePool ──────────────────────────────────────────────────────
/// <inheritdoc/>
public PipeView Get()
{
PipeView pipe = _inactive.Count > 0
? _inactive.Pop()
: _factory.Create();
pipe.OnSpawn();
return pipe;
}
/// <inheritdoc/>
public void Return(PipeView pipe)
{
pipe.OnDespawn();
_inactive.Push(pipe);
}
// ── Private helpers ────────────────────────────────────────────────
private void Prewarm(int count)
{
for (int i = 0; i < count; i++)
{
PipeView pipe = _factory.Create();
pipe.OnDespawn();
_inactive.Push(pipe);
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e56a4832c28c4154cb34c081429bdb2f
@@ -0,0 +1,46 @@
using UnityEngine;
namespace FlappyBird.Gameplay.Pipes
{
/// <summary>
/// ScriptableObject — all configurable pipe-spawning parameters.
/// Create via: Assets -> Create -> FlappyBird -> Pipe Settings
/// </summary>
[CreateAssetMenu(fileName = "PipeSettings", menuName = "FlappyBird/Pipe Settings")]
public sealed class PipeSettings : ScriptableObject
{
[Header("Spawning")]
[Tooltip("Seconds between each pipe pair spawn.")]
[SerializeField, Min(0.1f)] private float spawnInterval = 2f;
[Tooltip("X position at which pipes are spawned (right edge of screen).")]
[SerializeField] private float spawnX = 10f;
[Header("Gap")]
[Tooltip("Vertical size of the gap the bird flies through.")]
[SerializeField, Min(0.5f)] private float gapSize = 3f;
[Tooltip("Minimum Y position of the gap centre.")]
[SerializeField] private float minGapCentreY = -2f;
[Tooltip("Maximum Y position of the gap centre.")]
[SerializeField] private float maxGapCentreY = 2f;
[Tooltip("Half the height of a pipe sprite in world units. " +
"Used to shift each pipe outward when its pivot is centred. " +
"Set to 0 if the pivot is already at the tip facing the gap.")]
[SerializeField, Min(0f)] private float pipeHalfHeight = 3f;
[Header("Movement")]
[Tooltip("Horizontal scroll speed (units per second, positive = leftward).")]
[SerializeField, Min(0f)] private float scrollSpeed = 3f;
[Tooltip("X position at which a pipe is considered off-screen and returned to pool.")]
[SerializeField] private float despawnX = -12f;
[Header("Pool")]
[Tooltip("Number of pipe pairs pre-allocated in the pool at startup.")]
[SerializeField, Min(1)] private int initialPoolSize = 5;
// Accessors
public float SpawnInterval => spawnInterval;
public float SpawnX => spawnX;
public float GapSize => gapSize;
public float MinGapCentreY => minGapCentreY;
public float MaxGapCentreY => maxGapCentreY;
public float PipeHalfHeight => pipeHalfHeight;
public float ScrollSpeed => scrollSpeed;
public float DespawnX => despawnX;
public int InitialPoolSize => initialPoolSize;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a8a8af3503f8ef54c87da58fe682058d
@@ -0,0 +1,60 @@
using UnityEngine;
namespace FlappyBird.Gameplay.Pipes
{
/// <summary>
/// Pure C# service — all pipe-spawning and timing logic.
/// No MonoBehaviour, no Unity lifecycle. Ticked externally by PipeSpawnerTicker.
/// </summary>
public sealed class PipeSpawner : IPipeSpawner
{
private readonly IPipePool _pool;
private readonly PipeSettings _settings;
private float _timer;
/// <summary>Constructor injection.</summary>
public PipeSpawner(IPipePool pool, PipeSettings settings)
{
_pool = pool;
_settings = settings;
}
// ── IPipeSpawner ───────────────────────────────────────────────────
/// <inheritdoc/>
public void Tick(float deltaTime)
{
_timer += deltaTime;
if (_timer >= _settings.SpawnInterval)
{
_timer -= _settings.SpawnInterval;
SpawnPair();
}
}
/// <inheritdoc/>
public void ResetTimer() => _timer = 0f;
// ── Private helpers ────────────────────────────────────────────────
/// <summary>
/// Fetches a pipe pair from the pool, positions it with a random gap centre,
/// then places the top and bottom pipe transforms around that gap.
/// </summary>
private void SpawnPair()
{
float gapCentreY = Random.Range(_settings.MinGapCentreY, _settings.MaxGapCentreY);
float halfGap = _settings.GapSize * 0.5f;
float pipeOffset = _settings.PipeHalfHeight; // shifts pivot to tip when centred
PipeView pipe = _pool.Get();
pipe.transform.position = new Vector3(_settings.SpawnX, 0f, 0f);
// Bottom pipe: pivot centre pushed downward so the tip meets the gap edge.
pipe.BottomPipe.position = new Vector3(
_settings.SpawnX,
gapCentreY - halfGap - pipeOffset,
0f);
// Top pipe: pivot centre pushed upward so the tip meets the gap edge.
pipe.TopPipe.position = new Vector3(
_settings.SpawnX,
gapCentreY + halfGap + pipeOffset,
0f);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9e3bbbc4e8f07a14f8ec12451f4d7129
@@ -0,0 +1,31 @@
using UnityEngine;
using Zenject;
using FlappyBird.Core;
namespace FlappyBird.Gameplay.Pipes
{
/// <summary>
/// Thin MonoBehaviour ticker — the only pipe-related MonoBehaviour in the scene.
/// Responsibilities:
/// - Forward Update to IPipeSpawner.Tick
/// NO spawn logic lives here.
/// </summary>
public sealed class PipeSpawnerTicker : MonoBehaviour
{
private IPipeSpawner _spawner;
private IGameStateManager _gameStateManager;
[Inject]
private void Construct(IPipeSpawner spawner, IGameStateManager gameStateManager)
{
_spawner = spawner;
_gameStateManager = gameStateManager;
}
private void Update()
{
if (_gameStateManager.CurrentState != GameState.Playing) return;
_spawner.Tick(Time.deltaTime);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6472d530845fa014bab746e8ddd05565
+69
View File
@@ -0,0 +1,69 @@
using UnityEngine;
using Zenject;
namespace FlappyBird.Gameplay.Pipes
{
/// <summary>
/// Thin MonoBehaviour that lives on the pipe-pair prefab.
/// Responsibilities:
/// - Expose top/bottom pipe transforms for positioning by the spawner
/// - Scroll leftward every frame
/// - Return itself to the pool when off-screen
/// NO spawn logic lives here.
/// </summary>
public sealed class PipeView : MonoBehaviour, Core.IPoolable
{
[Tooltip("Transform of the top pipe (flipped, hanging from above).")]
[SerializeField] private Transform topPipe;
[Tooltip("Transform of the bottom pipe (standing upright).")]
[SerializeField] private Transform bottomPipe;
[Tooltip("Score trigger collider child sitting in the centre of the gap.")]
[SerializeField] private ScoreTriggerView scoreTrigger;
// ── Public accessors used by PipeSpawner ───────────────────────────
public Transform TopPipe => topPipe;
public Transform BottomPipe => bottomPipe;
public ScoreTriggerView ScoreTrigger => scoreTrigger;
// ── Injected ───────────────────────────────────────────────────────
private IPipePool _pool;
private PipeSettings _settings;
[Inject]
private void Construct(IPipePool pool, PipeSettings settings)
{
_pool = pool;
_settings = settings;
}
// ── Unity lifecycle ────────────────────────────────────────────────
private void Update()
{
ScrollLeft();
}
// ── IPoolable ──────────────────────────────────────────────────────
/// <summary>Called by the pool just before this pair is handed to the spawner.</summary>
public void OnSpawn()
{
gameObject.SetActive(true);
}
/// <summary>Called by the pool when this pair is returned.</summary>
public void OnDespawn()
{
gameObject.SetActive(false);
}
// ── Private helpers ────────────────────────────────────────────────
/// <summary>Moves the pipe pair leftward and despawns when off-screen.</summary>
private void ScrollLeft()
{
transform.position += Vector3.left * (_settings.ScrollSpeed * Time.deltaTime);
if (transform.position.x <= _settings.DespawnX)
_pool.Return(this);
}
// ── Zenject factory ────────────────────────────────────────────────
/// <summary>
/// Zenject factory used by PipePool to instantiate pipe-pair prefabs
/// with full dependency injection applied.
/// </summary>
public sealed class Factory : PlaceholderFactory<PipeView> { }
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b296b80479d10754bb8d5d4ec6c9138f
@@ -0,0 +1,34 @@
using UnityEngine;
using Zenject;
using FlappyBird.Core;
namespace FlappyBird.Gameplay.Pipes
{
/// <summary>
/// Thin MonoBehaviour placed on a trigger-collider child of the pipe-pair prefab.
/// Sits in the centre of the gap. When the bird passes through it fires OnPlayerScored,
/// which the ScoreManager listens to.
/// NO scoring logic lives here — only trigger detection and event dispatch.
/// </summary>
[RequireComponent(typeof(Collider2D))]
public sealed class ScoreTriggerView : MonoBehaviour
{
/// <summary>
/// Fired when the bird's collider enters this trigger.
/// Subscribers (ScoreManager via installer) add the score.
/// </summary>
public event System.Action OnPlayerScored;
private IScoreManager _scoreManager;
[Inject]
private void Construct(IScoreManager scoreManager)
{
_scoreManager = scoreManager;
}
private void OnTriggerEnter2D(Collider2D other)
{
// Only react to the bird layer — avoids double-counting with pipe colliders.
if (!other.CompareTag("Bird")) return;
OnPlayerScored?.Invoke();
_scoreManager.AddScore(1);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1ffc3bbc99839164f90979e7a41e97b9
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c7c886745b19bfb43b664224214809ea
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+122
View File
@@ -0,0 +1,122 @@
using UnityEngine;
using Zenject;
using FlappyBird.Core;
using FlappyBird.Gameplay;
using FlappyBird.Gameplay.Bird;
using FlappyBird.Gameplay.Pipes;
namespace FlappyBird.Installers
{
/// <summary>
/// Zenject MonoInstaller for core gameplay bindings.
/// Attach this to the SceneContext GameObject in each gameplay scene.
/// </summary>
public sealed class GameInstaller : MonoInstaller
{
[Header("Game Management")]
[Tooltip("GameManagerView MonoBehaviour in the scene.")]
[SerializeField] private GameManagerView _gameManagerView;
[Tooltip("BirdCollisionView MonoBehaviour on the bird GameObject.")]
[SerializeField] private BirdCollisionView _birdCollisionView;
[Header("Bird")]
[Tooltip("Drag the Bird GameObject here (must have Rigidbody2D).")]
[SerializeField] private BirdView _birdView;
[Tooltip("BirdSettings ScriptableObject asset.")]
[SerializeField] private BirdSettings _birdSettings;
[Header("Pipes")]
[Tooltip("PipeSettings ScriptableObject asset.")]
[SerializeField] private PipeSettings _pipeSettings;
[Tooltip("Pipe-pair prefab with a PipeView component.")]
[SerializeField] private PipeView _pipePrefab;
[Tooltip("PipeSpawnerTicker MonoBehaviour in the scene.")]
[SerializeField] private PipeSpawnerTicker _pipeSpawnerTicker;
public override void InstallBindings()
{
BindGameState();
BindScore();
BindBird();
BindPipes();
BindDeath();
}
// ── Game State ─────────────────────────────────────────────────────
private void BindGameState()
{
Container.Bind<IGameStateManager>()
.To<GameStateManager>()
.AsSingle();
// Bind the concrete MonoBehaviour so BirdCollisionView can resolve it by type.
Container.Bind<GameManagerView>()
.FromInstance(_gameManagerView)
.AsSingle();
Container.QueueForInject(_gameManagerView);
Container.QueueForInject(_birdCollisionView);
}
// ── Score ──────────────────────────────────────────────────────────
private void BindScore()
{
Container.Bind<IScoreManager>()
.To<ScoreManager>()
.AsSingle();
}
// ── Bird ───────────────────────────────────────────────────────────
private void BindBird()
{
Container.Bind<Rigidbody2D>()
.FromComponentOn(_birdView.gameObject)
.AsSingle();
Container.BindInstance(_birdSettings).AsSingle();
Container.Bind<IBirdController>()
.To<BirdController>()
.AsSingle();
Container.QueueForInject(_birdView);
}
// ── Pipes ──────────────────────────────────────────────────────────
private void BindPipes()
{
Container.BindInstance(_pipeSettings).AsSingle();
Container.BindFactory<PipeView, PipeView.Factory>()
.FromComponentInNewPrefab(_pipePrefab)
.AsSingle();
Container.BindInterfacesTo<PipePool>()
.AsSingle();
Container.Bind<IPipeSpawner>()
.To<PipeSpawner>()
.AsSingle();
Container.QueueForInject(_pipeSpawnerTicker);
}
// ── Death ──────────────────────────────────────────────────────────
private void BindDeath()
{
// BindInterfacesTo registers the same instance for both
// IDeathHandler and IInitializable (so Zenject calls Initialize()).
Container.BindInterfacesTo<DeathHandler>()
.AsSingle();
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f6e316c2c7fe1714eb17ba354038e4cf