Projext/Assets/Scripts/Player/Sound/SoundManager.cs
2026-02-22 22:37:34 +09:00

255 lines
13 KiB
C#

using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
using System.Collections; // 코루틴을 사용할거에요 -> System.Collections를
using System.Collections.Generic; // 딕셔너리를 사용할거에요 -> System.Collections.Generic을
/// <summary>
/// 전역 사운드 매니저 (싱글톤)
/// BGM, 환경음(Ambience), SFX(효과음)를 통합 관리
/// 빈 오브젝트에 부착 후 인스펙터에서 AudioClip 연결
/// </summary>
public class SoundManager : MonoBehaviour // 클래스를 선언할거에요 -> 전역 사운드 매니저를
{
public static SoundManager Instance { get; private set; } // 프로퍼티를 선언할거에요 -> 싱글톤 인스턴스를
// ============================================
// AudioSource 채널 (자동 생성)
// ============================================
private AudioSource _bgmSource; // 변수를 선언할거에요 -> BGM 전용 채널을
private AudioSource _ambienceSource; // 변수를 선언할거에요 -> 환경음 전용 채널을
private AudioSource _sfxSource; // 변수를 선언할거에요 -> 효과음 전용 채널을
// ============================================
// BGM
// ============================================
[Header("--- BGM ---")] // 인스펙터 제목을 달거에요 -> BGM 설정을
[SerializeField] private AudioClip bgm_dungeon; // 변수를 선언할거에요 -> 던전 BGM을
[SerializeField] private AudioClip bgm_town; // 변수를 선언할거에요 -> 마을 BGM을
[Range(0f, 1f)]
[SerializeField] private float bgmVolume = 0.3f; // 변수를 선언할거에요 -> BGM 볼륨을
[SerializeField] private float bgmFadeDuration = 1.5f; // 변수를 선언할거에요 -> BGM 전환 페이드 시간을
// ============================================
// 환경음 (Ambience)
// ============================================
[Header("--- 환경음 ---")] // 인스펙터 제목을 달거에요 -> 환경음 설정을
[SerializeField] private AudioClip dungeon_ambience; // 변수를 선언할거에요 -> 던전 분위기 루프를
[Range(0f, 1f)]
[SerializeField] private float ambienceVolume = 0.15f; // 변수를 선언할거에요 -> 환경음 볼륨을
// ============================================
// UI 효과음
// ============================================
[Header("--- UI SFX ---")] // 인스펙터 제목을 달거에요 -> UI 효과음을
[SerializeField] private AudioClip sfx_levelUp; // 변수를 선언할거에요 -> 레벨업 효과음을
[SerializeField] private AudioClip sfx_itemPickup; // 변수를 선언할거에요 -> 아이템 줍기 효과음을
[SerializeField] private AudioClip sfx_uiClick; // 변수를 선언할거에요 -> UI 클릭 효과음을
[Range(0f, 1f)]
[SerializeField] private float sfxVolume = 0.5f; // 변수를 선언할거에요 -> 효과음 볼륨을
// ============================================
// 초기화
// ============================================
private void Awake() // 함수를 실행할거에요 -> 초기화 Awake를
{
// 싱글톤 설정 + 씬 전환 시 파괴 안 됨
if (Instance == null) // 조건이 맞으면 실행할거에요 -> 인스턴스가 없다면
{
Instance = this; // 설정할거에요 -> 나를 인스턴스로
DontDestroyOnLoad(gameObject); // 설정할거에요 -> 씬 전환 시 파괴 안 되게
}
else // 조건이 틀리면 실행할거에요 -> 이미 있다면
{
Destroy(gameObject); // 파괴할거에요 -> 중복 인스턴스를
return; // 중단할거에요
}
// AudioSource 채널 3개 자동 생성
_bgmSource = gameObject.AddComponent<AudioSource>(); // 추가할거에요 -> BGM 채널을
_bgmSource.loop = true; // 설정할거에요 -> 반복 재생으로
_bgmSource.playOnAwake = false; // 설정할거에요 -> 자동 재생 끄기로
_bgmSource.volume = bgmVolume; // 설정할거에요 -> 볼륨을
_ambienceSource = gameObject.AddComponent<AudioSource>(); // 추가할거에요 -> 환경음 채널을
_ambienceSource.loop = true; // 설정할거에요 -> 반복 재생으로
_ambienceSource.playOnAwake = false; // 설정할거에요 -> 자동 재생 끄기로
_ambienceSource.volume = ambienceVolume; // 설정할거에요 -> 볼륨을
_sfxSource = gameObject.AddComponent<AudioSource>(); // 추가할거에요 -> 효과음 채널을
_sfxSource.playOnAwake = false; // 설정할거에요 -> 자동 재생 끄기로
_sfxSource.volume = sfxVolume; // 설정할거에요 -> 볼륨을
}
// ============================================
// BGM 제어
// ============================================
/// <summary>
/// BGM을 페이드 전환으로 변경
/// </summary>
public void PlayBGM(string bgmName) // 함수를 선언할거에요 -> BGM 재생을
{
AudioClip clip = GetBGMClip(bgmName); // 가져올거에요 -> 이름에 맞는 클립을
if (clip == null) return; // 조건이 맞으면 중단할거에요 -> 클립이 없으면
if (_bgmSource.clip == clip && _bgmSource.isPlaying) return; // 조건이 맞으면 중단할거에요 -> 같은 BGM이 이미 재생 중이면
StartCoroutine(CrossfadeBGM(clip)); // 실행할거에요 -> 크로스페이드 전환을
}
/// <summary>
/// BGM 정지 (페이드 아웃)
/// </summary>
public void StopBGM() // 함수를 선언할거에요 -> BGM 정지를
{
StartCoroutine(FadeOut(_bgmSource, bgmFadeDuration)); // 실행할거에요 -> 페이드 아웃을
}
private IEnumerator CrossfadeBGM(AudioClip newClip) // 코루틴을 정의할거에요 -> BGM 크로스페이드를
{
// 현재 재생 중이면 페이드 아웃
if (_bgmSource.isPlaying) // 조건이 맞으면 실행할거에요 -> 재생 중이면
{
yield return FadeOut(_bgmSource, bgmFadeDuration * 0.5f); // 실행할거에요 -> 절반 시간으로 페이드 아웃
}
// 새 클립으로 교체 후 페이드 인
_bgmSource.clip = newClip; // 설정할거에요 -> 새 클립을
_bgmSource.Play(); // 재생할거에요 -> BGM을
yield return FadeIn(_bgmSource, bgmVolume, bgmFadeDuration * 0.5f); // 실행할거에요 -> 페이드 인을
}
private AudioClip GetBGMClip(string name) // 함수를 선언할거에요 -> 이름으로 BGM 클립을 찾는
{
switch (name.ToLower()) // 분기할거에요 -> 소문자로 변환한 이름으로
{
case "dungeon": return bgm_dungeon; // 반환할거에요 -> 던전 BGM을
case "town": return bgm_town; // 반환할거에요 -> 마을 BGM을
default: // 기본값이면
Debug.LogWarning($"[SoundManager] BGM '{name}'을 찾을 수 없습니다."); // 경고를 출력할거에요
return null; // 반환할거에요 -> null을
}
}
// ============================================
// 환경음 제어
// ============================================
public void PlayAmbience(string name) // 함수를 선언할거에요 -> 환경음 재생을
{
AudioClip clip = null; // 변수를 초기화할거에요
if (name.ToLower() == "dungeon") clip = dungeon_ambience; // 조건이 맞으면 설정할거에요
if (clip == null) return; // 조건이 맞으면 중단할거에요
if (_ambienceSource.clip == clip && _ambienceSource.isPlaying) return; // 이미 재생 중이면 중단
_ambienceSource.clip = clip; // 설정할거에요 -> 클립을
_ambienceSource.Play(); // 재생할거에요 -> 환경음을
}
public void StopAmbience() // 함수를 선언할거에요 -> 환경음 정지를
{
StartCoroutine(FadeOut(_ambienceSource, 1f)); // 실행할거에요 -> 페이드 아웃을
}
// ============================================
// SFX (효과음) — 전역에서 재생
// ============================================
/// <summary>
/// 이름으로 UI/전역 효과음 재생
/// </summary>
public void PlaySFX(string sfxName) // 함수를 선언할거에요 -> 효과음 재생을
{
AudioClip clip = GetSFXClip(sfxName); // 가져올거에요 -> 이름에 맞는 클립을
if (clip != null) _sfxSource.PlayOneShot(clip, sfxVolume); // 재생할거에요 -> 효과음을
}
/// <summary>
/// 외부에서 AudioClip을 직접 넘겨서 재생
/// </summary>
public void PlaySFX(AudioClip clip, float volume = -1f) // 함수를 선언할거에요 -> 클립 직접 재생을
{
if (clip == null) return; // 조건이 맞으면 중단할거에요
float vol = volume < 0 ? sfxVolume : volume; // 값을 결정할거에요 -> 볼륨을
_sfxSource.PlayOneShot(clip, vol); // 재생할거에요 -> 효과음을
}
/// <summary>
/// 3D 공간 위치에서 효과음 재생 (몬스터 사망 등)
/// </summary>
public void PlaySFXAtPosition(AudioClip clip, Vector3 position, float volume = -1f) // 함수를 선언할거에요 -> 위치 기반 효과음 재생을
{
if (clip == null) return; // 조건이 맞으면 중단할거에요
float vol = volume < 0 ? sfxVolume : volume; // 값을 결정할거에요 -> 볼륨을
AudioSource.PlayClipAtPoint(clip, position, vol); // 재생할거에요 -> 해당 위치에서 효과음을
}
private AudioClip GetSFXClip(string name) // 함수를 선언할거에요 -> 이름으로 SFX 클립을 찾는
{
switch (name.ToLower()) // 분기할거에요 -> 소문자로
{
case "levelup": return sfx_levelUp; // 반환할거에요 -> 레벨업을
case "pickup": return sfx_itemPickup; // 반환할거에요 -> 아이템 줍기를
case "click": return sfx_uiClick; // 반환할거에요 -> UI 클릭을
default: // 기본값이면
Debug.LogWarning($"[SoundManager] SFX '{name}'을 찾을 수 없습니다."); // 경고를 출력할거에요
return null; // 반환할거에요 -> null을
}
}
// ============================================
// 볼륨 조절 (설정 메뉴용)
// ============================================
public void SetBGMVolume(float volume) // 함수를 선언할거에요 -> BGM 볼륨 조절을
{
bgmVolume = Mathf.Clamp01(volume); // 값을 제한할거에요 -> 0~1 사이로
_bgmSource.volume = bgmVolume; // 적용할거에요 -> 볼륨을
}
public void SetSFXVolume(float volume) // 함수를 선언할거에요 -> SFX 볼륨 조절을
{
sfxVolume = Mathf.Clamp01(volume); // 값을 제한할거에요 -> 0~1 사이로
_sfxSource.volume = sfxVolume; // 적용할거에요 -> 볼륨을
}
public void SetAmbienceVolume(float volume) // 함수를 선언할거에요 -> 환경음 볼륨 조절을
{
ambienceVolume = Mathf.Clamp01(volume); // 값을 제한할거에요 -> 0~1 사이로
_ambienceSource.volume = ambienceVolume; // 적용할거에요 -> 볼륨을
}
// ============================================
// 페이드 유틸리티
// ============================================
private IEnumerator FadeOut(AudioSource source, float duration) // 코루틴을 정의할거에요 -> 페이드 아웃을
{
float startVol = source.volume; // 값을 저장할거에요 -> 시작 볼륨을
float elapsed = 0f; // 변수를 초기화할거에요 -> 경과 시간을
while (elapsed < duration) // 반복할거에요 -> 지속 시간 동안
{
elapsed += Time.unscaledDeltaTime; // 더할거에요 -> 시간을 (히트스톱 영향 안 받게)
source.volume = Mathf.Lerp(startVol, 0f, elapsed / duration); // 보간할거에요 -> 볼륨을 0으로
yield return null; // 다음 프레임까지 기다릴거에요
}
source.volume = 0f; // 확정할거에요 -> 볼륨 0으로
source.Stop(); // 정지할거에요 -> 재생을
}
private IEnumerator FadeIn(AudioSource source, float targetVolume, float duration) // 코루틴을 정의할거에요 -> 페이드 인을
{
source.volume = 0f; // 초기화할거에요 -> 볼륨을 0으로
float elapsed = 0f; // 변수를 초기화할거에요 -> 경과 시간을
while (elapsed < duration) // 반복할거에요 -> 지속 시간 동안
{
elapsed += Time.unscaledDeltaTime; // 더할거에요 -> 시간을
source.volume = Mathf.Lerp(0f, targetVolume, elapsed / duration); // 보간할거에요 -> 볼륨을 목표까지
yield return null; // 다음 프레임까지 기다릴거에요
}
source.volume = targetVolume; // 확정할거에요 -> 목표 볼륨으로
}
}