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 따로 만들어서 관리하니까, 나중에 총 소리, 재장전 소리, 버튼 클릭 등 재사용 편할 듯.
  • 다음에는 충돌 감지를 이용해서 탄피가 땅에 닿을 때 사운드 출력하는 방식으로 개선해보고 싶음.