728x90
SMALL

SoundManager 제작

배경음과 효과음의 볼륨을 관리할 수 있도록 AudioMixer 추가였습니다.
AudioMixerBGM 그룹과 SFX 그룹 추가한 후 볼륨을 조절할 수 있도록 BgmVolume, SfxVolume 파라미터도 추가하였습니다.

 

SoundManager 스크립트를 추가하였고, SoundManager 스크립트에는 아래 기능들을 구현하였습니다.

  • 배경음 출력용 AudioSource와 효과음 출력용 AudioSource를 1개씩 추가하여 배경음과 간단한 효과음을 재생할 수 있도록 구현
  • 배경음, 효과음 볼륨을 조절할 수 있도록 구현
  • 볼륨은 PlayerPrefs에 저장하여 어플을 종료하여도 조절했던 볼륨이 초기화되지 않도록 구현
  • 도중에 재생을 멈추거나 반복 재생을 해줘야 되는 효과음들도 있기 때문에 효과음 재생용 AudioSource를 생성하는 기능 구현
public class SoundManager : MonoBehaviour
{
    [SerializeField]
    AudioMixer audioMixer;
    [SerializeField]
    AudioSource bgmSource;
    [SerializeField]
    AudioSource sfxSource;

    public const float maxVolume = 0f;
    public const float minVolume = -80f;

    public static SoundManager Instance
    {
        get;
        private set;
    }

    public float BgmVolume
    {
        get
        {
            audioMixer.GetFloat("BgmVolume", out var value);
            return value;
        }
        set
        {
            var volume = Mathf.Clamp(value, minVolume, maxVolume);
            audioMixer.SetFloat("BgmVolume", volume);
            PlayerPrefs.SetFloat("BgmVolume", volume);
        }
    }

    public float SfxVolume
    {
        get
        {
            audioMixer.GetFloat("SfxVolume", out var value);
            return value;
        }
        set
        {
            var volume = Mathf.Clamp(value, minVolume, maxVolume);
            audioMixer.SetFloat("SfxVolume", volume);
            PlayerPrefs.SetFloat("SfxVolume", volume);
        }
    }

    void Awake()
    {
        Instance = this;
        DontDestroyOnLoad(gameObject);
        audioMixer.SetFloat("BgmVolume", PlayerPrefs.GetFloat("BgmVolume", maxVolume));
        audioMixer.SetFloat("SfxVolume", PlayerPrefs.GetFloat("SfxVolume", maxVolume));
        bgmSource.loop = true;
        bgmSource.playOnAwake = false;
        sfxSource.loop = false;
        sfxSource.playOnAwake = false;
    }

    public void PlayBgm(AudioClip clip)
    {
        if (clip == null)
            return;
        if (bgmSource.isPlaying && bgmSource.clip == clip)
            return;
        if (bgmSource.clip != clip)
            bgmSource.clip = clip;
        bgmSource.Play();
    }

    public void StopBgm()
    {
        bgmSource.Stop();
    }

    public void PauseBgm()
    {
        bgmSource.Pause();
    }

    public void ResumeBgm()
    {
        bgmSource.UnPause();
    }

    public void PlaySfx(AudioClip clip)
    {
        if (clip != null)
            sfxSource.PlayOneShot(clip);
    }

    public AudioSource CreateSfxSource(string name, Transform parent = null)
    {
        var source = Instantiate(sfxSource, parent);
        source.name = name;
        return source;
    }

    public static void CreateInstance()
    {
        if (Instance != null)
            return;
        Instantiate(Resources.Load(nameof(SoundManager)));
    }
}

 

Resources 폴더에 SoundManager 프리팹을 추가한 후 SoundManager 스크립트와 연결하였습니다.

 

배경음 구현

 

로그인 씬에서는 배경음을 재생하지 않도록 LoginLogic 스크립트를 수정하였습니다.

IEnumerator Start()
{
    if (GameUI.Instance != null)
        GameUI.Instance.HideAll();
    SoundManager.CreateInstance();
    SoundManager.Instance.StopBgm();
    ClientManager.CreateInstance();
    yield return new WaitForEndOfFrame();
    Loading();
}

 

게임 씬에서는 배경음을 재생하도록 구현 GameLogic 수정하였습니다.

void Start()
{
    GameUI.CreateInstance();
    SoundManager.CreateInstance();
    SoundManager.Instance.PlayBgm(bgmClip);
    npcCarManager.Play();
    foreach (var trigger in customerTriggers)
    {
        trigger.OnPlayerEntered += (sender, args) =>
        {
            OnCarEnterTrigger(sender as CustomerTrigger);
        };
    }
    RespawnPlayerCar();
    GameUI.Instance.OnGameLoaded();
}

 

