feat(game): implement win/lose events and UI updates- Added GameOverEvent and GameWonEvent with total play time

- Updated ContinueGame method to reset lives and coins- Modified GameOver and GameWin methods to invoke events
- Adjusted stage count per world from5 to 3
- Added win banner UI with header text and layout changes
- Updated canvas elements positioning and sizing
- Included new UI elements for win/lose states- Reorganized UI hierarchy in Level UI scene
- Added menu and continue buttons in win panel
- Fixed text components naming and alignment
- Added game start time tracking for play duration
This commit is contained in:
Данил Ярош 2025-10-31 17:03:20 +03:00
parent 5bafc385da
commit 152066e9a3
12 changed files with 22858 additions and 10713 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -1034,7 +1034,7 @@ MonoBehaviour:
m_HandleRect: {fileID: 434895585}
m_Direction: 2
m_Value: 0
m_Size: 0.9999981
m_Size: 1
m_NumberOfSteps: 0
m_OnValueChanged:
m_PersistentCalls:
@ -4540,7 +4540,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0.0006653946}
m_AnchoredPosition: {x: 0, y: 0.0006207353}
m_SizeDelta: {x: 0, y: 300}
m_Pivot: {x: 0, y: 1}
--- !u!114 &1792218764

View File

@ -20,7 +20,6 @@ namespace RR.Game2D.Core.Managers
[Header("Пауза")]
public bool IsPaused { get; private set; }
public event Action<bool> PauseStateChanged;
[Header("Базовые настройки игры")]
[SerializeField] private int maxLives = 3;
@ -28,25 +27,28 @@ namespace RR.Game2D.Core.Managers
[Header("Информация для игры")]
public int World { get; private set; } = 1;
public int Stage { get; private set; } = 1;
public int StagesByWorld { get; private set; } = 5;
public int StagesByWorld { get; private set; } = 3;
public int Lives { get; set; } = 3;
public int Coins { get; private set; } = 0;
private float gameStartTime;
[Header("Игрок")]
[SerializeField] private GameObject playerPrefab;
[SerializeField] private string playerPrefabResourcePath = "Prefabs/Player"; // Путь в Resources для загрузки префаба игрока, если не назначен в инспекторе
public GameObject PlayerObj { get; private set; }
private bool _shouldSpawnPlayer;
private void OnEnable() => SceneManager.sceneLoaded += OnSceneLoaded;
private void OnDisable() => SceneManager.sceneLoaded -= OnSceneLoaded;
public event Action<GameObject> PlayerSpawned;
public event Action<int> CoinsChanged;
public event Action<int> LivesChanged;
// События менеджера
public event Action<bool> PauseStateChanged; // Когда меняется состояние паузы
public event Action<GameObject> PlayerSpawned; // Когда игрос спавнится на сцене
public event Action<int> CoinsChanged; // Когда меняется кол-во монет
public event Action<int> LivesChanged; // Когда меняется кол-во жизней
public event Action<float> GameOverEvent; // Когда игрок проигрывает
public event Action<float> GameWonEvent; // Когда игрок выигрывает
//---------------------------
// Singleton
//---------------------------
@ -77,6 +79,7 @@ namespace RR.Game2D.Core.Managers
Audio = gameObject.AddComponent<AudioManager>();
Audio.Initialize();
}
gameStartTime = Time.time;
if (SceneManager.GetActiveScene().name != "MainMenu") _shouldSpawnPlayer = true;
}
@ -131,8 +134,20 @@ namespace RR.Game2D.Core.Managers
LivesChanged?.Invoke(Lives);
Coins = 0;
CoinsChanged?.Invoke(Coins);
gameStartTime = Time.time;
LoadLevel(1, 1);
}
public void ContinueGame()
{
Lives = maxLives + 1;
LivesChanged?.Invoke(Lives);
Coins = 0;
CoinsChanged?.Invoke(Coins);
RespawnPlayer();
// gameStartTime = Time.time;
// LoadLevel(World, Stage);
}
// Можно заменить на другое поведение
/// <summary>
@ -141,12 +156,23 @@ namespace RR.Game2D.Core.Managers
/// </summary>
private void GameOver() // => LoadScene("MainMenu", false);
{
LoadScene("MainMenu", false);
Lives = maxLives;
LivesChanged?.Invoke(Lives);
// LoadScene("MainMenu", false);
// Lives = maxLives;
// LivesChanged?.Invoke(Lives);
float totalTime = Time.time - gameStartTime;
GameOverEvent?.Invoke(totalTime);
}
// --
private void GameWin() // => LoadScene("MainMenu", false);
{
// LoadScene("MainMenu", false);
// Lives = maxLives;
// LivesChanged?.Invoke(Lives);
float totalTime = Time.time - gameStartTime;
GameWonEvent?.Invoke(totalTime);
}
/// <summary>
/// Загрузка уровня по данным.
/// Загружает уровень по номерам мира и уровня
@ -166,9 +192,13 @@ namespace RR.Game2D.Core.Managers
else
{
//NewGame();
// Debug.Log("WIN");
// LoadScene("MainMenu", false);
// Можно заменить на другое поведение
print("WIN");
LoadScene("MainMenu", false);
GameWin();
// --
}
}

