using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을 using System.Collections; // 코루틴을 사용할거에요 -> System.Collections를 using System.Collections.Generic; // 딕셔너리를 사용할거에요 -> System.Collections.Generic을 /// /// 전역 사운드 매니저 (싱글톤) /// BGM, 환경음(Ambience), SFX(효과음)를 통합 관리 /// 빈 오브젝트에 부착 후 인스펙터에서 AudioClip 연결 /// 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(); // 추가할거에요 -> BGM 채널을 _bgmSource.loop = true; // 설정할거에요 -> 반복 재생으로 _bgmSource.playOnAwake = false; // 설정할거에요 -> 자동 재생 끄기로 _bgmSource.volume = bgmVolume; // 설정할거에요 -> 볼륨을 _ambienceSource = gameObject.AddComponent(); // 추가할거에요 -> 환경음 채널을 _ambienceSource.loop = true; // 설정할거에요 -> 반복 재생으로 _ambienceSource.playOnAwake = false; // 설정할거에요 -> 자동 재생 끄기로 _ambienceSource.volume = ambienceVolume; // 설정할거에요 -> 볼륨을 _sfxSource = gameObject.AddComponent(); // 추가할거에요 -> 효과음 채널을 _sfxSource.playOnAwake = false; // 설정할거에요 -> 자동 재생 끄기로 _sfxSource.volume = sfxVolume; // 설정할거에요 -> 볼륨을 } // ============================================ // BGM 제어 // ============================================ /// /// BGM을 페이드 전환으로 변경 /// 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)); // 실행할거에요 -> 크로스페이드 전환을 } /// /// BGM 정지 (페이드 아웃) /// 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 (효과음) — 전역에서 재생 // ============================================ /// /// 이름으로 UI/전역 효과음 재생 /// public void PlaySFX(string sfxName) // 함수를 선언할거에요 -> 효과음 재생을 { AudioClip clip = GetSFXClip(sfxName); // 가져올거에요 -> 이름에 맞는 클립을 if (clip != null) _sfxSource.PlayOneShot(clip, sfxVolume); // 재생할거에요 -> 효과음을 } /// /// 외부에서 AudioClip을 직접 넘겨서 재생 /// public void PlaySFX(AudioClip clip, float volume = -1f) // 함수를 선언할거에요 -> 클립 직접 재생을 { if (clip == null) return; // 조건이 맞으면 중단할거에요 float vol = volume < 0 ? sfxVolume : volume; // 값을 결정할거에요 -> 볼륨을 _sfxSource.PlayOneShot(clip, vol); // 재생할거에요 -> 효과음을 } /// /// 3D 공간 위치에서 효과음 재생 (몬스터 사망 등) /// 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; // 확정할거에요 -> 목표 볼륨으로 } }