각각의 게임 맵에 배경음으로 사용할 AudioClip을 연결하였습니다.

 

UI 효과음 추가

 

버튼을 클릭하였을 때 효과음이 발생하는 기능을 간단하게 구현해보기 위해 ButtonClickSFX 스크립트를 추가하였습니다.

[RequireComponent(typeof(Button))]
public class ButtonClickSFX : MonoBehaviour
{
    [SerializeField]
    AudioClip clip;

    void Start()
    {
        GetComponent<Button>().onClick.AddListener(() =>
        {
            SoundManager.Instance.PlaySfx(clip);
        });
    }
}

 

각각의 버튼 오브젝트에 ButtonClickSFX 스크립트를 연결하였습니다.

 

ResultViewUI 게임 성공시 배경음 일시 중단하고, 다른 효과음 나오도록 수정하였습니다.

public class ResultViewUI : MonoBehaviour
{
    [SerializeField]
    TMP_Text coinText;
    [SerializeField]
    Button claimButton;
    [SerializeField]
    AudioClip goalSfx;

    void Start()
    {
        claimButton.onClick.AddListener(() =>
        {
            gameObject.SetActive(false);
            GameLogic.LoadStage(ClientManager.Instance.UserService.User.CurrentStage.Index);
        });
    }

    void OnDisable()
    {
        coinText.text = "0";
        SoundManager.Instance.ResumeBgm();
    }

    public void Show(bool isGoal)
    {
        coinText.text = GameLogic.Instance.RewardedCoin.ToString();
        gameObject.SetActive(true);
        if (isGoal)
            StartCoroutine(PlayGoalSfx());
    }

    IEnumerator PlayGoalSfx()
    {
        SoundManager.Instance.PauseBgm();
        SoundManager.Instance.PlaySfx(goalSfx);
        yield return new WaitForSeconds(goalSfx.length);
        SoundManager.Instance.ResumeBgm();
    }
}

 

택시에 타고 있던 손님이 내린 후 새로운 새로운 손님이 택시를 부르는 메시지 창이 출력될 때 효과음이 나오도록 수정하였습니다.

public class TalkViewUI : MonoBehaviour
{
    [SerializeField]
    Image customerIconImage;
    [SerializeField]
    TMP_Text talkContentText;
    [SerializeField]
    AudioClip callSfx;

    public void Show(int customerIndex, int talkIndex)
    {
        var templateService = ClientManager.Instance.TemplateService;
        var talk = templateService.Talks[talkIndex];
        if (talk.Type == TalkType.Call)
            SoundManager.Instance.PlaySfx(callSfx);
        customerIconImage.sprite = templateService.Customers[customerIndex].Icon;
        talkContentText.text = talk.Content.Key;
        gameObject.SetActive(true);
    }
}

 

자동차 효과음 추가

 

자동차의 효과음을 관리하기 위해 CarSFXController 추가하였습니다.
각각의 효과음들은 등록된 효과음 중 1개를 렌덤하게 출력할 수 있도록 각각의 효과음은 배열로 정의하였습니다.

  • brakeSfx
    • 자동차를 급제동할 때 발생하는 타이어 마찰음
    • 화면을 터치하지 않을 때 출력
    • 최저 속도까지 감소하거나 바로 멈출 경우에는 출력되지 않음
    • 소리가 중복 출력되면 안 되고, 상황에 따라 바로 재생을 멈춰야 되는 경우도 있기 때문에 AudioSource를 따로 사용
  • doorSfx
    • 문을 열고 닫을 때 출력되는 효과음
    • 손님이 차에 타고, 내릴 때 출력
  • incomeSfx
    • 동전소리 효과음
    • 손님이 차에 내릴 때, 돈을 벌었다는 느낌을 주기 위해 출력
    • 나중에 동전 이펙트를 구현하면 수정할 계획
  • crashSfx
    • 다른 자동차와 충돌할 때 발생하는 효과음
  • hornSfx
    • 자동차 경적음
    • 앞에 다른 자동차가 있을 경우 출력
public class CarSFXController : MonoBehaviour
{
    [SerializeField]
    AudioClip[] brakeSfx;
    [SerializeField]
    AudioClip[] doorSfx;
    [SerializeField]
    AudioClip[] incomeSfx;
    [SerializeField]
    AudioClip[] crashSfx;
    [SerializeField]
    AudioClip[] hornSfx;

    AudioSource brakeSfxSource;
    AudioSource hornSfxSource;

