Unity 최종
Unity SoundManager 구조와 활용
1vdlrwnsv1
2025. 4. 8. 20:35
오늘 한 일
- SoundManager 클래스를 만들어 탄피가 땅에 떨어질 때 사운드 재생 구현
- WeaponStatHandler와 SoundManager를 연동해 타이밍 맞춰 사운드 출력
- AudioSource.PlayOneShot() 사용
→ 사운드를 중첩해서 재생할 수 있음. 반복되거나 끊기는 문제 없이 효과음에 적합. - FindObjectOfType<SoundManager>() 사용
→ 씬 내에 있는 SoundManager를 참조해서 쉽게 사운드 제어 가능 (단, 퍼포먼스 측면에서 자주 쓰이는 건 비추. 싱글톤 구조 추천)
2. SoundManager 구조 및 작동 방식
목적:
- 배경음악(BGM) 및 효과음(SFX) 재생을 중앙에서 관리하는 싱글톤(Singleton) 매니저.
구성요소:
- musicSource, sfxSource: 각각 BGM과 효과음을 재생하는 AudioSource 컴포넌트.
- backgroundMusicVol, sfxVol: 볼륨 조절용 float 변수.
- PlayBackgroundMusic(AudioClip): BGM 재생.
- PlaySFX(string soundName): Resources/Audio/SFX/ 경로에서 효과음 불러와 재생.
- AddSoundEffect(string, AudioClip): 사운드를 직접 추가 가능.
- SetMusicVolume, SetSFXVolume: 볼륨 조절 함수.
using System;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 예시
/// 배경음악: SoundManager.Instance.PlayBackgroundMusic("사운드 이름");
/// 효과음: SoundManager.Instance.PlaySFX("사운드 이름");
/// </summary>
public class SoundManager : MonoBehaviour
{
public static SoundManager Instance;
private AudioSource musicSource;
private AudioSource sfxSource;
[Header("Settings")]
[Tooltip("배경음악 볼륨")]
[Range(0f, 1f)]
public float backgroundMusicVol = 0.5f;
[Tooltip("효과음 볼륨")]
[Range(0f, 1f)]
public float sfxVol = 0.5f;
[Header("오디오 클립")]
public AudioClip backgroundMusic;
public Dictionary<String, AudioClip> soundEffects = new Dictionary<string, AudioClip>();
private void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
// Ensure the music and sfx sources are assigned
if (musicSource == null) musicSource = GetComponent<AudioSource>(); // 음악 소스
if (sfxSource == null) sfxSource = gameObject.AddComponent<AudioSource>(); // 효과음 소스
musicSource.volume = backgroundMusicVol;
sfxSource.volume = sfxVol;
musicSource.loop = true;
}
void Start()
{
PlayBackgroundMusic(backgroundMusic);
}
#region 배경음악 관련
void PlayBackgroundMusic(AudioClip music)
{
if (music != null)
{
musicSource.clip = music;
musicSource.Play();
}
}
public void PauseBackgroundMusic()
{
//ToDo 음악멈추기
}
public void ResumeBackgroundMusic()
{
//ToDo 음악다시켜기
}
#endregion
#region 효과음 관련
/// <summary>
/// 사운드 넣고 싶은거 넣으세요 리소스 폴더안에 SFX 경로에 넣어야합니다.
/// </summary>
/// <param name="string"></param>
/// <param name="AudioClip"></param>
public void AddSoundEffect(string soundName, AudioClip clip)
{
if (!soundEffects.ContainsKey(soundName))
{
soundEffects.Add(soundName, clip);
}
}
/// <summary>
/// 사운드 효과 재생
/// </summary>
/// <param name="string"></param>
public void PlaySFX(string soundName)
{
AudioClip clip = Resources.Load<AudioClip>("Audio/SFX/" + soundName);
if (clip != null)
{
sfxSource.PlayOneShot(clip);
}
else
{
Debug.LogWarning("Sound not found in Resources/Audio/SfX: " + soundName);
}
}
#endregion
#region 볼륨
/// <summary>
/// 효과음 볼륨
/// </summary>
/// <param name="float"></param>
public void SetSFXVolume(float volume)
{
sfxVol = Mathf.Clamp(volume, 0f, 1f);
sfxSource.volume = sfxVol;
}
/// <summary>
/// 배경음악 볼륨
/// </summary>
/// <param name="volume"></param>
public void SetMusicVolume(float volume)
{
backgroundMusicVol = Mathf.Clamp(volume, 0f, 1f);
musicSource.volume = backgroundMusicVol;
}
#endregion
}
using System.Collections;
using JetBrains.Annotations;
using UnityEngine;
public class WeaponStatHandler : MonoBehaviour
{
public WeaponData weaponData;
public Animator gunAnimator;
public GameObject casingPrefab;
public GameObject muzzleFlashPrefab;
public GameObject bulletImpactPrefab;
[SerializeField] private Transform barrelLocation;
[SerializeField] private Transform casingExitLocation;
[Header("Settings")]
[Tooltip("Specify time to destroy the casing object")]
[SerializeField] private float destroyTimer = 2f;
[Tooltip("Casing Ejection Speed")]
[SerializeField] private float ejectPower = 150f;
private float fireCooldown = 0.7f;
private float lastFireTime = 0f;
void Start()
{
LoadWeaponData();
if (barrelLocation == null)
barrelLocation = transform;
if (gunAnimator == null)
gunAnimator = GetComponentInChildren<Animator>();
}
void Update()
{
if (Input.GetButtonDown("Fire1") && Time.time - lastFireTime >= fireCooldown)
{
FireWeapon();
}
if (Input.GetKeyDown(KeyCode.R))
{
ReloadWeapon();
}
}
public void FireWeapon()
{
if (weaponData != null)
{
if (weaponData.currentAmmo > 0)
{
if (gunAnimator != null && weaponData.currentAmmo != 1)
{
gunAnimator.SetTrigger("Fire");
}
if (weaponData.currentAmmo == 1)
{
gunAnimator.SetBool("OutOfAmmo", true);
}
// 레이 발사
ShootRay();
CasingRelease();
MuzzleFlash();
SoundManager.Instance.PlaySFX("M1911Fire");
weaponData.currentAmmo--;
lastFireTime = Time.time;
}
}
}
public void ShootRay()
{
Ray ray = new Ray(barrelLocation.position, barrelLocation.forward);
RaycastHit hit;
Debug.DrawRay(ray.origin, ray.direction * 100f, Color.red, 0f);
if (Physics.Raycast(ray, out hit))
{
Debug.Log("Hit: " + hit.collider.name);
if (bulletImpactPrefab)
{
//ToDO: 오브젝트 풀링으로 관리
Quaternion hitRotation = Quaternion.LookRotation(hit.normal);
GameObject impact = Instantiate(bulletImpactPrefab, hit.point, hitRotation);
Destroy(impact, 2f);
}
}
}
// 탄피 배출 처리
void CasingRelease()
{
//ToDO: 오브젝트 풀링으로 관리
if (casingExitLocation && casingPrefab)
{
GameObject tempCasing = Instantiate(casingPrefab, casingExitLocation.position, casingExitLocation.rotation);
Rigidbody casingRb = tempCasing.GetComponent<Rigidbody>();
if (casingRb != null)
{
casingRb.AddExplosionForce(Random.Range(ejectPower * 0.7f, ejectPower),
(casingExitLocation.position - casingExitLocation.right * 0.3f - casingExitLocation.up * 0.6f), 1f);
casingRb.AddTorque(new Vector3(0, Random.Range(100f, 500f), Random.Range(100f, 1000f)), ForceMode.Impulse);
}
SoundManager.Instance.PlaySFX("Shell");
Destroy(tempCasing, destroyTimer);
}
}
// 뮤즐 플래시 처리
void MuzzleFlash()
{
//ToDO: 오브젝트 풀링으로 관리
if (muzzleFlashPrefab)
{
GameObject tempFlash = Instantiate(muzzleFlashPrefab, barrelLocation.position, barrelLocation.rotation);
Destroy(tempFlash, destroyTimer);
}
}
// WeaponData 로드
void LoadWeaponData()
{
TextAsset jsonData = Resources.Load<TextAsset>("Data/JSON/PistolData");
if (jsonData != null)
{
weaponData = JsonUtility.FromJson<WeaponData>(jsonData.text);
}
}
void ReloadWeapon()
{
if (weaponData != null)
{
weaponData.currentAmmo = 0;
gunAnimator.SetBool("OutOfAmmo", true);
SoundManager.Instance.PlaySFX("Reload");
StartCoroutine(WaitForEndOfReload());
}
}
private IEnumerator WaitForEndOfReload()
{
yield return new WaitForSeconds(1.6f);
gunAnimator.SetBool("OutOfAmmo", false);
weaponData.currentAmmo = weaponData.maxAmmo;
}
}
- 탄피 떨어질 때 소리 추가하니까 총기 느낌 확 살아남.
- SoundManager 따로 만들어서 관리하니까, 나중에 총 소리, 재장전 소리, 버튼 클릭 등 재사용 편할 듯.
- 다음에는 충돌 감지를 이용해서 탄피가 땅에 닿을 때 사운드 출력하는 방식으로 개선해보고 싶음.