255 lines
13 KiB
C#
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; // 확정할거에요 -> 목표 볼륨으로
|
||
|
|
}
|
||
|
|
}
|