2025-09-19 19:43:49 +05:00
|
|
|
|
using UnityEngine;
|
2025-09-19 14:56:58 +05:00
|
|
|
|
using System.Collections.Generic;
|
2025-09-19 19:43:49 +05:00
|
|
|
|
using Cysharp.Threading.Tasks;
|
|
|
|
|
using BulletHellTemplate.Audio;
|
2025-09-19 14:56:58 +05:00
|
|
|
|
|
|
|
|
|
namespace BulletHellTemplate
|
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
|
2025-09-19 14:56:58 +05:00
|
|
|
|
/// <summary>
|
2025-09-19 19:43:49 +05:00
|
|
|
|
/// Manages all game audio, storing per-category volumes (Master, VFX, Ambience, Custom tags)
|
|
|
|
|
/// and computing the effective output as <c>Master × Category</c>.
|
|
|
|
|
/// Volumes are persisted with <see cref="SecurePrefs"/> in an encrypted JSON file so
|
|
|
|
|
/// they survive the “clean login” flow.
|
2025-09-19 14:56:58 +05:00
|
|
|
|
/// </summary>
|
2025-09-19 19:43:49 +05:00
|
|
|
|
public sealed class AudioManager : MonoBehaviour
|
2025-09-19 14:56:58 +05:00
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
/*────────────────────────── Serialized Fields ──────────────────────────*/
|
|
|
|
|
[Header("Audio Sources")]
|
|
|
|
|
[Tooltip("Main AudioSource for sounds that follow the listener (Master output).")]
|
2025-09-19 14:56:58 +05:00
|
|
|
|
public AudioSource masterAudioSource;
|
2025-09-19 19:43:49 +05:00
|
|
|
|
[Tooltip("Dedicated looping AudioSource for ambient tracks.")]
|
|
|
|
|
public AudioSource ambientAudioSource;
|
|
|
|
|
[Tooltip("One-shot AudioSource for loading/menu clips.")]
|
|
|
|
|
public AudioSource loadingAudioSource;
|
2025-09-19 14:56:58 +05:00
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
[Header("Volume Settings (0–1)")]
|
|
|
|
|
[Range(0, 1)] public float masterVolume = 1f; // Multiplicative root
|
|
|
|
|
[Range(0, 1)] public float vfxVolume = 1f; // Relative to Master
|
|
|
|
|
[Range(0, 1)] public float ambienceVolume = 1f; // Relative to Master
|
2025-09-19 14:56:58 +05:00
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
[Tooltip("Extra categories (e.g. “ui”, “others”)")]
|
|
|
|
|
public List<CustomAudioTag> customTagVolumes = new();
|
2025-09-19 14:56:58 +05:00
|
|
|
|
|
|
|
|
|
[Header("Audio Limitations")]
|
2025-09-19 19:43:49 +05:00
|
|
|
|
[Tooltip("Global cap – simultaneous clips (0 = unlimited).")]
|
|
|
|
|
public int maxConcurrentAudio = 20;
|
2025-09-19 14:56:58 +05:00
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
/*────────────────────────── Runtime ───────────────────────────*/
|
|
|
|
|
private int _currentAudioCount;
|
2025-09-19 14:56:58 +05:00
|
|
|
|
public static AudioManager Singleton;
|
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
/*────────────────────────── Persistence Keys ──────────────────────────*/
|
|
|
|
|
private const string KEY_MASTER = "AUDIO_MASTER_VOLUME";
|
|
|
|
|
private const string KEY_VFX = "AUDIO_VFX_VOLUME";
|
|
|
|
|
private const string KEY_AMBIENCE = "AUDIO_AMBIENCE_VOLUME";
|
|
|
|
|
|
|
|
|
|
/*────────────────────────── Unity - Lifecycle ─────────────────────────*/
|
2025-09-19 14:56:58 +05:00
|
|
|
|
private void Awake()
|
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
if (Singleton != null) { Destroy(gameObject); return; }
|
|
|
|
|
|
|
|
|
|
Singleton = this;
|
|
|
|
|
DontDestroyOnLoad(gameObject);
|
|
|
|
|
|
|
|
|
|
ambientAudioSource.loop = true;
|
|
|
|
|
LoadVolumeSettings();
|
|
|
|
|
ApplyVolumeSettings();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*────────────────────────── Public API – Volume Setters ───────────────*/
|
|
|
|
|
public void SetMasterVolume(float value)
|
|
|
|
|
{
|
|
|
|
|
masterVolume = Mathf.Clamp01(value);
|
|
|
|
|
ApplyVolumeSettings();
|
|
|
|
|
SaveVolumeSettings();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void SetVFXVolume(float value)
|
|
|
|
|
{
|
|
|
|
|
vfxVolume = Mathf.Clamp01(value);
|
|
|
|
|
ApplyVolumeSettings();
|
|
|
|
|
SaveVolumeSettings();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void SetAmbienceVolume(float value)
|
|
|
|
|
{
|
|
|
|
|
ambienceVolume = Mathf.Clamp01(value);
|
|
|
|
|
ApplyVolumeSettings();
|
|
|
|
|
SaveVolumeSettings();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>Sets a custom tag volume (e.g. “ui”, “others”).</summary>
|
|
|
|
|
public void SetCustomTagVolume(string tag, float value)
|
|
|
|
|
{
|
|
|
|
|
value = Mathf.Clamp01(value);
|
|
|
|
|
for (int i = 0; i < customTagVolumes.Count; ++i)
|
|
|
|
|
if (customTagVolumes[i].tag == tag)
|
|
|
|
|
{
|
|
|
|
|
customTagVolumes[i] = new CustomAudioTag { tag = tag, volume = value };
|
|
|
|
|
SaveVolumeSettings();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
customTagVolumes.Add(new CustomAudioTag { tag = tag, volume = value });
|
|
|
|
|
SaveVolumeSettings();
|
2025-09-19 14:56:58 +05:00
|
|
|
|
}
|
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
/*────────────────────────── Playback Helpers ──────────────────────────*/
|
|
|
|
|
|
2025-09-19 14:56:58 +05:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Plays an ambient audio clip in a loop with the specified tag.
|
|
|
|
|
/// Does not affect the current audio count or maxConcurrentAudio.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="clip">The audio clip to play.</param>
|
|
|
|
|
/// <param name="tag">The tag used to determine the volume.</param>
|
2025-09-19 19:43:49 +05:00
|
|
|
|
public void PlayAmbientAudio(AudioClip clip, string tag = "ambient")
|
2025-09-19 14:56:58 +05:00
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
if (clip == null) return;
|
|
|
|
|
loadingAudioSource.Stop();
|
2025-09-19 14:56:58 +05:00
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
ambientAudioSource.Stop();
|
2025-09-19 14:56:58 +05:00
|
|
|
|
ambientAudioSource.clip = clip;
|
2025-09-19 19:43:49 +05:00
|
|
|
|
ambientAudioSource.volume = GetEffectiveVolume(tag);
|
2025-09-19 14:56:58 +05:00
|
|
|
|
ambientAudioSource.Play();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Plays a loading menu audio clip and stops the ambient audio.
|
|
|
|
|
/// The ambient audio will be stopped and prevented from playing again until reloaded.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="clip">The audio clip to play.</param>
|
|
|
|
|
/// <param name="tag">The tag used to determine the volume.</param>
|
2025-09-19 19:43:49 +05:00
|
|
|
|
public void PlayLoadingMenu(AudioClip clip, string tag = "master")
|
2025-09-19 14:56:58 +05:00
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
if (clip == null) return;
|
|
|
|
|
ambientAudioSource.Stop();
|
|
|
|
|
|
|
|
|
|
loadingAudioSource.volume = GetEffectiveVolume(tag);
|
|
|
|
|
loadingAudioSource.PlayOneShot(clip);
|
2025-09-19 14:56:58 +05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-09-19 19:43:49 +05:00
|
|
|
|
/// New signature – plays the clip at an arbitrary **world position**.
|
|
|
|
|
/// Ideal para explosões, projéteis, etc.
|
2025-09-19 14:56:58 +05:00
|
|
|
|
/// </summary>
|
2025-09-19 19:43:49 +05:00
|
|
|
|
public void PlayAudio(AudioClip clip, string tag, Vector3 worldPos)
|
2025-09-19 14:56:58 +05:00
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
if (clip == null) return;
|
|
|
|
|
if (maxConcurrentAudio > 0 && _currentAudioCount >= maxConcurrentAudio) return;
|
2025-09-19 14:56:58 +05:00
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
float vol = GetEffectiveVolume(tag);
|
|
|
|
|
SoundEffectsPool.PlayClip(clip, worldPos, vol);
|
2025-09-19 14:56:58 +05:00
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
_currentAudioCount++;
|
|
|
|
|
ReduceAudioCountAsync(clip.length).Forget();
|
2025-09-19 14:56:58 +05:00
|
|
|
|
}
|
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Old signature – plays *at the listener* (2D).
|
|
|
|
|
/// </summary>
|
|
|
|
|
public void PlayAudio(AudioClip clip, string tag)
|
|
|
|
|
=> PlayAudio(clip, tag, Camera.main ? Camera.main.transform.position : Vector3.zero);
|
2025-09-19 14:56:58 +05:00
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Stops all currently playing audio, including ambient and loading audio.
|
|
|
|
|
/// This function is useful when the game ends or the player dies,
|
|
|
|
|
/// ensuring that no audio is left playing.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public void StopAllAudioPlay()
|
|
|
|
|
{
|
|
|
|
|
masterAudioSource.Stop();
|
|
|
|
|
ambientAudioSource.Stop();
|
|
|
|
|
loadingAudioSource.Stop();
|
|
|
|
|
_currentAudioCount = 0;
|
2025-09-19 14:56:58 +05:00
|
|
|
|
}
|
|
|
|
|
|
2025-09-19 19:43:49 +05:00
|
|
|
|
public void StopLoadingAudioPlay() => loadingAudioSource.Stop();
|
|
|
|
|
|
2025-09-19 14:56:58 +05:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets the volume based on the tag.
|
|
|
|
|
/// </summary>
|
2025-09-19 19:43:49 +05:00
|
|
|
|
private float GetEffectiveVolume(string tag)
|
2025-09-19 14:56:58 +05:00
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
tag = tag.ToLowerInvariant();
|
|
|
|
|
if (tag == "master") return masterVolume;
|
|
|
|
|
|
|
|
|
|
float category = tag switch
|
2025-09-19 14:56:58 +05:00
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
"vfx" => vfxVolume,
|
|
|
|
|
"ambient" => ambienceVolume,
|
|
|
|
|
_ => GetCustomTag(tag)
|
|
|
|
|
};
|
|
|
|
|
return masterVolume * category;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private float GetCustomTag(string tag)
|
|
|
|
|
{
|
|
|
|
|
foreach (var t in customTagVolumes)
|
|
|
|
|
if (t.tag == tag) return t.volume;
|
|
|
|
|
Debug.LogWarning($"Audio tag '{tag}' not found – using 1.");
|
|
|
|
|
return 1f;
|
2025-09-19 14:56:58 +05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Reduces the audio count after a certain amount of time (the clip length).
|
|
|
|
|
/// </summary>
|
2025-09-19 19:43:49 +05:00
|
|
|
|
private async UniTaskVoid ReduceAudioCountAsync(float seconds)
|
2025-09-19 14:56:58 +05:00
|
|
|
|
{
|
2025-09-19 19:43:49 +05:00
|
|
|
|
await UniTask.Delay(System.TimeSpan.FromSeconds(seconds));
|
|
|
|
|
_currentAudioCount = Mathf.Max(0, _currentAudioCount - 1);
|
2025-09-19 14:56:58 +05:00
|
|
|
|
}
|
2025-09-19 19:43:49 +05:00
|
|
|
|
|
|
|
|
|
/*────────────────────────── Persistence ──────────────────────────*/
|
|
|
|
|
private void LoadVolumeSettings()
|
|
|
|
|
{
|
|
|
|
|
masterVolume = SecurePrefs.GetDecryptedFloatFromFile(KEY_MASTER, masterVolume);
|
|
|
|
|
vfxVolume = SecurePrefs.GetDecryptedFloatFromFile(KEY_VFX, vfxVolume);
|
|
|
|
|
ambienceVolume = SecurePrefs.GetDecryptedFloatFromFile(KEY_AMBIENCE, ambienceVolume);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void SaveVolumeSettings()
|
|
|
|
|
{
|
|
|
|
|
SecurePrefs.SetEncryptedFloatToFile(KEY_MASTER, masterVolume);
|
|
|
|
|
SecurePrefs.SetEncryptedFloatToFile(KEY_VFX, vfxVolume);
|
|
|
|
|
SecurePrefs.SetEncryptedFloatToFile(KEY_AMBIENCE, ambienceVolume);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ApplyVolumeSettings()
|
|
|
|
|
{
|
|
|
|
|
masterAudioSource.volume = masterVolume;
|
|
|
|
|
ambientAudioSource.volume = masterVolume * ambienceVolume;
|
|
|
|
|
loadingAudioSource.volume = masterVolume;
|
|
|
|
|
}
|
2025-09-19 14:56:58 +05:00
|
|
|
|
}
|
2025-09-19 19:43:49 +05:00
|
|
|
|
/// <summary>Simple pair (tag, volume) used by <see cref="AudioManager"/>.</summary>
|
|
|
|
|
[System.Serializable]
|
|
|
|
|
public struct CustomAudioTag
|
|
|
|
|
{
|
|
|
|
|
public string tag;
|
|
|
|
|
[Range(0, 1)] public float volume;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-19 14:56:58 +05:00
|
|
|
|
}
|