Projext/Assets/Scripts/Player/Sound/AudioManager.cs
2026-02-27 09:44:52 +09:00

195 lines
11 KiB
C#

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>(); // 추가할거에요 -> AudioSource A를
_sourceA.loop = true; // 설정할거에요 -> 루프 재생으로
_sourceA.playOnAwake = false; // 끌거에요 -> 자동 재생을
_sourceA.volume = 0f; // 설정할거에요 -> 초기 볼륨을 0으로
// BGM 채널 B 생성
_sourceB = gameObject.AddComponent<AudioSource>(); // 추가할거에요 -> AudioSource B를
_sourceB.loop = true; // 설정할거에요 -> 루프 재생으로
_sourceB.playOnAwake = false; // 끌거에요 -> 자동 재생을
_sourceB.volume = 0f; // 설정할거에요 -> 초기 볼륨을 0으로
}
private void Start() // 함수를 실행할거에요 -> 시작 시
{
if (defaultBGM != null) // 조건이 맞으면 실행할거에요 -> 기본 BGM이 있으면
PlayBGM(defaultBGM, defaultFadeDuration); // 재생할거에요 -> 기본 BGM을 페이드로
}
// ─────────────────────────────────────────────────────────
// 공개 API
// ─────────────────────────────────────────────────────────
/// <summary> BGM 전환 (페이드 시간 생략 시 기본값 사용) </summary>
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)); // 시작할거에요 -> 크로스페이드 코루틴을
}
/// <summary> BGM 정지 </summary>
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)); // 시작할거에요 -> 페이드아웃 코루틴을
}
/// <summary> BGM 볼륨 변경 </summary>
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; // 프로퍼티를 선언할거에요 -> 현재 비활성 채널을
}