SoundManager 제작
배경음과 효과음의 볼륨을 관리할 수 있도록 AudioMixer 추가였습니다.
AudioMixer에 BGM 그룹과 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
'개발노트 > Taxi Game 3D' 카테고리의 다른 글
Devlog) Taxi Game 3D) 24) Localization 적용 (0) | 2024.02.16 |
---|---|
Devlog) Taxi Game 3D) 23) 이펙트 추가 (0) | 2024.02.14 |
Devlog) Taxi Game 3D) 21) 자동차 목록창 수정, 보상획득창 구현 (0) | 2024.02.02 |
Devlog) Taxi Game 3D) 20) 룰렛, 빨간점 구현 (2) | 2024.01.26 |
Devlog) Taxi Game 3D) 19) 출석보상, 재화 수집 구현 (0) | 2024.01.23 |