Danro/Assets/Scripts/GameManager.cs
Danil Iarosh 152066e9a3 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
2025-10-31 17:03:20 +03:00

440 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.IO;
using UnityEngine;
using UnityEngine.SceneManagement;
using RR.Common.Managers;
using RR.Game2D.Behaviour;
using RR.Game2D.Entities;
namespace RR.Game2D.Core.Managers
{
[DefaultExecutionOrder(-1000)]
public class GameManager : MonoBehaviour
{
// Singleton instance
public static GameManager Instance { get; private set; }
// AudioManager instance
public AudioManager Audio {get; private set;}
[Header("Пауза")]
public bool IsPaused { get; private set; }
[Header("Базовые настройки игры")]
[SerializeField] private int maxLives = 3;
[Header("Информация для игры")]
public int World { get; private set; } = 1;
public int Stage { get; private set; } = 1;
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<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
//---------------------------
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void Bootstrap()
{
if (Instance == null)
{
var go = new GameObject(nameof(GameManager));
Instance = go.AddComponent<GameManager>();
DontDestroyOnLoad(go);
}
}
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
if (Audio == null)
{
Audio = gameObject.AddComponent<AudioManager>();
Audio.Initialize();
}
gameStartTime = Time.time;
if (SceneManager.GetActiveScene().name != "MainMenu") _shouldSpawnPlayer = true;
}
private void OnDestroy()
{
if (Instance == this) Instance = null;
}
//---------------------------
//
//---------------------------
//private void Start()
//{
// //Application.targetFrameRate = 60;
// //LoadScene("MainMenu", false);
// //NewGame();
// if (SceneManager.GetActiveScene().name != "MainMenu")
// {
// TrySpawnPlayerForActiveScene();
// }
//}
/// <summary>
/// Поиск префаба игрока.
/// Если не найдет префаб из инспектора, ищет в "Resources"
/// </summary>
/// <returns> Префаб игрока </returns>
private GameObject ResolvePlayerPrefab()
{
if (playerPrefab != null) return playerPrefab;
GameObject loaded = Resources.Load<GameObject>(playerPrefabResourcePath);
if (loaded == null)
{
Debug.LogError(
$"GameManager: playerPrefab is null. Resources.Load('{playerPrefabResourcePath}') вернул null. " +
$"Ожидается файл: Assets/Resources/{playerPrefabResourcePath}.prefab (точное совпадение регистра).");
}
return loaded;
}
/// <summary>
/// Начало новой игры.
/// Загружает первый уровень.
/// Устанавливает жизни на 3, монеты на 0
/// </summary>
public void NewGame()
{
Lives = maxLives;
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>
/// Окончание игры.
/// Загружает сцену главного меню
/// </summary>
private void GameOver() // => LoadScene("MainMenu", false);
{
// 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>
/// Загрузка уровня по данным.
/// Загружает уровень по номерам мира и уровня
/// </summary>
/// <param name="world">Номер мира</param>
/// <param name="stage">Номер уровня</param>
public void LoadLevel(int world, int stage)
{
this.World = world;
this.Stage = stage;
string sceneName = $"{world}-{stage}";
if (SceneInBuildSettingsExists(sceneName))
{
LoadScene(sceneName);
}
else
{
//NewGame();
// Debug.Log("WIN");
// LoadScene("MainMenu", false);
// Можно заменить на другое поведение
GameWin();
// --
}
}
/// <summary>
/// Загрузка сцены по названию.
/// Вызывает одноименный метод в SceneManager.
/// Может загружать сцены с UI и без.
/// В сценах без UI игрок не спавнится
/// </summary>
/// <param name="sceneName">Строковое название сцены</param>
/// <param name="useUI">Использовать ли UI</param>
public void LoadScene(string sceneName, bool useUI = true)
{
_shouldSpawnPlayer = true;
SceneManager.LoadScene(sceneName);
if (useUI)
{
SceneManager.LoadScene("Level UI", LoadSceneMode.Additive);
}
else _shouldSpawnPlayer = false;
}
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
string currentSceneName = SceneManager.GetActiveScene().name;
if (!currentSceneName.Contains("-")) return;
TrySpawnPlayerForActiveScene();
if (currentSceneName == $"{World}-{Stage}") return;
string[] levelInfo = currentSceneName.Split("-");
World = Convert.ToInt32(levelInfo[0]);
Stage = Convert.ToInt32(levelInfo[1]);
}
/// <summary>
/// Метод для спавна игрока.
/// Ищет первый спавнпоинт, спавнит игрока на его позиции.
/// Если на карте есть объект игрока, удаляет его и спавнит свой
/// </summary>
private void TrySpawnPlayerForActiveScene()
{
if (!_shouldSpawnPlayer) return;
PlayerSpawn spawn = FindFirstObjectByType<PlayerSpawn>();
Vector3 pos = spawn ? spawn.transform.position : new Vector3(0, 1.3f, 0);
Quaternion rot = spawn ? spawn.transform.rotation : Quaternion.identity;
PlayerObj = GameObject.FindGameObjectWithTag("Player");
if (PlayerObj != null) Destroy(PlayerObj);
var prefab = ResolvePlayerPrefab();
if (prefab == null) return;
PlayerObj = Instantiate(prefab, pos, rot);
SceneManager.MoveGameObjectToScene(PlayerObj, SceneManager.GetActiveScene());
PlayerSpawned?.Invoke(PlayerObj);
}
// Новый метод: респавн игрока без перезагрузки сцены
public void RespawnPlayer()
{
// отнимем жизнь (оставляем текущее поведение); монеты не сбрасываем
Lives--;
LivesChanged?.Invoke(Lives);
if (Lives <= 0)
{
GameOver();
return;
}
// Определяем дефолтные позицию и направление из PlayerSpawn в сцене
PlayerSpawn spawn = FindFirstObjectByType<PlayerSpawn>();
Vector3 defaultPos = spawn ? spawn.transform.position : new Vector3(0, 1.3f, 0);
Vector2 defaultForward = spawn ? spawn.defaultForward : Vector2.right;
// Если есть активный чекпоинт — берём из него; иначе дефолт
Vector3 respawnPos = defaultPos;
Vector2 respawnForward = defaultForward;
if (CheckpointSystem.Instance != null)
{
respawnPos = CheckpointSystem.Instance.GetSpawnPosition(defaultPos);
respawnForward = CheckpointSystem.Instance.GetSpawnForward(defaultForward);
if (CheckpointSystem.Instance.Active != null)
{
respawnPos = CheckpointSystem.Instance.Active.GetSpawnPosition();
respawnForward = CheckpointSystem.Instance.Active.GetForward();
}
}
// Если потеряли ссылку — попробуем найти игрока в сцене
if (PlayerObj == null)
PlayerObj = GameObject.FindGameObjectWithTag("Player");
if (PlayerObj == null)
{
// Создадим нового игрока, если объект был уничтожен
var prefab = ResolvePlayerPrefab();
if (prefab == null) return;
PlayerObj = Instantiate(prefab, respawnPos, Quaternion.identity);
SceneManager.MoveGameObjectToScene(PlayerObj, SceneManager.GetActiveScene());
PlayerSpawned?.Invoke(PlayerObj);
}
else
{
// Остановим движение и телепортируем
var rb = PlayerObj.GetComponent<Rigidbody2D>();
if (rb != null)
{
rb.linearVelocity = Vector2.zero;
rb.angularVelocity = 0f;
}
PlayerObj.transform.position = respawnPos;
}
// Повернём в нужную сторону (по X)
if (PlayerObj != null)
{
if (respawnForward.x != 0)
{
PlayerObj.transform.localEulerAngles = respawnForward.x >= 0
? Vector3.zero
: new Vector3(0, 180f, 0);
}
// Восстановим здоровье
var hp = PlayerObj.GetComponent<Health>();
if (hp != null)
hp.ResetHealth();
}
}
/// <summary>
/// Метод переход на следующий уровень.
/// Может загружать уровни и миры в прямой зависимости от
/// Количества уровней на мир(StagesByWorld)
/// </summary>
public void NextLevel()
{
if (Stage >= StagesByWorld)
{
World++;
Stage = 1;
}
else Stage++;
LoadLevel(World, Stage);
}
/// <summary>
/// Метод сброса уровня.
/// При сбросе также сбрасывает монеты и отнимает жизни
/// </summary>
public void ResetLevel()
{
Lives--;
LivesChanged?.Invoke(Lives);
Coins = 0;
CoinsChanged?.Invoke(Coins);
//print($"Lives: {lives}");
if (Lives > 0) LoadLevel(World, Stage);
else GameOver();
}
/// <summary>
/// Метод добавления монеты
/// </summary>
public void AddCoin(int count = 1)
{
Coins += count;
// Можно заменить на другое поведение
if (Coins >= 100)
{
Coins -= 100;
AddLife();
}
// --
CoinsChanged?.Invoke(Coins);
}
/// <summary>
/// Метод паузы игры.
/// Ставит на паузу все SFX и саму игру
/// </summary>
/// <param name="pause">Ставить ли на паузу</param>
public void SetPause(bool pause)
{
if (IsPaused == pause) return;
IsPaused = pause;
Time.timeScale = pause ? 0f : 1f;
Audio.SmoothPauseMusic(pause);
PauseStateChanged?.Invoke(IsPaused);
}
// Можно заменить на другое поведение
/// <summary>
/// Метод переключателя паузы.
/// </summary>
public void TogglePause() => SetPause(!IsPaused);
// --
// Можно заменить на другое поведение
/// <summary>
/// Метод для добавления жизни.
/// </summary>
private void AddLife()
{
Lives++;
LivesChanged?.Invoke(Lives);
}
// --
/// <summary>
/// Метод проверки сцены на существование в настройках билда
/// </summary>
/// <param name="sceneName">Строковое название сцены</param>
/// <returns></returns>
private static bool SceneInBuildSettingsExists(string sceneName)
{
var count = SceneManager.sceneCountInBuildSettings;
for (var i = 0; i < count; i++)
{
var path = SceneUtility.GetScenePathByBuildIndex(i);
if (Path.GetFileNameWithoutExtension(path) == sceneName)
return true;
}
return false;
}
}
}