    public void PlayBrakeSfx()
    {
        if (brakeSfx.Length == 0)
            return;
        if (brakeSfxSource == null)
        {
            brakeSfxSource = SoundManager.Instance.CreateSfxSource("BrakeSFX", transform);
            brakeSfxSource.loop = true;
            brakeSfxSource.transform.localPosition = Vector3.zero;
        }
        if (brakeSfxSource.isPlaying)
            return;
        brakeSfxSource.clip = brakeSfx[Random.Range(0, brakeSfx.Length)];
        brakeSfxSource.Play();
    }

    public void StopBrakeSfx()
    {
        if (brakeSfxSource == null || !brakeSfxSource.isPlaying)            
            return;
        brakeSfxSource.Stop();
    }

    public void PlayDoorSfx()
    {
        if (doorSfx.Length == 0)
            return;
        SoundManager.Instance.PlaySfx(doorSfx[Random.Range(0, doorSfx.Length)]);
    }

    public void PlayIncomeSfx()
    {
        if (incomeSfx.Length == 0)
            return;
        SoundManager.Instance.PlaySfx(incomeSfx[Random.Range(0, incomeSfx.Length)]);
    }

    public void PlayCrashSfx()
    {
        if (crashSfx.Length == 0)
            return;
        SoundManager.Instance.PlaySfx(crashSfx[Random.Range(0, crashSfx.Length)]);
    }

    public void PlayHornSfx()
    {
        if (hornSfx.Length == 0)
            return;
        if (hornSfxSource == null)
        {
            hornSfxSource = SoundManager.Instance.CreateSfxSource("HornSFX", transform);
            hornSfxSource.transform.localPosition = Vector3.zero;
        }
        if (hornSfxSource.isPlaying)
            return;
        hornSfxSource.clip = hornSfx[Random.Range(0, hornSfx.Length)];
        hornSfxSource.Play();
    }

    public void StopHornSfx()
    {
        if (hornSfxSource == null)
            return;
        hornSfxSource.Stop();
    }
}

 

바로 앞에 다른 자동차가 지나가는지 안 지나가는지 감지하기 위해 BoxCollider 추가하였습니다.

 

앞에 지나가는 자동차가 감지되었을 때 PlayerCar 스크립트에 이벤트를 전달할 수 있도록 TriggerInvoker 스크립트를 추가하였습니다.

public class TriggerInvoker : MonoBehaviour
{
    public event EventHandler<Collider> TriggerEnteredEvent;
    public event EventHandler<Collider> TriggerStayingEvent;
    public event EventHandler<Collider> TriggerExitedEvent;

    public void OnTriggerEnter(Collider other) =>
        TriggerEnteredEvent?.Invoke(this, other);

    public void OnTriggerStay(Collider other) =>
        TriggerStayingEvent?.Invoke(this, other);

    public void OnTriggerExit(Collider other) =>
        TriggerExitedEvent?.Invoke(this, other);
}

 

PlayerCar 스크립트에서 필요한 상황에 맞춰 효과음을 출력하도록 수정하였습니다.

public class PlayerCar : MonoBehaviour
{
    [Header("Movement")]
    [SerializeField]
    float acceleration = 1f;
    [SerializeField]
    float maxSpeed = 5f;
    [SerializeField]
    float brakeForce = 1f;
    VertexPath path;
    const float minSpeed = 0.5f;
    float speed = minSpeed;
    Rigidbody rb;
    CarSFXController sfxController;

    public bool IsEnableMoving
    {
        get;
        set;
    }
    public float Movement
    {
        get;
        private set;
    }
    public bool IsArrive => Movement >= path.length;
    [field: Header("Points")]
    [field: SerializeField]
    public Transform LeftPoint
    {
        get;
        private set;
    }
    [field: SerializeField]
    public Transform RightPoint
    {
        get;
        private set;
    }

    public event EventHandler OnCrashed;
    public event EventHandler OnArrive;

    void Awake()
    {
        rb = GetComponent<Rigidbody>();
        sfxController = GetComponent<CarSFXController>();
    }

    void Start()
    {
        GameLogic.Instance.CustomerTakeInEvent += (sender, numOfCustomers) =>
        {
            sfxController.PlayDoorSfx();
        };
        GameLogic.Instance.CustomerTakeOutEvent += (sender, numOfCustomers) =>
        {
            sfxController.PlayDoorSfx();
            sfxController.PlayIncomeSfx();
        };
        GetComponentInChildren<TriggerInvoker>().TriggerEnteredEvent += (sender, other) =>
        {
            if (other.gameObject.CompareTag("NpcCar"))
                sfxController.PlayHornSfx();
        };
    }

