using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을 using System.Collections; // 코루틴을 사용할거에요 -> System.Collections를 // ============================================================ // AudioManager // // 역할: BGM 전환을 크로스페이드로 처리하는 싱글톤 매니저 // // 사용법: // AudioManager.Instance.PlayBGM(clip); // 즉시 전환 // AudioManager.Instance.PlayBGM(clip, 1.5f); // 1.5초 페이드 // AudioManager.Instance.StopBGM(); // 정지 // AudioManager.Instance.StopBGM(1.0f); // 1초 페이드아웃 // // 구조: // AudioSource A ─┐ // ├─ 크로스페이드 (한쪽 올리고 한쪽 내리기) // AudioSource B ─┘ // ============================================================ public class AudioManager : MonoBehaviour // 클래스를 선언할거에요 -> AudioManager를 { // ───────────────────────────────────────────────────────── // 싱글톤 // ───────────────────────────────────────────────────────── public static AudioManager Instance { get; private set; } // 프로퍼티를 선언할거에요 -> 싱글톤 인스턴스를 // ───────────────────────────────────────────────────────── // Inspector 설정 // ───────────────────────────────────────────────────────── [Header("=== BGM 설정 ===")] [Tooltip("BGM 기본 볼륨 (0~1)")] [SerializeField] [Range(0f, 1f)] private float bgmVolume = 0.5f; // 변수를 선언할거에요 -> BGM 볼륨을 [Tooltip("기본 페이드 시간 (초)")] [SerializeField] private float defaultFadeDuration = 1.0f; // 변수를 선언할거에요 -> 기본 페이드 시간을 [Tooltip("씬 시작 시 재생할 기본 BGM (없으면 무음)")] [SerializeField] private AudioClip defaultBGM; // 변수를 선언할거에요 -> 시작 BGM을 // ───────────────────────────────────────────────────────── // 내부 변수 // ───────────────────────────────────────────────────────── private AudioSource _sourceA; // 변수를 선언할거에요 -> BGM 채널 A를 private AudioSource _sourceB; // 변수를 선언할거에요 -> BGM 채널 B를 private bool _isAActive = true; // 변수를 선언할거에요 -> 현재 활성 채널이 A인지를 private Coroutine _fadeCoroutine; // 변수를 선언할거에요 -> 진행 중인 페이드 코루틴 핸들을 private AudioClip _currentClip; // 변수를 선언할거에요 -> 현재 재생 중인 클립을 // ───────────────────────────────────────────────────────── // 초기화 // ───────────────────────────────────────────────────────── private void Awake() // 함수를 실행할거에요 -> 초기화를 { // 싱글톤 중복 방지 if (Instance != null && Instance != this) // 조건이 맞으면 실행할거에요 -> 이미 인스턴스가 있으면 { Destroy(gameObject); // 제거할거에요 -> 중복 오브젝트를 return; // 중단할거에요 } Instance = this; // 설정할거에요 -> 싱글톤 인스턴스를 DontDestroyOnLoad(gameObject); // 유지할거에요 -> 씬 전환해도 파괴되지 않게 // BGM 채널 A 생성 _sourceA = gameObject.AddComponent(); // 추가할거에요 -> AudioSource A를 _sourceA.loop = true; // 설정할거에요 -> 루프 재생으로 _sourceA.playOnAwake = false; // 끌거에요 -> 자동 재생을 _sourceA.volume = 0f; // 설정할거에요 -> 초기 볼륨을 0으로 // BGM 채널 B 생성 _sourceB = gameObject.AddComponent(); // 추가할거에요 -> AudioSource B를 _sourceB.loop = true; // 설정할거에요 -> 루프 재생으로 _sourceB.playOnAwake = false; // 끌거에요 -> 자동 재생을 _sourceB.volume = 0f; // 설정할거에요 -> 초기 볼륨을 0으로 } private void Start() // 함수를 실행할거에요 -> 시작 시 { if (defaultBGM != null) // 조건이 맞으면 실행할거에요 -> 기본 BGM이 있으면 PlayBGM(defaultBGM, defaultFadeDuration); // 재생할거에요 -> 기본 BGM을 페이드로 } // ───────────────────────────────────────────────────────── // 공개 API // ───────────────────────────────────────────────────────── /// BGM 전환 (페이드 시간 생략 시 기본값 사용) public void PlayBGM(AudioClip clip, float fadeDuration = -1f) // 함수를 선언할거에요 -> BGM을 재생하는 PlayBGM을 { if (clip == null) return; // 중단할거에요 -> 클립 없으면 // 이미 같은 클립이 재생 중이면 무시 if (_currentClip == clip) return; // 중단할거에요 -> 동일 클립이면 float duration = fadeDuration < 0f ? defaultFadeDuration : fadeDuration; // 결정할거에요 -> 페이드 시간을 _currentClip = clip; // 저장할거에요 -> 현재 클립을 // 진행 중인 페이드 취소 if (_fadeCoroutine != null) // 조건이 맞으면 실행할거에요 -> 페이드 중이면 { StopCoroutine(_fadeCoroutine); // 취소할거에요 -> 이전 페이드를 _fadeCoroutine = null; // 초기화할거에요 -> 핸들을 } _fadeCoroutine = StartCoroutine(CrossFadeRoutine(clip, duration)); // 시작할거에요 -> 크로스페이드 코루틴을 } /// BGM 정지 public void StopBGM(float fadeDuration = -1f) // 함수를 선언할거에요 -> BGM을 정지하는 StopBGM을 { float duration = fadeDuration < 0f ? defaultFadeDuration : fadeDuration; // 결정할거에요 -> 페이드 시간을 _currentClip = null; // 초기화할거에요 -> 현재 클립을 if (_fadeCoroutine != null) StopCoroutine(_fadeCoroutine); // 취소할거에요 -> 이전 페이드를 _fadeCoroutine = StartCoroutine(FadeOutRoutine(duration)); // 시작할거에요 -> 페이드아웃 코루틴을 } /// BGM 볼륨 변경 public void SetVolume(float volume) // 함수를 선언할거에요 -> 볼륨을 변경하는 SetVolume을 { bgmVolume = Mathf.Clamp01(volume); // 저장할거에요 -> 0~1로 제한한 볼륨을 ActiveSource.volume = bgmVolume; // 적용할거에요 -> 현재 활성 채널에 } // ───────────────────────────────────────────────────────── // 크로스페이드 코루틴 // ───────────────────────────────────────────────────────── private IEnumerator CrossFadeRoutine(AudioClip newClip, float duration) // 코루틴을 정의할거에요 -> 크로스페이드를 { AudioSource incoming = InactiveSource; // 가져올거에요 -> 새 클립 재생할 채널을 (현재 비활성 채널) AudioSource outgoing = ActiveSource; // 가져올거에요 -> 페이드아웃할 채널을 (현재 활성 채널) // 새 채널 준비 incoming.clip = newClip; // 설정할거에요 -> 새 클립을 incoming.volume = 0f; // 설정할거에요 -> 볼륨을 0으로 incoming.Play(); // 재생할거에요 -> 새 BGM을 _isAActive = !_isAActive; // 전환할거에요 -> 활성 채널을 float elapsed = 0f; // 초기화할거에요 -> 경과 시간을 float outgoingStart = outgoing.volume; // 저장할거에요 -> 기존 채널 시작 볼륨을 while (elapsed < duration) // 반복할거에요 -> 페이드 시간 동안 { elapsed += Time.deltaTime; // 더할거에요 -> 경과 시간을 float t = Mathf.Clamp01(elapsed / duration); // 계산할거에요 -> 진행률을 (0~1) incoming.volume = Mathf.Lerp(0f, bgmVolume, t); // 올릴거에요 -> 새 채널 볼륨을 outgoing.volume = Mathf.Lerp(outgoingStart, 0f, t); // 내릴거에요 -> 기존 채널 볼륨을 yield return null; // 대기할거에요 -> 다음 프레임까지 } incoming.volume = bgmVolume; // 고정할거에요 -> 최종 볼륨을 outgoing.volume = 0f; // 고정할거에요 -> 기존 채널을 0으로 outgoing.Stop(); // 정지할거에요 -> 기존 채널을 outgoing.clip = null; // 초기화할거에요 -> 기존 클립을 _fadeCoroutine = null; // 초기화할거에요 -> 핸들을 } private IEnumerator FadeOutRoutine(float duration) // 코루틴을 정의할거에요 -> 페이드아웃을 { AudioSource active = ActiveSource; // 가져올거에요 -> 활성 채널을 float startVolume = active.volume; // 저장할거에요 -> 시작 볼륨을 float elapsed = 0f; // 초기화할거에요 -> 경과 시간을 while (elapsed < duration) // 반복할거에요 -> 페이드 시간 동안 { elapsed += Time.deltaTime; // 더할거에요 -> 경과 시간을 active.volume = Mathf.Lerp(startVolume, 0f, elapsed / duration); // 내릴거에요 -> 볼륨을 yield return null; // 대기할거에요 -> 다음 프레임까지 } active.volume = 0f; // 고정할거에요 -> 0으로 active.Stop(); // 정지할거에요 -> 채널을 active.clip = null; // 초기화할거에요 -> 클립을 _fadeCoroutine = null; // 초기화할거에요 -> 핸들을 } // ───────────────────────────────────────────────────────── // 내부 유틸 // ───────────────────────────────────────────────────────── private AudioSource ActiveSource => _isAActive ? _sourceA : _sourceB; // 프로퍼티를 선언할거에요 -> 현재 활성 채널을 private AudioSource InactiveSource => _isAActive ? _sourceB : _sourceA; // 프로퍼티를 선언할거에요 -> 현재 비활성 채널을 }