399 lines
14 KiB
C#
399 lines
14 KiB
C#
using System;
|
||
using System.IO;
|
||
using UnityEngine;
|
||
using UnityEngine.SceneManagement;
|
||
|
||
using RR.Game2D.Behaviour;
|
||
using RR.Game2D.Entities;
|
||
|
||
namespace RR.Game2D.Core
|
||
{
|
||
[DefaultExecutionOrder(-1000)]
|
||
public class GameManager : MonoBehaviour
|
||
{
|
||
public static GameManager Instance { get; private set; }
|
||
|
||
[Header("Пауза")]
|
||
public bool IsPaused { get; private set; }
|
||
public event Action<bool> PauseStateChanged;
|
||
|
||
[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; } = 5;
|
||
public int Lives { get; set; } = 3;
|
||
public int Coins { get; private set; } = 0;
|
||
|
||
[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;
|
||
|
||
//---------------------------
|
||
// 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 (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);
|
||
LoadLevel(1, 1);
|
||
}
|
||
|
||
// Можно заменить на другое поведение
|
||
/// <summary>
|
||
/// Окончание игры.
|
||
/// Загружает сцену главного меню
|
||
/// </summary>
|
||
private void GameOver() // => LoadScene("MainMenu", false);
|
||
{
|
||
LoadScene("MainMenu", false);
|
||
Lives = maxLives;
|
||
LivesChanged?.Invoke(Lives);
|
||
}
|
||
// --
|
||
|
||
/// <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();
|
||
// Можно заменить на другое поведение
|
||
print("WIN");
|
||
LoadScene("MainMenu", false);
|
||
// --
|
||
}
|
||
}
|
||
|
||
/// <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()
|
||
{
|
||
Coins++;
|
||
// Можно заменить на другое поведение
|
||
if (Coins >= 100)
|
||
{
|
||
Coins = 0;
|
||
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;
|
||
AudioListener.pause = 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;
|
||
}
|
||
}
|
||
} |