    void Update()
    {
        if (!IsEnableMoving)
            return;
        Movement += Time.deltaTime * speed;
        rb.MovePosition(path.GetPointAtDistance(Movement, EndOfPathInstruction.Stop));
        rb.MoveRotation(path.GetRotationAtDistance(Movement, EndOfPathInstruction.Stop));
        if (IsArrive)
            OnArrive?.Invoke(this, EventArgs.Empty);
    }

    void OnCollisionEnter(Collision collision)
    {
        if (collision.gameObject.CompareTag("NpcCar"))
        {
            sfxController.PlayCrashSfx();
            OnCrashed?.Invoke(this, EventArgs.Empty);
        }
    }

    public void SetPath(VertexPath path)
    {
        this.path = path;
        Movement = 0f;
        rb.position = path.GetPoint(0);
        rb.rotation = path.GetRotation(0f, EndOfPathInstruction.Stop);
    }

    public void PlayMoving()
    {
        IsEnableMoving = true;
        speed = minSpeed;
    }

    public void StopMoving()
    {
        IsEnableMoving = false;
        speed = 0f;
        sfxController.StopBrakeSfx();
    }

    public void PressAccel()
    {
        if (IsEnableMoving)
        {
            speed = Mathf.Min(speed + Time.deltaTime * acceleration, maxSpeed);
            sfxController.StopBrakeSfx();
        }
    }

    public void PressBrake()
    {
        if (IsEnableMoving)
        {
            speed = Mathf.Max(speed - Time.deltaTime * brakeForce, minSpeed);
            if (speed > minSpeed)
                sfxController.PlayBrakeSfx();
            else
                sfxController.StopBrakeSfx();
        }
    }

    public Transform SelectNearestPoint(Vector3 poisition)
    {
        var left = (LeftPoint.position - poisition).sqrMagnitude;
        var right = (RightPoint.position - poisition).sqrMagnitude;
        return left < right ? LeftPoint : RightPoint;
    }
}

 

설정창 구현

 

UGUI를 이용하여 설정창을 제작하였습니다.

 


UGUI에서 기본 제공하는 Toggle는 ✔️ 아이콘을 보여줬다, 감췄다하는 방식으로 구현되어 있기 때문에 간단하게 SimpleToggle를 구현하였습니다.

public class SimpleToggle : MonoBehaviour, IPointerClickHandler
{
    [SerializeField]
    Image target;
    [SerializeField]
    Sprite on;
    [SerializeField]
    Sprite off;
    [SerializeField]
    AudioClip clickSfx;

    bool value;

    public bool Value
    {
        get => value;
        set => SetValue(value, true);
    }

    public event EventHandler<bool> ValueChangedEvent;

    void OnEnable()
    {
        target.sprite = value ? on : off;
    }

    public void OnPointerClick(PointerEventData eventData)
    {
        SoundManager.Instance.PlaySfx(clickSfx);
        Value = !Value;
    }

    public void SetValue(bool value, bool callEvent)
    {
        this.value = value;
        target.sprite = value ? on : off;
        if (Application.isPlaying)
        {
            if (callEvent)
                ValueChangedEvent.Invoke(this, value);
        }
    }
}

 

SettingsViewUI 스크립트를 추가하여 배경음 on/off, 효과음 on/off, 로그아웃을 구현하였습니다.

public class SettingsViewUI : MonoBehaviour
{
    [SerializeField]
    SimpleToggle bgmToggle;
    [SerializeField]
    SimpleToggle sfxToggle;
    [SerializeField]
    Button logoutButton;
    [SerializeField]
    Button closeButton;

    void Start()
    {
        bgmToggle.ValueChangedEvent += (sender, value) =>
        {
            SoundManager.Instance.BgmVolume = value ? SoundManager.maxVolume : SoundManager.minVolume;
        };
        sfxToggle.ValueChangedEvent += (sender, value) =>
        {
            SoundManager.Instance.SfxVolume = value ? SoundManager.maxVolume : SoundManager.minVolume;
        };
        logoutButton.onClick.AddListener(() =>
        {
            ClientManager.Instance.AuthService.Logout();
            GameUI.Instance.HideAll();
            SceneManager.LoadScene(0);
        });
        closeButton.onClick.AddListener(() =>
        {
            gameObject.SetActive(false);
        });
    }

    void OnEnable()
    {
        bgmToggle.SetValue(SoundManager.Instance.BgmVolume > SoundManager.minVolume, false);
        sfxToggle.SetValue(SoundManager.Instance.SfxVolume > SoundManager.minVolume, false);
    }
}

 

 

다음에는 자동차에 제동등, 타이어 자국 등의 이펙트를 추가하는 작업을 하겠습니다.

 

깃 허브 저장소 : taxi-game-3d-unity

728x90
LIST

+ Recent posts