Initial Commit

This commit is contained in:
2026-03-16 14:38:46 +02:00
commit b8f7327a21
2327 changed files with 253610 additions and 0 deletions
+85
View File
@@ -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
+135
View File
@@ -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);
}
}
}
+13
View File
@@ -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:
+7
View File
@@ -0,0 +1,7 @@
namespace FlappyBird.UI
{
public interface IGameUIController
{
void Bind(GameUIView view);
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0a789f671fdcf144c8e4aaaa426913c6
+86
View File
@@ -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();
}
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 290f48f5d4edb434ea4df3f78f078986