Update Project
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user