Danro/Assets/Scripts/GameManager.cs
Danil Iarosh a845161079 feat(audio): Integrated level-specific sound effects and music control
-Add LevelSpecificPlayerSFX prefabs for levels 1-1 and 1-2
-Configure checkpoints to activate sound effects, including pitch, volume, and audio editing
-Rename the checkpoint effect object to Checkpoint VFX
-Update the audio manager, the default value of the music target volume is 0.2
-Add SoundEffect structure to support sound parameter configuration
-Optimize audio source creation and crossfade logic
-Remove the old music control components and use the new audio management method
2025-10-29 16:46:24 +03:00

410 lines
15 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; }
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 (Audio == null)
{
Audio = gameObject.AddComponent<AudioManager>();
Audio.Initialize();
}
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;
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;
}
}
}