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
@@ -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