Initial Commit
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 288392c46976a6f44b8d7455a04bd100
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6c5199b03c629b0438388962d478af20
|
||||
@@ -0,0 +1,127 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using Zenject;
|
||||
|
||||
namespace FlappyBird.Core
|
||||
{
|
||||
public class DeathManager : IDeathManager, IInitializable, ITickable, IDisposable
|
||||
{
|
||||
private const float ShakeDuration = 0.2f;
|
||||
private const float ShakeMagnitude = 0.18f;
|
||||
|
||||
private readonly IGameManager _gameManager;
|
||||
|
||||
private Transform _cameraTransform;
|
||||
private Vector3 _cameraBaseLocalPosition;
|
||||
private float _shakeTimeRemaining;
|
||||
|
||||
[Inject]
|
||||
public DeathManager(IGameManager gameManager)
|
||||
{
|
||||
_gameManager = gameManager;
|
||||
}
|
||||
|
||||
public bool IsDead { get; private set; }
|
||||
|
||||
public event Action OnDied;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_gameManager.OnGameStateChanged += HandleGameStateChanged;
|
||||
CacheMainCamera();
|
||||
IsDead = false;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_gameManager.OnGameStateChanged -= HandleGameStateChanged;
|
||||
ResetCameraPosition();
|
||||
}
|
||||
|
||||
public void Tick()
|
||||
{
|
||||
if (_shakeTimeRemaining <= 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!CacheMainCamera())
|
||||
{
|
||||
_shakeTimeRemaining = 0f;
|
||||
return;
|
||||
}
|
||||
|
||||
_shakeTimeRemaining -= Time.deltaTime;
|
||||
if (_shakeTimeRemaining <= 0f)
|
||||
{
|
||||
ResetCameraPosition();
|
||||
return;
|
||||
}
|
||||
|
||||
float strength = ShakeMagnitude * (_shakeTimeRemaining / ShakeDuration);
|
||||
Vector2 offset = UnityEngine.Random.insideUnitCircle * strength;
|
||||
_cameraTransform.localPosition = _cameraBaseLocalPosition + new Vector3(offset.x, offset.y, 0f);
|
||||
}
|
||||
|
||||
public void Die()
|
||||
{
|
||||
if (IsDead || _gameManager.CurrentState != GameState.Playing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IsDead = true;
|
||||
StartCameraShake();
|
||||
OnDied?.Invoke();
|
||||
_gameManager.GameOver();
|
||||
}
|
||||
|
||||
private void HandleGameStateChanged(GameState state)
|
||||
{
|
||||
if (state == GameState.Menu)
|
||||
{
|
||||
IsDead = false;
|
||||
_shakeTimeRemaining = 0f;
|
||||
ResetCameraPosition();
|
||||
}
|
||||
}
|
||||
|
||||
private void StartCameraShake()
|
||||
{
|
||||
if (!CacheMainCamera())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_shakeTimeRemaining = ShakeDuration;
|
||||
}
|
||||
|
||||
private bool CacheMainCamera()
|
||||
{
|
||||
if (_cameraTransform != null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
Camera mainCamera = Camera.main;
|
||||
if (mainCamera == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_cameraTransform = mainCamera.transform;
|
||||
_cameraBaseLocalPosition = _cameraTransform.localPosition;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ResetCameraPosition()
|
||||
{
|
||||
if (_cameraTransform == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_cameraTransform.localPosition = _cameraBaseLocalPosition;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3ab5ec331cec5ae40bb491b4a5003c21
|
||||
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using Zenject;
|
||||
|
||||
namespace FlappyBird.Core
|
||||
{
|
||||
public class GameManager : IGameManager, IInitializable
|
||||
{
|
||||
public GameState CurrentState { get; private set; } = GameState.Menu;
|
||||
|
||||
public event Action<GameState> OnGameStateChanged;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
CurrentState = GameState.Menu;
|
||||
Debug.Log($"Gameplay state: {CurrentState}");
|
||||
}
|
||||
|
||||
public void StartGame()
|
||||
{
|
||||
if (CurrentState != GameState.Menu)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TransitionTo(GameState.Playing);
|
||||
}
|
||||
|
||||
public void GameOver()
|
||||
{
|
||||
if (CurrentState != GameState.Playing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TransitionTo(GameState.GameOver);
|
||||
}
|
||||
|
||||
public void ReturnToMenu()
|
||||
{
|
||||
if (CurrentState == GameState.Menu)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TransitionTo(GameState.Menu);
|
||||
}
|
||||
|
||||
private void TransitionTo(GameState state)
|
||||
{
|
||||
if (CurrentState == state)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CurrentState = state;
|
||||
Debug.Log($"Gameplay state: {CurrentState}");
|
||||
OnGameStateChanged?.Invoke(CurrentState);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ba4fbdb4d840a6c48b974ed2f0c910b4
|
||||
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
|
||||
namespace FlappyBird.Core
|
||||
{
|
||||
public class GameStartNotifier : IGameStartNotifier
|
||||
{
|
||||
public bool HasStarted { get; private set; }
|
||||
|
||||
public event Action OnGameStarted;
|
||||
|
||||
public void NotifyGameStarted()
|
||||
{
|
||||
if (HasStarted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
HasStarted = true;
|
||||
OnGameStarted?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7c03d9fbb1b6930498065f5321a0fa4b
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace FlappyBird.Core
|
||||
{
|
||||
public enum GameState
|
||||
{
|
||||
Menu,
|
||||
Playing,
|
||||
GameOver
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2df155af08d5fdb45a3f4bd5124c26d2
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b9db9961106beb4489ae7da8ae0957dc
|
||||
@@ -0,0 +1,11 @@
|
||||
using System;
|
||||
|
||||
namespace FlappyBird.Core
|
||||
{
|
||||
public interface IDeathManager
|
||||
{
|
||||
bool IsDead { get; }
|
||||
event Action OnDied;
|
||||
void Die();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9db954e729c053b40844b0a6ee9187f2
|
||||
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
|
||||
namespace FlappyBird.Core
|
||||
{
|
||||
public interface IGameManager
|
||||
{
|
||||
GameState CurrentState { get; }
|
||||
event Action<GameState> OnGameStateChanged;
|
||||
void StartGame();
|
||||
void GameOver();
|
||||
void ReturnToMenu();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c1468ec5fa7ce8a4d84f1c5835bf8994
|
||||
@@ -0,0 +1,11 @@
|
||||
using System;
|
||||
|
||||
namespace FlappyBird.Core
|
||||
{
|
||||
public interface IGameStartNotifier
|
||||
{
|
||||
bool HasStarted { get; }
|
||||
event Action OnGameStarted;
|
||||
void NotifyGameStarted();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: afa8469042fc2df429c88b875f9a96fa
|
||||
@@ -0,0 +1,25 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace FlappyBird.Core
|
||||
{
|
||||
public interface IObjectPoolManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers a new prefab type with the pooling system.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The component type that implements IPoolable.</typeparam>
|
||||
/// <param name="prefab">The GameObject prefab to instantiate.</param>
|
||||
/// <param name="initialSize">How many instances to pre-warm in the pool.</param>
|
||||
void RegisterPool<T>(GameObject prefab, int initialSize = 0) where T : Component, IPoolable;
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves an instance from the pool, or creates a new one if the pool is empty.
|
||||
/// </summary>
|
||||
T Get<T>() where T : Component, IPoolable;
|
||||
|
||||
/// <summary>
|
||||
/// Returns an instance to the pool and deactivates it.
|
||||
/// </summary>
|
||||
void Return<T>(T item) where T : Component, IPoolable;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bbfbc77fd3ee5cb40a099d0108484aaa
|
||||
@@ -0,0 +1,10 @@
|
||||
using System;
|
||||
|
||||
namespace FlappyBird.Core
|
||||
{
|
||||
public interface IPipePassedNotifier
|
||||
{
|
||||
event Action OnPipePassed;
|
||||
void NotifyPipePassed();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8f4d49c65078b2e4e81f1887586c4c77
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace FlappyBird.Core
|
||||
{
|
||||
public interface IPlayerPrefsStorage
|
||||
{
|
||||
bool HasKey(string key);
|
||||
int GetInt(string key, int defaultValue = 0);
|
||||
void SetInt(string key, int value);
|
||||
void Save();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 679bebf4f786fef409d9c40e69034315
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace FlappyBird.Core
|
||||
{
|
||||
public interface IPoolable
|
||||
{
|
||||
void OnSpawned();
|
||||
void OnDespawned();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 555e90e79e1b9d244bb25dc6bdf758e4
|
||||
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
|
||||
namespace FlappyBird.Core
|
||||
{
|
||||
public interface IScoreManager
|
||||
{
|
||||
int CurrentScore { get; }
|
||||
int BestScore { get; }
|
||||
event Action<int> OnScoreChanged;
|
||||
event Action<int> OnBestScoreChanged;
|
||||
void AddScore(int points);
|
||||
void ResetScore();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3cc284d25b3acb240b6617c2fbe898e2
|
||||
@@ -0,0 +1,135 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using Zenject;
|
||||
|
||||
namespace FlappyBird.Core
|
||||
{
|
||||
public class ObjectPoolManager : IObjectPoolManager, IInitializable
|
||||
{
|
||||
private readonly DiContainer _container;
|
||||
|
||||
private readonly Dictionary<Type, Queue<Component>> _pools = new Dictionary<Type, Queue<Component>>();
|
||||
private readonly Dictionary<Type, GameObject> _prefabs = new Dictionary<Type, GameObject>();
|
||||
private readonly Dictionary<Type, Transform> _poolRoots = new Dictionary<Type, Transform>();
|
||||
|
||||
private Transform _mainPoolRoot;
|
||||
|
||||
[Inject]
|
||||
public ObjectPoolManager(DiContainer container)
|
||||
{
|
||||
_container = container;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the main root transform to keep the scene hierarchy clean.
|
||||
/// </summary>
|
||||
public void Initialize()
|
||||
{
|
||||
_mainPoolRoot = new GameObject("ObjectPools").transform;
|
||||
UnityEngine.Object.DontDestroyOnLoad(_mainPoolRoot);
|
||||
}
|
||||
|
||||
public void RegisterPool<T>(GameObject prefab, int initialSize = 0) where T : Component, IPoolable
|
||||
{
|
||||
Type type = typeof(T);
|
||||
|
||||
if (_pools.ContainsKey(type))
|
||||
{
|
||||
Debug.LogWarning($"[ObjectPoolManager] Pool for type {type.Name} is already registered.");
|
||||
return;
|
||||
}
|
||||
|
||||
_prefabs[type] = prefab;
|
||||
_pools[type] = new Queue<Component>();
|
||||
|
||||
// Create a specific parent transform for this type
|
||||
GameObject rootGo = new GameObject($"{type.Name}_Pool");
|
||||
|
||||
// Check if prefab is a UI element (RectTransform)
|
||||
if (prefab.transform is RectTransform)
|
||||
{
|
||||
// UI elements need to stay under a Canvas even when pooled,
|
||||
// or be moved to a hidden Canvas to stay "initialized"
|
||||
rootGo.AddComponent<RectTransform>();
|
||||
}
|
||||
|
||||
Transform typeRoot = rootGo.transform;
|
||||
typeRoot.SetParent(_mainPoolRoot);
|
||||
_poolRoots[type] = typeRoot;
|
||||
|
||||
// Pre-warm the pool
|
||||
for (int i = 0; i < initialSize; i++)
|
||||
{
|
||||
T instance = CreateInstance<T>();
|
||||
instance.gameObject.SetActive(false);
|
||||
_pools[type].Enqueue(instance);
|
||||
}
|
||||
}
|
||||
|
||||
public T Get<T>() where T : Component, IPoolable
|
||||
{
|
||||
Type type = typeof(T);
|
||||
|
||||
if (!_pools.TryGetValue(type, out Queue<Component> pool))
|
||||
{
|
||||
throw new InvalidOperationException($"Pool for type {type.Name} is not registered.");
|
||||
}
|
||||
|
||||
T instance = (pool.Count == 0) ? CreateInstance<T>() : pool.Dequeue() as T;
|
||||
|
||||
instance.gameObject.SetActive(true);
|
||||
instance.OnSpawned();
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an item to its respective pool.
|
||||
/// Automatically handles UI parenting back to the pool root.
|
||||
/// </summary>
|
||||
public void Return<T>(T item) where T : Component, IPoolable
|
||||
{
|
||||
if (item == null) return;
|
||||
|
||||
Type type = typeof(T);
|
||||
|
||||
if (!_pools.TryGetValue(type, out Queue<Component> pool))
|
||||
{
|
||||
// Fallback for cases where T might be an interface or base class
|
||||
type = item.GetType();
|
||||
if (!_pools.TryGetValue(type, out pool))
|
||||
{
|
||||
Debug.LogError($"[ObjectPoolManager] No pool registered for {type.Name}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
item.OnDespawned();
|
||||
|
||||
// Re-parent to the specific pool root to keep Hierarchy clean
|
||||
item.transform.SetParent(_poolRoots[type], false);
|
||||
item.gameObject.SetActive(false);
|
||||
|
||||
pool.Enqueue(item);
|
||||
}
|
||||
|
||||
private T CreateInstance<T>() where T : Component, IPoolable
|
||||
{
|
||||
Type type = typeof(T);
|
||||
GameObject prefab = _prefabs[type];
|
||||
Transform root = _poolRoots[type];
|
||||
|
||||
// Zenject InstantiatePrefab ensures [Inject] works inside the prefab (needed for ScorePopupView)
|
||||
GameObject instance = _container.InstantiatePrefab(prefab, root);
|
||||
T component = instance.GetComponent<T>();
|
||||
|
||||
if (component == null)
|
||||
{
|
||||
component = instance.AddComponent<T>();
|
||||
}
|
||||
|
||||
return component;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a7a6735ad66b59a43841c280b0f792a2
|
||||
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
|
||||
namespace FlappyBird.Core
|
||||
{
|
||||
public class PipePassedNotifier : IPipePassedNotifier
|
||||
{
|
||||
public event Action OnPipePassed;
|
||||
|
||||
public void NotifyPipePassed()
|
||||
{
|
||||
OnPipePassed?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e04163983505da74f89e304b1f594f52
|
||||
@@ -0,0 +1,27 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace FlappyBird.Core
|
||||
{
|
||||
public class PlayerPrefsStorage : IPlayerPrefsStorage
|
||||
{
|
||||
public bool HasKey(string key)
|
||||
{
|
||||
return PlayerPrefs.HasKey(key);
|
||||
}
|
||||
|
||||
public int GetInt(string key, int defaultValue = 0)
|
||||
{
|
||||
return PlayerPrefs.GetInt(key, defaultValue);
|
||||
}
|
||||
|
||||
public void SetInt(string key, int value)
|
||||
{
|
||||
PlayerPrefs.SetInt(key, value);
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
PlayerPrefs.Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 66e3cd5bed2132b4a86b8df540a1dbbf
|
||||
@@ -0,0 +1,83 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using Zenject;
|
||||
|
||||
namespace FlappyBird.Core
|
||||
{
|
||||
public class ScoreManager : IScoreManager, IInitializable, IDisposable
|
||||
{
|
||||
private const string BestScoreKey = "BestScore";
|
||||
|
||||
private readonly IGameManager _gameManager;
|
||||
private readonly IPlayerPrefsStorage _playerPrefsStorage;
|
||||
|
||||
[Inject]
|
||||
public ScoreManager(IGameManager gameManager, IPlayerPrefsStorage playerPrefsStorage)
|
||||
{
|
||||
_gameManager = gameManager;
|
||||
_playerPrefsStorage = playerPrefsStorage;
|
||||
}
|
||||
|
||||
public int CurrentScore { get; private set; }
|
||||
public int BestScore { get; private set; }
|
||||
|
||||
public event Action<int> OnScoreChanged;
|
||||
public event Action<int> OnBestScoreChanged;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
BestScore = _playerPrefsStorage.GetInt(BestScoreKey, 0);
|
||||
OnBestScoreChanged?.Invoke(BestScore);
|
||||
Debug.Log($"High score loaded: {BestScore}");
|
||||
_gameManager.OnGameStateChanged += HandleGameStateChanged;
|
||||
ResetScore();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_gameManager.OnGameStateChanged -= HandleGameStateChanged;
|
||||
}
|
||||
|
||||
public void AddScore(int points)
|
||||
{
|
||||
if (points <= 0 || _gameManager.CurrentState != GameState.Playing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CurrentScore += points;
|
||||
OnScoreChanged?.Invoke(CurrentScore);
|
||||
Debug.Log($"Current score: {CurrentScore}");
|
||||
UpdateBestScore(CurrentScore);
|
||||
}
|
||||
|
||||
public void ResetScore()
|
||||
{
|
||||
CurrentScore = 0;
|
||||
OnScoreChanged?.Invoke(CurrentScore);
|
||||
Debug.Log($"Current score reset: {CurrentScore}");
|
||||
}
|
||||
|
||||
private void HandleGameStateChanged(GameState state)
|
||||
{
|
||||
if (state == GameState.Menu)
|
||||
{
|
||||
ResetScore();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateBestScore(int score)
|
||||
{
|
||||
if (score <= BestScore)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
BestScore = score;
|
||||
_playerPrefsStorage.SetInt(BestScoreKey, BestScore);
|
||||
_playerPrefsStorage.Save();
|
||||
OnBestScoreChanged?.Invoke(BestScore);
|
||||
Debug.Log($"High score updated: {BestScore}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0d78b5c289c877040943bc5a1e563dfc
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 02a99c42afa747f45a4ebea48f21f40f
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1c5580cd97b1c0e4db11f0e60860e945
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2418ea2830a9ae84cb75389256ea2025
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,23 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace FlappyBird.Ground
|
||||
{
|
||||
[CreateAssetMenu(fileName = "GroundSettings", menuName = "FlappyBird/Ground Settings")]
|
||||
public class GroundSettings : ScriptableObject
|
||||
{
|
||||
[Tooltip("World X position where the first ground segment starts.")]
|
||||
public float spawnX = -5f;
|
||||
|
||||
[Tooltip("World X position where ground segments are returned to the pool.")]
|
||||
public float despawnX = -15f;
|
||||
|
||||
[Tooltip("The exact width of the ground sprite/collider to ensure seamless looping.")]
|
||||
public float groundWidth = 10f;
|
||||
|
||||
[Tooltip("World Y position where the ground is placed.")]
|
||||
public float yPosition = -4f;
|
||||
|
||||
[Tooltip("Number of ground segments to create up front.")]
|
||||
public int initialPoolSize = 3;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2646352a67506b34181e2563502f05fb
|
||||
@@ -0,0 +1,127 @@
|
||||
using FlappyBird.Core;
|
||||
using FlappyBird.Ground;
|
||||
using FlappyBird.Pipes;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using Zenject;
|
||||
|
||||
namespace FlappyBird.Environment
|
||||
{
|
||||
public class GroundSpawner : IInitializable, ITickable, IDisposable
|
||||
{
|
||||
private readonly IObjectPoolManager _objectPoolManager;
|
||||
private readonly IGameManager _gameManager;
|
||||
private readonly PipeSettings _pipeSettings;
|
||||
private readonly GroundSettings _groundSettings;
|
||||
private readonly GameObject _groundPrefab;
|
||||
|
||||
private readonly List<GroundView> _activeGrounds = new List<GroundView>();
|
||||
|
||||
private bool _isRunning;
|
||||
|
||||
[Inject]
|
||||
public GroundSpawner(
|
||||
IObjectPoolManager objectPoolManager,
|
||||
IGameManager gameManager,
|
||||
PipeSettings pipeSettings,
|
||||
GroundSettings groundSettings,
|
||||
[Inject(Id = "GroundPrefab")] GameObject groundPrefab)
|
||||
{
|
||||
_objectPoolManager = objectPoolManager;
|
||||
_gameManager = gameManager;
|
||||
_pipeSettings = pipeSettings;
|
||||
_groundSettings = groundSettings;
|
||||
_groundPrefab = groundPrefab;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the ground system by registering the object pool and
|
||||
/// spawning initial segments. They will remain stationary until the game starts.
|
||||
/// </summary>
|
||||
public void Initialize()
|
||||
{
|
||||
_objectPoolManager.RegisterPool<GroundView>(_groundPrefab, _groundSettings.initialPoolSize);
|
||||
|
||||
_gameManager.OnGameStateChanged += HandleGameStateChanged;
|
||||
|
||||
// Pre-spawn segments so the floor is visible in the Menu state
|
||||
float currentX = _groundSettings.spawnX;
|
||||
for (int i = 0; i < Mathf.Max(1, _groundSettings.initialPoolSize); i++)
|
||||
{
|
||||
SpawnGround(currentX);
|
||||
currentX += _groundSettings.groundWidth;
|
||||
}
|
||||
|
||||
// Sets initial movement state based on current GameManager state (Menu = stopped)
|
||||
ApplyState(_gameManager.CurrentState);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_gameManager.OnGameStateChanged -= HandleGameStateChanged;
|
||||
}
|
||||
|
||||
public void Tick()
|
||||
{
|
||||
if (!_isRunning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
MoveActiveGrounds();
|
||||
}
|
||||
|
||||
private void HandleGameStateChanged(GameState state)
|
||||
{
|
||||
ApplyState(state);
|
||||
}
|
||||
|
||||
private void ApplyState(GameState state)
|
||||
{
|
||||
// Ground only moves during active gameplay
|
||||
_isRunning = state == GameState.Playing;
|
||||
}
|
||||
|
||||
private void MoveActiveGrounds()
|
||||
{
|
||||
float step = _pipeSettings.moveSpeed * Time.deltaTime;
|
||||
float rightmostX = GetRightmostGroundX();
|
||||
|
||||
for (int i = _activeGrounds.Count - 1; i >= 0; i--)
|
||||
{
|
||||
GroundView groundView = _activeGrounds[i];
|
||||
groundView.MoveLeft(step);
|
||||
|
||||
if (groundView.XPosition > _groundSettings.despawnX)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_activeGrounds.RemoveAt(i);
|
||||
_objectPoolManager.Return(groundView);
|
||||
|
||||
// Re-spawn at the end of the chain
|
||||
SpawnGround(rightmostX + _groundSettings.groundWidth - step);
|
||||
rightmostX += _groundSettings.groundWidth;
|
||||
}
|
||||
}
|
||||
|
||||
private float GetRightmostGroundX()
|
||||
{
|
||||
float maxX = float.MinValue;
|
||||
foreach (var ground in _activeGrounds)
|
||||
{
|
||||
if (ground.XPosition > maxX) maxX = ground.XPosition;
|
||||
}
|
||||
return maxX == float.MinValue ? _groundSettings.spawnX : maxX;
|
||||
}
|
||||
|
||||
private void SpawnGround(float xPosition)
|
||||
{
|
||||
GroundView groundView = _objectPoolManager.Get<GroundView>();
|
||||
groundView.SetPosition(new Vector3(xPosition, _groundSettings.yPosition, 0f));
|
||||
_activeGrounds.Add(groundView);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9b7f0d695b68afb4bbc3556af67e7c7b
|
||||
@@ -0,0 +1,37 @@
|
||||
using FlappyBird.Core;
|
||||
using FlappyBird.Pipes;
|
||||
using UnityEngine;
|
||||
using Zenject;
|
||||
|
||||
namespace FlappyBird.Ground
|
||||
{
|
||||
public class GroundView : MonoBehaviour, Core.IPoolable
|
||||
{
|
||||
private Transform _cachedTransform;
|
||||
|
||||
public float XPosition => _cachedTransform.position.x;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_cachedTransform = transform;
|
||||
}
|
||||
|
||||
public void SetPosition(Vector3 position)
|
||||
{
|
||||
_cachedTransform.position = position;
|
||||
}
|
||||
|
||||
public void MoveLeft(float distance)
|
||||
{
|
||||
_cachedTransform.position += Vector3.left * distance;
|
||||
}
|
||||
|
||||
public void OnSpawned()
|
||||
{
|
||||
}
|
||||
|
||||
public void OnDespawned()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0d496ba852926f14cb1a0cbbda11884f
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: eb893dec691073e4b86b89c57a58400f
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,37 @@
|
||||
using FlappyBird.Core;
|
||||
using UnityEngine;
|
||||
|
||||
namespace FlappyBird.Pipes
|
||||
{
|
||||
public class PipeScoreTrigger : MonoBehaviour
|
||||
{
|
||||
private IScoreManager _scoreManager;
|
||||
private bool _canScore;
|
||||
|
||||
public void Initialize(IScoreManager scoreManager)
|
||||
{
|
||||
_scoreManager = scoreManager;
|
||||
}
|
||||
|
||||
public void ResetTrigger()
|
||||
{
|
||||
_canScore = true;
|
||||
}
|
||||
|
||||
private void OnTriggerEnter2D(Collider2D other)
|
||||
{
|
||||
if (!_canScore || _scoreManager == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!other.CompareTag("Player"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_canScore = false;
|
||||
_scoreManager.AddScore(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5e53bfbf6adf1dd4b8b2b230469171a9
|
||||
@@ -0,0 +1,41 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace FlappyBird.Pipes
|
||||
{
|
||||
[CreateAssetMenu(fileName = "PipeSettings", menuName = "FlappyBird/Pipe Settings")]
|
||||
public class PipeSettings : ScriptableObject
|
||||
{
|
||||
[Header("Spawn")]
|
||||
[Tooltip("Seconds between pipe spawns.")]
|
||||
public float spawnInterval = 1.5f;
|
||||
|
||||
[Tooltip("World X position where new pipe pairs appear.")]
|
||||
public float spawnX = 10f;
|
||||
|
||||
[Tooltip("World X position where pipe pairs are returned to the pool.")]
|
||||
public float despawnX = -10f;
|
||||
|
||||
[Tooltip("Number of pipe pairs to create up front.")]
|
||||
public int initialPoolSize = 4;
|
||||
|
||||
[Tooltip("Spawn the first pipe immediately when gameplay starts.")]
|
||||
public bool spawnImmediately = false;
|
||||
|
||||
[Header("Movement")]
|
||||
[Tooltip("Horizontal speed for active pipe pairs.")]
|
||||
public float moveSpeed = 2.5f;
|
||||
|
||||
[Header("Gap")]
|
||||
[Tooltip("Distance between the top and bottom pipes.")]
|
||||
public float gapSize = 4f;
|
||||
|
||||
[Tooltip("Minimum vertical center position for the pipe gap.")]
|
||||
public float minGapCenterY = -1.5f;
|
||||
|
||||
[Tooltip("Maximum vertical center position for the pipe gap.")]
|
||||
public float maxGapCenterY = 2.5f;
|
||||
|
||||
[Tooltip("Width of the scoring trigger collider.")]
|
||||
public float scoreTriggerWidth = 1f;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c5ca25bedc7c8714b87c9d32fbc5946b
|
||||
@@ -0,0 +1,140 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using FlappyBird.Core;
|
||||
using UnityEngine;
|
||||
using Zenject;
|
||||
|
||||
namespace FlappyBird.Pipes
|
||||
{
|
||||
public class PipeSpawner : IInitializable, ITickable, IDisposable
|
||||
{
|
||||
private readonly IObjectPoolManager _objectPoolManager;
|
||||
private readonly IGameManager _gameManager;
|
||||
private readonly PipeSettings _settings;
|
||||
private readonly GameObject _pipePrefab;
|
||||
|
||||
private readonly List<PipeView> _activePipes = new List<PipeView>();
|
||||
|
||||
private bool _isRunning;
|
||||
private float _spawnTimer;
|
||||
|
||||
[Inject]
|
||||
public PipeSpawner(
|
||||
IObjectPoolManager objectPoolManager,
|
||||
IGameManager gameManager,
|
||||
PipeSettings settings,
|
||||
[Inject(Id = "PipePrefab")] GameObject pipePrefab)
|
||||
{
|
||||
_objectPoolManager = objectPoolManager;
|
||||
_gameManager = gameManager;
|
||||
_settings = settings;
|
||||
_pipePrefab = pipePrefab;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the spawner by registering the Pipe pool, subscribing to game state changes,
|
||||
/// and setting the initial spawn timer based on settings.
|
||||
/// </summary>
|
||||
public void Initialize()
|
||||
{
|
||||
// Register the pool with the new generic ObjectPoolManager
|
||||
_objectPoolManager.RegisterPool<PipeView>(_pipePrefab, _settings.initialPoolSize);
|
||||
|
||||
_gameManager.OnGameStateChanged += HandleGameStateChanged;
|
||||
ApplyState(_gameManager.CurrentState);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_gameManager.OnGameStateChanged -= HandleGameStateChanged;
|
||||
}
|
||||
|
||||
public void Tick()
|
||||
{
|
||||
if (!_isRunning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
MoveActivePipes();
|
||||
UpdateSpawnTimer();
|
||||
}
|
||||
|
||||
private void HandleGameStateChanged(GameState state)
|
||||
{
|
||||
ApplyState(state);
|
||||
}
|
||||
|
||||
private void ApplyState(GameState state)
|
||||
{
|
||||
if (state == GameState.Playing)
|
||||
{
|
||||
_isRunning = true;
|
||||
_spawnTimer = _settings.spawnImmediately ? 0f : _settings.spawnInterval;
|
||||
return;
|
||||
}
|
||||
|
||||
_isRunning = false;
|
||||
|
||||
if (state == GameState.Menu)
|
||||
{
|
||||
DespawnAllActivePipes();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateSpawnTimer()
|
||||
{
|
||||
_spawnTimer -= Time.deltaTime;
|
||||
if (_spawnTimer > 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SpawnPipe();
|
||||
_spawnTimer += Mathf.Max(0.01f, _settings.spawnInterval);
|
||||
}
|
||||
|
||||
private void SpawnPipe()
|
||||
{
|
||||
PipeView pipeView = _objectPoolManager.Get<PipeView>();
|
||||
|
||||
// Randomize the vertical position of the gap
|
||||
float gapCenterY = UnityEngine.Random.Range(_settings.minGapCenterY, _settings.maxGapCenterY);
|
||||
|
||||
pipeView.ConfigureGap(gapCenterY, _settings.gapSize, _settings.scoreTriggerWidth);
|
||||
pipeView.SetWorldPosition(new Vector3(_settings.spawnX, 0f, 0f));
|
||||
|
||||
_activePipes.Add(pipeView);
|
||||
}
|
||||
|
||||
private void MoveActivePipes()
|
||||
{
|
||||
float step = _settings.moveSpeed * Time.deltaTime;
|
||||
|
||||
for (int i = _activePipes.Count - 1; i >= 0; i--)
|
||||
{
|
||||
PipeView pipeView = _activePipes[i];
|
||||
pipeView.MoveLeft(step);
|
||||
|
||||
// Despawn if the pipe has moved past the despawn threshold
|
||||
if (pipeView.XPosition > _settings.despawnX)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_activePipes.RemoveAt(i);
|
||||
_objectPoolManager.Return(pipeView);
|
||||
}
|
||||
}
|
||||
|
||||
private void DespawnAllActivePipes()
|
||||
{
|
||||
for (int i = _activePipes.Count - 1; i >= 0; i--)
|
||||
{
|
||||
_objectPoolManager.Return(_activePipes[i]);
|
||||
}
|
||||
|
||||
_activePipes.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 62dc8cef99ffb954b838a53c73cb8a0c
|
||||
@@ -0,0 +1,110 @@
|
||||
using FlappyBird.Core;
|
||||
using UnityEngine;
|
||||
using Zenject;
|
||||
|
||||
namespace FlappyBird.Pipes
|
||||
{
|
||||
public class PipeView : MonoBehaviour, global::FlappyBird.Core.IPoolable
|
||||
{
|
||||
private const string UpperPipeName = "Pipe_Upper";
|
||||
private const string LowerPipeName = "Pipe_Lower";
|
||||
private const string ScoreTriggerName = "ScoreTrigger";
|
||||
|
||||
private Transform _cachedTransform;
|
||||
private Transform _upperPipe;
|
||||
private Transform _lowerPipe;
|
||||
private Transform _scoreTrigger;
|
||||
private SpriteRenderer _upperRenderer;
|
||||
private SpriteRenderer _lowerRenderer;
|
||||
private BoxCollider2D _scoreTriggerCollider;
|
||||
private PipeScoreTrigger _pipeScoreTrigger;
|
||||
private FlappyBird.Core.IScoreManager _scoreManager;
|
||||
|
||||
public float XPosition => _cachedTransform.position.x;
|
||||
|
||||
[Inject]
|
||||
public void Construct(FlappyBird.Core.IScoreManager scoreManager)
|
||||
{
|
||||
_scoreManager = scoreManager;
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_cachedTransform = transform;
|
||||
_upperPipe = _cachedTransform.Find(UpperPipeName);
|
||||
_lowerPipe = _cachedTransform.Find(LowerPipeName);
|
||||
_scoreTrigger = _cachedTransform.Find(ScoreTriggerName);
|
||||
|
||||
if (_upperPipe != null)
|
||||
{
|
||||
_upperRenderer = _upperPipe.GetComponent<SpriteRenderer>();
|
||||
}
|
||||
|
||||
if (_lowerPipe != null)
|
||||
{
|
||||
_lowerRenderer = _lowerPipe.GetComponent<SpriteRenderer>();
|
||||
}
|
||||
|
||||
if (_scoreTrigger != null)
|
||||
{
|
||||
_scoreTriggerCollider = _scoreTrigger.GetComponent<BoxCollider2D>();
|
||||
_pipeScoreTrigger = _scoreTrigger.GetComponent<PipeScoreTrigger>();
|
||||
|
||||
if (_pipeScoreTrigger == null)
|
||||
{
|
||||
_pipeScoreTrigger = _scoreTrigger.gameObject.AddComponent<PipeScoreTrigger>();
|
||||
}
|
||||
|
||||
_pipeScoreTrigger.Initialize(_scoreManager);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetWorldPosition(Vector3 position)
|
||||
{
|
||||
_cachedTransform.position = position;
|
||||
}
|
||||
|
||||
public void MoveLeft(float distance)
|
||||
{
|
||||
_cachedTransform.position += Vector3.left * distance;
|
||||
}
|
||||
|
||||
public void ConfigureGap(float gapCenterY, float gapSize, float scoreTriggerWidth)
|
||||
{
|
||||
if (_upperPipe == null || _lowerPipe == null || _upperRenderer == null || _lowerRenderer == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float upperHalfHeight = _upperRenderer.size.y * 0.5f;
|
||||
float lowerHalfHeight = _lowerRenderer.size.y * 0.5f;
|
||||
float gapHalf = gapSize * 0.5f;
|
||||
|
||||
_upperPipe.localPosition = new Vector3(0f, gapCenterY + gapHalf + upperHalfHeight, 0f);
|
||||
_lowerPipe.localPosition = new Vector3(0f, gapCenterY - gapHalf - lowerHalfHeight, 0f);
|
||||
|
||||
if (_scoreTrigger != null)
|
||||
{
|
||||
_scoreTrigger.localPosition = new Vector3(0f, gapCenterY, 0f);
|
||||
}
|
||||
|
||||
if (_scoreTriggerCollider != null)
|
||||
{
|
||||
_scoreTriggerCollider.size = new Vector2(scoreTriggerWidth, gapSize);
|
||||
_scoreTriggerCollider.offset = Vector2.zero;
|
||||
}
|
||||
}
|
||||
|
||||
public void OnSpawned()
|
||||
{
|
||||
if (_pipeScoreTrigger != null)
|
||||
{
|
||||
_pipeScoreTrigger.ResetTrigger();
|
||||
}
|
||||
}
|
||||
|
||||
public void OnDespawned()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 12677c2e66077cc4f89582de3982d3e7
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7d55a3345724ac74a87558191609f438
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,119 @@
|
||||
using FlappyBird.Bird;
|
||||
using FlappyBird.Core;
|
||||
using FlappyBird.Environment;
|
||||
using FlappyBird.Ground;
|
||||
using FlappyBird.Pipes;
|
||||
using FlappyBird.UI;
|
||||
using UnityEngine;
|
||||
using Zenject;
|
||||
|
||||
namespace FlappyBird.Installers
|
||||
{
|
||||
/// <summary>
|
||||
/// Core Zenject installer for the game scene.
|
||||
/// Handles binding of all game services, settings, and prefabs.
|
||||
/// </summary>
|
||||
public class GameInstaller : MonoInstaller
|
||||
{
|
||||
[Header("Bird")]
|
||||
[SerializeField] private BirdSettings birdSettings;
|
||||
[SerializeField] private BirdView birdPrefab;
|
||||
|
||||
[Header("Pipes")]
|
||||
[SerializeField] private PipeSettings pipeSettings;
|
||||
[SerializeField] private GameObject pipePairPrefab;
|
||||
|
||||
[Header("Environment")]
|
||||
[SerializeField] private GroundSettings groundSettings;
|
||||
[SerializeField] private GameObject groundPrefab;
|
||||
|
||||
[Header("UI & Effects")]
|
||||
[SerializeField] private GameUIView gameUIPrefab;
|
||||
[SerializeField] private GameObject scorePopupPrefab;
|
||||
|
||||
public override void InstallBindings()
|
||||
{
|
||||
InstallCore();
|
||||
InstallEnvironment();
|
||||
InstallBird();
|
||||
InstallPipes();
|
||||
InstallScoring();
|
||||
InstallUI();
|
||||
}
|
||||
|
||||
private void InstallCore()
|
||||
{
|
||||
Container.BindInterfacesTo<GameManager>().AsSingle();
|
||||
Container.BindInterfacesTo<DeathManager>().AsSingle();
|
||||
|
||||
// Generic ObjectPoolManager handles all IPoolable types
|
||||
Container.BindInterfacesTo<ObjectPoolManager>().AsSingle().NonLazy();
|
||||
}
|
||||
|
||||
private void InstallEnvironment()
|
||||
{
|
||||
Container.BindInstance(groundSettings).AsSingle();
|
||||
|
||||
Container.Bind<GameObject>()
|
||||
.WithId("GroundPrefab")
|
||||
.FromInstance(groundPrefab)
|
||||
.AsCached();
|
||||
|
||||
Container.BindInterfacesTo<GroundSpawner>()
|
||||
.AsSingle()
|
||||
.NonLazy();
|
||||
}
|
||||
|
||||
private void InstallPipes()
|
||||
{
|
||||
Container.BindInstance(pipeSettings).AsSingle();
|
||||
|
||||
Container.Bind<GameObject>()
|
||||
.WithId("PipePrefab")
|
||||
.FromInstance(pipePairPrefab)
|
||||
.AsCached();
|
||||
|
||||
Container.BindInterfacesTo<PipeSpawner>()
|
||||
.AsSingle()
|
||||
.NonLazy();
|
||||
}
|
||||
|
||||
private void InstallBird()
|
||||
{
|
||||
Container.BindInstance(birdSettings).AsSingle();
|
||||
|
||||
Container.Bind<BirdView>()
|
||||
.FromComponentInNewPrefab(birdPrefab)
|
||||
.AsSingle()
|
||||
.NonLazy();
|
||||
|
||||
Container.BindInterfacesTo<BirdController>()
|
||||
.AsSingle()
|
||||
.NonLazy();
|
||||
}
|
||||
|
||||
private void InstallScoring()
|
||||
{
|
||||
Container.BindInterfacesTo<PlayerPrefsStorage>().AsSingle();
|
||||
Container.BindInterfacesTo<ScoreManager>().AsSingle().NonLazy();
|
||||
}
|
||||
|
||||
private void InstallUI()
|
||||
{
|
||||
// Binds the +1 popup prefab so GameUIController can resolve it
|
||||
Container.Bind<GameObject>()
|
||||
.WithId("ScorePopupPrefab")
|
||||
.FromInstance(scorePopupPrefab)
|
||||
.AsCached();
|
||||
|
||||
Container.Bind<GameUIView>()
|
||||
.FromComponentInNewPrefab(gameUIPrefab)
|
||||
.AsSingle()
|
||||
.NonLazy();
|
||||
|
||||
Container.BindInterfacesTo<GameUIController>()
|
||||
.AsSingle()
|
||||
.NonLazy();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 13dc0f93ad37bad4897997aa214b0638
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 332a142b87549e449858ae2af0ccd92a
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,85 @@
|
||||
using System;
|
||||
using FlappyBird.Core;
|
||||
using UnityEngine;
|
||||
using Zenject;
|
||||
|
||||
namespace FlappyBird.UI
|
||||
{
|
||||
public class GameUIController : IGameUIController, IInitializable, IDisposable
|
||||
{
|
||||
private readonly IGameManager _gameManager;
|
||||
private readonly IScoreManager _scoreManager;
|
||||
private readonly IObjectPoolManager _poolManager;
|
||||
private readonly GameObject _popupPrefab;
|
||||
|
||||
private GameUIView _view;
|
||||
|
||||
[Inject]
|
||||
public GameUIController(
|
||||
IGameManager gameManager,
|
||||
IScoreManager scoreManager,
|
||||
IObjectPoolManager poolManager,
|
||||
[Inject(Id = "ScorePopupPrefab")] GameObject popupPrefab)
|
||||
{
|
||||
_gameManager = gameManager;
|
||||
_scoreManager = scoreManager;
|
||||
_poolManager = poolManager;
|
||||
_popupPrefab = popupPrefab;
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_poolManager.RegisterPool<ScorePopupView>(_popupPrefab, 5);
|
||||
_gameManager.OnGameStateChanged += HandleGameStateChanged;
|
||||
_scoreManager.OnScoreChanged += HandleScoreChanged;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_gameManager.OnGameStateChanged -= HandleGameStateChanged;
|
||||
_scoreManager.OnScoreChanged -= HandleScoreChanged;
|
||||
}
|
||||
|
||||
public void Bind(GameUIView view)
|
||||
{
|
||||
_view = view;
|
||||
RefreshView();
|
||||
}
|
||||
|
||||
private void HandleScoreChanged(int score)
|
||||
{
|
||||
if (_view == null || score == 0) return;
|
||||
|
||||
_view.SetCurrentScore(score);
|
||||
_view.ShowScoreGain();
|
||||
|
||||
// 1. Get from pool
|
||||
var popup = _poolManager.Get<ScorePopupView>();
|
||||
|
||||
// 2. Use the container provided by the View
|
||||
// 'worldPositionStays: false' is CRITICAL for UI elements
|
||||
popup.transform.SetParent(_view.PopupContainer, false);
|
||||
|
||||
// 3. Reset local position to zero (the center of the container)
|
||||
popup.transform.localPosition = Vector3.zero;
|
||||
}
|
||||
|
||||
private void HandleGameStateChanged(GameState state) => ApplyState(state);
|
||||
|
||||
private void ApplyState(GameState state)
|
||||
{
|
||||
if (_view == null) return;
|
||||
|
||||
_view.SetTapToStartVisible(state == GameState.Menu);
|
||||
_view.SetGameOverVisible(state == GameState.GameOver);
|
||||
_view.SetCurrentScoreVisible(state == GameState.Playing);
|
||||
}
|
||||
|
||||
private void RefreshView()
|
||||
{
|
||||
_view.SetCurrentScore(_scoreManager.CurrentScore);
|
||||
_view.SetBestScore(_scoreManager.BestScore);
|
||||
ApplyState(_gameManager.CurrentState);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3a269425488125448858d0b188267a66
|
||||
@@ -0,0 +1,135 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using UnityEngine;
|
||||
using Zenject;
|
||||
using FlappyBird.Core;
|
||||
|
||||
namespace FlappyBird.UI
|
||||
{
|
||||
public class GameUIView : MonoBehaviour
|
||||
{
|
||||
[Header("Score Display")]
|
||||
[SerializeField] private Component currentScoreText;
|
||||
[SerializeField] private Component bestScoreText;
|
||||
[SerializeField] private RectTransform popupContainer;
|
||||
[SerializeField] private float scorePopScale = 1.4f;
|
||||
[SerializeField] private float scorePopDuration = 0.1f;
|
||||
|
||||
[Header("Screens")]
|
||||
[SerializeField] private CanvasGroup tapToStartGroup;
|
||||
[SerializeField] private CanvasGroup gameOverGroup;
|
||||
[SerializeField] private float fadeSpeed = 5f;
|
||||
|
||||
[Header("Game Over Details")]
|
||||
[SerializeField] private Component gameOverScoreText;
|
||||
[SerializeField] private Component gameOverBestScoreText;
|
||||
|
||||
|
||||
private IGameUIController _controller;
|
||||
private RectTransform _currentScoreRect;
|
||||
private Vector3 _currentScoreBaseScale;
|
||||
private float _scorePopTimer;
|
||||
private List<ScorePopupView> _activePopups = new List<ScorePopupView>();
|
||||
public Transform PopupTransform => transform;
|
||||
public RectTransform PopupContainer => popupContainer != null ? popupContainer : (transform as RectTransform);
|
||||
|
||||
[Inject]
|
||||
public void Construct(IGameUIController controller)
|
||||
{
|
||||
_controller = controller;
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (currentScoreText != null)
|
||||
{
|
||||
_currentScoreRect = currentScoreText.transform as RectTransform;
|
||||
_currentScoreBaseScale = _currentScoreRect.localScale;
|
||||
}
|
||||
}
|
||||
|
||||
private void Start() => _controller.Bind(this);
|
||||
|
||||
private void Update()
|
||||
{
|
||||
UpdateScorePop();
|
||||
UpdateScreenFades();
|
||||
UpdateMenuPulse();
|
||||
HandlePopupCleanup();
|
||||
}
|
||||
|
||||
public void ShowScoreGain()
|
||||
{
|
||||
_scorePopTimer = scorePopDuration;
|
||||
}
|
||||
|
||||
public void SetCurrentScore(int score)
|
||||
{
|
||||
SetText(currentScoreText, score.ToString());
|
||||
SetText(gameOverScoreText, score.ToString());
|
||||
}
|
||||
|
||||
public void SetBestScore(int bestScore)
|
||||
{
|
||||
SetText(bestScoreText, $"BEST: {bestScore}");
|
||||
SetText(gameOverBestScoreText, bestScore.ToString());
|
||||
}
|
||||
|
||||
private void UpdateScorePop()
|
||||
{
|
||||
if (_scorePopTimer <= 0) return;
|
||||
|
||||
_scorePopTimer -= Time.unscaledDeltaTime;
|
||||
// Use an animation curve feel: pop out fast, shrink back
|
||||
float t = _scorePopTimer / scorePopDuration;
|
||||
float scale = Mathf.Lerp(1f, scorePopScale, t);
|
||||
_currentScoreRect.localScale = _currentScoreBaseScale * scale;
|
||||
}
|
||||
|
||||
private void UpdateScreenFades()
|
||||
{
|
||||
float step = fadeSpeed * Time.unscaledDeltaTime;
|
||||
// Smoothly fade groups based on their active state handled in Controller
|
||||
tapToStartGroup.alpha = Mathf.MoveTowards(tapToStartGroup.alpha, tapToStartGroup.interactable ? 1 : 0, step);
|
||||
gameOverGroup.alpha = Mathf.MoveTowards(gameOverGroup.alpha, gameOverGroup.interactable ? 1 : 0, step);
|
||||
}
|
||||
|
||||
private void UpdateMenuPulse()
|
||||
{
|
||||
if (tapToStartGroup.interactable)
|
||||
{
|
||||
float pulse = 1f + Mathf.Sin(Time.unscaledTime * 4f) * 0.05f;
|
||||
tapToStartGroup.transform.localScale = Vector3.one * pulse;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetTapToStartVisible(bool visible)
|
||||
{
|
||||
tapToStartGroup.interactable = visible;
|
||||
tapToStartGroup.blocksRaycasts = visible;
|
||||
}
|
||||
|
||||
public void SetGameOverVisible(bool visible)
|
||||
{
|
||||
gameOverGroup.interactable = visible;
|
||||
gameOverGroup.blocksRaycasts = visible;
|
||||
}
|
||||
|
||||
public void SetCurrentScoreVisible(bool visible)
|
||||
{
|
||||
currentScoreText.gameObject.SetActive(visible);
|
||||
}
|
||||
|
||||
private void HandlePopupCleanup()
|
||||
{
|
||||
// Logic to return expired popups to pool would go here via the Controller
|
||||
}
|
||||
|
||||
private static void SetText(Component target, string value)
|
||||
{
|
||||
if (target == null) return;
|
||||
var prop = target.GetType().GetProperty("text");
|
||||
prop?.SetValue(target, value, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
File: Assets\Scripts\UI\GameUIView.cs.meta
|
||||
````````
|
||||
fileFormatVersion: 2
|
||||
guid: b9d0a31d5b6f4b71a1c231728a17b601
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace FlappyBird.UI
|
||||
{
|
||||
public interface IGameUIController
|
||||
{
|
||||
void Bind(GameUIView view);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0a789f671fdcf144c8e4aaaa426913c6
|
||||
@@ -0,0 +1,86 @@
|
||||
using System.Threading;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using FlappyBird.Core;
|
||||
using UnityEngine;
|
||||
using Zenject;
|
||||
|
||||
namespace FlappyBird.UI
|
||||
{
|
||||
[RequireComponent(typeof(CanvasGroup))]
|
||||
public class ScorePopupView : MonoBehaviour, Core.IPoolable
|
||||
{
|
||||
[SerializeField] private float floatSpeed = 100f;
|
||||
[SerializeField] private float duration = 0.7f;
|
||||
[SerializeField] private AnimationCurve alphaCurve = AnimationCurve.Linear(0, 1, 1, 0);
|
||||
|
||||
private IObjectPoolManager _poolManager;
|
||||
private CanvasGroup _canvasGroup;
|
||||
private RectTransform _rectTransform;
|
||||
private CancellationTokenSource _cts;
|
||||
|
||||
[Inject]
|
||||
public void Construct(IObjectPoolManager poolManager)
|
||||
{
|
||||
_poolManager = poolManager;
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_canvasGroup = GetComponent<CanvasGroup>();
|
||||
_rectTransform = transform as RectTransform;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called by the ObjectPoolManager when the popup is retrieved from the pool.
|
||||
/// Starts the async animation task.
|
||||
/// </summary>
|
||||
public void OnSpawned()
|
||||
{
|
||||
_cts?.Cancel();
|
||||
_cts = new CancellationTokenSource();
|
||||
|
||||
AnimateAndReturnAsync(_cts.Token).Forget();
|
||||
}
|
||||
|
||||
public void OnDespawned()
|
||||
{
|
||||
_cts?.Cancel();
|
||||
_cts?.Dispose();
|
||||
_cts = null;
|
||||
}
|
||||
|
||||
private async UniTaskVoid AnimateAndReturnAsync(CancellationToken token)
|
||||
{
|
||||
float elapsed = 0f;
|
||||
Vector2 startPosition = _rectTransform.anchoredPosition;
|
||||
|
||||
while (elapsed < duration)
|
||||
{
|
||||
if (token.IsCancellationRequested) return;
|
||||
|
||||
elapsed += Time.deltaTime;
|
||||
float normalizedTime = elapsed / duration;
|
||||
|
||||
// Move upwards using RectTransform for UI consistency
|
||||
_rectTransform.anchoredPosition = startPosition + (Vector2.up * (floatSpeed * normalizedTime));
|
||||
|
||||
// Apply fading based on curve
|
||||
if (_canvasGroup != null)
|
||||
{
|
||||
_canvasGroup.alpha = alphaCurve.Evaluate(normalizedTime);
|
||||
}
|
||||
|
||||
await UniTask.Yield(PlayerLoopTiming.Update, token);
|
||||
}
|
||||
|
||||
// Return to pool after animation finishes
|
||||
_poolManager.Return(this);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
_cts?.Cancel();
|
||||
_cts?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 290f48f5d4edb434ea4df3f78f078986
|
||||
Reference in New Issue
Block a user