Initial Commit
This commit is contained in:
@@ -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