View File

@ -0,0 +1,129 @@
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using RR.Game2D.Core.Managers;
using TMPro;
namespace RR.Common.UI
{
public class GameEndUIController : MonoBehaviour
{
[Header("Меню выигрыша")]
[SerializeField] private CanvasGroup winPanel;
[SerializeField] private Button mainMenuButtonWin;
[SerializeField] private TextMeshProUGUI winText;
[Header("Меню проигрыша")]
[SerializeField] private CanvasGroup losePanel;
public event Action<float> WinBannerShown;
public event Action<float> LoseBannerShown;
public event Action BannerDismissed;
private void OnEnable()
{
if (GameManager.Instance != null)
{
GameManager.Instance.GameWonEvent += OnGameWon;
GameManager.Instance.GameOverEvent += OnGameOver;
}
}
private void OnDisable()
{
if (GameManager.Instance != null)
{
GameManager.Instance.GameWonEvent -= OnGameWon;
GameManager.Instance.GameOverEvent -= OnGameOver;
}
}
private void Awake()
{
if (mainMenuButtonWin) mainMenuButtonWin.onClick.AddListener(OnMainMenuClicked);
if (losePanel) losePanel.GetComponent<LoseBannerController>().ContinueClicked += OnContinueClicked;
// Изначально скрыто
SetVisible(winPanel, false, true);
SetVisible(losePanel, false, true);
}
private void OnMainMenuClicked()
{
if(winPanel.interactable) SetVisible(winPanel, false);
if(losePanel.interactable) SetVisible(losePanel, false);
BannerDismissed?.Invoke();
GameManager.Instance.SetPause(false);
GameManager.Instance.LoadScene("MainMenu", false);
}
private void OnContinueClicked()
{
if(winPanel.interactable) SetVisible(winPanel, false);
if(losePanel.interactable) SetVisible(losePanel, false);
BannerDismissed?.Invoke();
}
private void OnGameWon(float gameTime)
{
WinBannerShown?.Invoke(gameTime);
GameManager.Instance.SetPause(true);
winText.text = $"Время игры: {GameEndUIController.FormatTime(gameTime)}";
SetVisible(winPanel, true);
}
private void OnGameOver(float gameTime)
{
LoseBannerShown?.Invoke(gameTime);
GameManager.Instance.SetPause(true);
SetVisible(losePanel, true);
}
private void SetVisible(CanvasGroup canvasGroup, bool visible, bool instant = false)
{
if (!canvasGroup) return;
canvasGroup.interactable = visible;
canvasGroup.blocksRaycasts = visible;
if (instant)
{
canvasGroup.alpha = visible ? 1f : 0f;
}
else
{
StopAllCoroutines();
StartCoroutine(Fade(canvasGroup, visible ? 1f : 0f, 0.15f));
}
}
/// <summary>
/// Метод для плавного изменения видимости
/// </summary>
/// <param name="cg">Группа холстов</param>
/// <param name="target">Целевое значение альфа-канала</param>
/// <param name="duration">Продолжительность перехода</param>
private IEnumerator Fade(CanvasGroup cg, float target, float duration)
{
float start = cg.alpha;
float t = 0f;
while (t < duration)
{
t += Time.unscaledDeltaTime;
cg.alpha = Mathf.Lerp(start, target, t / duration);
yield return null;
}
cg.alpha = target;
}
public static string FormatTime(float gameTime)
{
TimeSpan interval = TimeSpan.FromMilliseconds(gameTime);
return interval.ToString(@"mm\:ss\.fff");
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1b61ec11c32d8aa42b00ee29ce87def2

View File

@ -0,0 +1,134 @@
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using RR.Common.UI;
using RR.Game2D.Core.Managers;
public class LoseBannerController : MonoBehaviour
{
[SerializeField] private GameEndUIController bannersController;
[Header("Кнопки")]
[SerializeField] private TextMeshProUGUI loseText;
[SerializeField] private Button continueButton;
[SerializeField] private TextMeshProUGUI continueButtonText;
[SerializeField] private float continueButtonCountdown = 10f;
[SerializeField] private Button mainMenuButton;
private Coroutine continueCountdownCoroutine;
public event Action ContinueClicked;
private void OnEnable()
{
bannersController.LoseBannerShown += OnLoseBannerShown;
}
private void OnDisable()
{
bannersController.LoseBannerShown -= OnLoseBannerShown;
// Остановим корутину, если объект деактивируется
if (continueCountdownCoroutine != null)
{
StopCoroutine(continueCountdownCoroutine);
continueCountdownCoroutine = null;
}
}
private void Awake()
{
if (continueButton) continueButton.onClick.AddListener(OnContinueClicked);
// Инициализируем видимость/интерактивность кнопок в безопасное состояние
if (continueButton) continueButton.gameObject.SetActive(false);
if (continueButton && continueButtonText) continueButtonText.text = string.Empty;
}
private void OnLoseBannerShown(float gameTime)
{
loseText.text = $"Время игры: {GameEndUIController.FormatTime(gameTime)}";
if (continueButtonCountdown <= 0f)
{
ContinueClicked?.Invoke();
GameManager.Instance.SetPause(false);
GameManager.Instance.LoadScene("MainMenu", false);
}
if (continueButton)
{
continueButton.gameObject.SetActive(true);
continueButton.interactable = true;
}
//
// if (mainMenuButton)
// mainMenuButton.gameObject.SetActive(false);
// Остановим предыдущую корутину, если есть
if (continueCountdownCoroutine != null)
{
StopCoroutine(continueCountdownCoroutine);
continueCountdownCoroutine = null;
}
continueCountdownCoroutine = StartCoroutine(ContinueCountdownCoroutine(continueButtonCountdown));
}
private IEnumerator ContinueCountdownCoroutine(float duration)
{
float elapsed = 0f;
// Обновляем сразу первоначальный текст
int initialSeconds = Mathf.RoundToInt(duration);
UpdateContinueText(initialSeconds);
while (elapsed < duration)
{
// Using yield return null and manual timing with unscaled time for more reliable updates
float startTime = Time.unscaledTime;
yield return null;
// Wait for approximately 1 second using unscaled time
while (Time.unscaledTime - startTime < 1.0f)
{
yield return null;
}
elapsed += 1f;
int remaining = Mathf.Max(0, Mathf.RoundToInt(duration - elapsed));
UpdateContinueText(remaining);
}
if (continueButton)
{
ContinueClicked?.Invoke();
GameManager.Instance.SetPause(false);
GameManager.Instance.LoadScene("MainMenu", false);
}
continueCountdownCoroutine = null;
}
private void UpdateContinueText(int secondsRemaining)
{
if (continueButtonText)
continueButtonText.text = $"Продолжить? ({secondsRemaining})";
}
private void OnContinueClicked()
{
// При клике на "Продолжить" — остановим таймер и продолжим игру
if (continueCountdownCoroutine != null)
{
StopCoroutine(continueCountdownCoroutine);
continueCountdownCoroutine = null;
}
ContinueClicked?.Invoke();
GameManager.Instance.SetPause(false);
GameManager.Instance.ContinueGame();
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7fd0f5dd52ebd7f41a46cb02a7f83497

View File

@ -16,9 +16,30 @@ namespace RR.Common.UI
[SerializeField] private CanvasGroup panel;
[SerializeField] private Button resumeButton;
[SerializeField] private Button quitButton;
[Header("Связь с другими элементами")]
[SerializeField] private GameEndUIController bannerController;
//[SerializeField] private Button restartButton;
//[SerializeField] private KeyCode toggleKey = KeyCode.Escape; // Esc
private bool isActive = true;
private void OnEnable()
{
if (GameManager.Instance != null)
GameManager.Instance.PauseStateChanged += HandlePauseChanged;
bannerController.WinBannerShown += (f) => { isActive = false; };
bannerController.LoseBannerShown += (f) => { isActive = false; };
bannerController.BannerDismissed += () => { isActive = true; };
}
private void OnDisable()
{
if (GameManager.Instance != null)
GameManager.Instance.PauseStateChanged -= HandlePauseChanged;
}
private void Awake()
{
if (resumeButton) resumeButton.onClick.AddListener(OnResumeClicked);
@ -29,18 +50,6 @@ namespace RR.Common.UI
SetVisible(false, instant: true);
}
private void OnEnable()
{
if (GameManager.Instance != null)
GameManager.Instance.PauseStateChanged += HandlePauseChanged;
}
private void OnDisable()
{
if (GameManager.Instance != null)
GameManager.Instance.PauseStateChanged -= HandlePauseChanged;
}
private void Update()
{
if (close.action.WasPressedThisFrame())
@ -53,6 +62,7 @@ namespace RR.Common.UI
/// <param name="paused">Ставить ли на паузу</param>
private void HandlePauseChanged(bool paused)
{
if (!isActive) return;
SetVisible(paused);
}

View File

@ -5,7 +5,7 @@ using RR.Game2D.Core.Managers;
namespace RR.Common.UI
{
public class UITextController : MonoBehaviour
public class TopPanelTextController : MonoBehaviour
{
[SerializeField] private int livesCount;
[SerializeField] private int coinsCount;
@ -40,7 +40,7 @@ namespace RR.Common.UI
livesText.text = livesCount.ToString();
coinsText.text = coinsCount.ToString();
levelText.text = levelNumber.ToString();
levelText.text = levelNumber;
}
private void OnCoinsChanged(int newCoins)