0. 들어가기에 앞서
주말 개인 일정이 있다 보니 작업할 시간이 많지 않았다. 특히 버그 수정 위주로 작업을 진행했다 보니 생각보다 성과물이 별로 없었다. 다만 조바심을 내지 말자
백로그로 남길 만한 내용이 많지는 않았다. 다만 크게 아래와 같은 작업을 하였다.
오늘은 오디오 설정에 대한 문제와 버그 문제 해결에 중점적으로 작업했다.
항상 발생하는 것도 아니고 간혹 스펠 데미지가 두 번 들어가는 것으로 추정되는 버그가 발생했었다. 처음에는 잘 인지하지 못했는데 간혹 스펠 데미지에 몬스터가 한 방에 죽는 경우가 발생하여 이 문제에 대해 의심했다.
(원래 스펠을 두 번 던져야지 몬스터가 죽게끔 수치를 설정해놨는데, 한 방에 죽는 경우가 발생함)
이 부분을 해결하기 위해 스펠 쪽 데미지 계산 방식에 무언가 문제가 있는지 살펴보았다.
using System.Collections;
using DesignPattern;
using UnityEngine;
public class Spell : PooledObject
{
private Animator m_animator;
private Rigidbody2D m_rigid;
[SerializeField] private float m_spellSpeed;
private void Awake()
{
m_animator = GetComponent<Animator>();
m_rigid = GetComponent<Rigidbody2D>();
}
private void OnCollisionEnter2D(Collision2D collision)
{
m_rigid.velocity = Vector2.zero;
m_animator.SetTrigger("Burst");
if (collision.gameObject.CompareTag("Enemy"))
{
collision.gameObject.GetComponent<IDamageable>().TakeDamage(1);
}
StartCoroutine(DestroyTerm());
}
IEnumerator DestroyTerm()
{
yield return new WaitForSeconds(0.5f);
ReturnPool();
}
}
처음엔 코드를 이렇게 작성했었다. 여기서 문제가 될 만한 부분은 마지막의 코루틴 부분인데, 이 부분을 설정한 이유는 시간을 두고 풀로 돌아가지 않게 하면 마지막의 터지는 애니메이션이 출력되지 않는 오류가 있었기 때문이었다.
하지만 이와 같이 설정하면 문제가 발생할 수도 있다는 것을 인지하고 있기는 했다.
이렇게 작동시켰다간 코루틴을 할당해제하지 않고 계속 중첩해나가는 방식이기 때문이었다.
물론 이 방식으로 왜 데미지가 두 번 들어가는 버그가 생기는지는 알 수가 없었지만? 코드에 문제가 있는 부분을 고칠 필요가 있어 보였다.
생각보다 이 부분이 쉽지 않았던 것이, 할당 해제와 할당을 다시 하는 과정을 해야 하는 건 맞는데, 순서를 이렇게 하면 애니메이션이 출력이 안 되고, 순서를 또 바꾸면 스펠이 풀로 안 돌아가는 현상이 생기기도 했다.
여러모로 코드를 수정하고 테스트해본 결과, 버그가 더 이상 발생하지 않는 코드는 아래와 같이 수정했을 때였다.
using System.Collections;
using DesignPattern;
using UnityEngine;
public class Spell : PooledObject
{
private Animator m_animator;
private Rigidbody2D m_rigid;
private Coroutine m_coroutine;
[SerializeField] private float m_spellSpeed;
private void Awake()
{
m_animator = GetComponent<Animator>();
m_rigid = GetComponent<Rigidbody2D>();
}
private void OnCollisionEnter2D(Collision2D collision)
{
m_rigid.velocity = Vector2.zero;
m_animator.SetTrigger("Burst");
if (collision.gameObject.CompareTag("Enemy"))
{
collision.gameObject.GetComponent<IDamageable>().TakeDamage(1);
}
if (m_coroutine != null)
{
StopCoroutine(m_coroutine);
m_coroutine = null;
}
if (m_coroutine == null)
{
m_coroutine = StartCoroutine(DestroyTerm());
}
}
IEnumerator DestroyTerm()
{
yield return new WaitForSeconds(0.5f);
ReturnPool();
}
}
이와 같이 하면 코루틴 중첩으로 인한 최적화 문제 및 버그를 해결하면서 동시에 오브젝트 풀도 정상 작동할 수 있게 하는 구조로 만들 수 있다.
AudioManager도 만들 예정이었지만, SFXController를 따로 만든 이유는 캐릭터가 내는 효과음은 캐릭터가 가지고 있어야 하고, 몬스터가 내는 효과음은 몬스터가 가지고 있어야 한다는 생각 때문이었다. 그래서 SFXController로 캐릭터에게 적용해서 효과음이 정상적으로 나는 것을 확인했고, 이걸 그대로 몬스터에게도 적용하면 되겠다는 생각을 했다.
여기서 유의해야 할 것은 코드 구조상 이전 효과음과 다른 효과음이 재생될 때 바로 이전 재생이 멈춘다는 것이다.
using UnityEngine;
[System.Serializable]
public class Sound
{
public string Name;
public AudioClip Clip;
}
public class SFXController : MonoBehaviour
{
[SerializeField] private Sound[] m_sfx = null;
private AudioSource m_sfxPlayer;
private void Awake() => Init();
private void Init()
{
m_sfxPlayer = GetComponent<AudioSource>();
}
public void PlaySFX(string p_sfxName)
{
m_sfxPlayer.loop = false;
for (int i = 0; i < m_sfx.Length; i++)
{
if (p_sfxName == m_sfx[i].Name)
{
m_sfxPlayer.clip = m_sfx[i].Clip;
m_sfxPlayer.Play();
return;
}
}
}
public void LoopSFX(string p_sfxName)
{
m_sfxPlayer.loop = true;
for (int i = 0; i < m_sfx.Length; i++)
{
if (p_sfxName == m_sfx[i].Name)
{
m_sfxPlayer.clip = m_sfx[i].Clip;
m_sfxPlayer.Play();
return;
}
}
}
public void StopSFX()
{
m_sfxPlayer.Stop();
}
}
이것 때문에 슬라임이 공격할 때 사운드가 제대로 호출되지 않는 문제가 발생했다.
(회오리 공격을 하는데, Loop로 돌린 사운드가 제대로 출력되지 않거나 한 박자 늦게 출력되는 등의 문제)
특히나 슬라임 에셋도, 사운드 에셋도 딱 맞지가 않다 보니 의도대로 소리가 출력되지 않아 슬라임 공격에 대해서는 출력하는 것을 보류하기로 하고, 대신 몬스터가 피격되었을 때 데미지를 받는 사운드를 추가하기로 했다.
실제로 공격 효과음을 넣는 것보다 이게 훨씬 자연스러웠고, 불필요하게 공격 사운드를 넣지 않는 방식으로 이번에는 구현해 보기로 했다.
오디오 매니저를 만들어 본 적이 없다 보니, 우선은 유튜브 강좌 - 골드메탈 영상의 오디오매니저 만들기 방법을 참고하였다.
using DesignPattern;
using UnityEngine;
public class AudioManager : Singleton<AudioManager>
{
[Header("BGM")]
public AudioClip BgmClip;
public float BgmVolume;
private AudioSource m_bgmPlayer;
[Header("SFX")]
public AudioClip[] SfxClips;
public float SfxVolume;
public int channels;
AudioSource[] m_sfxPlayers;
int channelIndex;
public enum Sfx { };
private void Awake() => Init();
private void Init()
{
GameObject bgmObject = new GameObject("BgmPlayer");
bgmObject.transform.parent = transform;
m_bgmPlayer = bgmObject.AddComponent<AudioSource>();
m_bgmPlayer.playOnAwake = false;
m_bgmPlayer.loop = true;
m_bgmPlayer.volume = BgmVolume;
m_bgmPlayer.clip = BgmClip;
GameObject sfxObject = new GameObject("SfxObject");
sfxObject.transform.parent = transform;
m_sfxPlayers = new AudioSource[channels];
for(int index = 0; index < m_sfxPlayers.Length; index++)
{
m_sfxPlayers[index] = sfxObject.AddComponent<AudioSource>();
m_sfxPlayers[index].playOnAwake = false;
m_sfxPlayers[index].volume = SfxVolume;
}
}
public void PlayBgm(bool isPlay)
{
if (isPlay) m_bgmPlayer.Play();
else m_bgmPlayer.Stop();
}
public void PlaySfx(Sfx sfx)
{
for (int index = 0; index < m_sfxPlayers.Length; index++)
{
int loopIndex = (index + channelIndex) % m_sfxPlayers.Length;
if (m_sfxPlayers[loopIndex].isPlaying)
continue;
int ranIndex = 0;
/*
if (sfx == Sfx.Hit || sfx == Sfx.Melee)
{
ranIndex = Random.Range(0, 2);
}
*/
channelIndex = loopIndex;
m_sfxPlayers[loopIndex].clip = SfxClips[(int)sfx];
m_sfxPlayers[loopIndex].Play();
break;
}
}
}
이와 같이 만들어 보고 나니 오디오 매니저가 필요한 것이 무엇인지 감을 잡을 수 있게 되었다.
이런 방법으로 오디오 소스를 이용한다는 사실을 알게 되었으며 시간이 남으면 이 구조를 좀 더 효율적으로 만들 수 있을지 고민해보고자 한다.
(어제자와 변하지 않은 내용이나 하지 못한 일에 대해 메모용으로 남겨둔다)
리팩토링과 구조 개선 등이 필요한 코드가 많았다.
다만 우선순위로 둘 작업은 아니며, 시간이 남을 경우에 해당 문제에 대해 고민해보자.
옵저버 패턴에 대해서도 공부를 해 봤지만, 아무래도 다른 것보다 내가 잘 다루지 못하는 취약점이 있었다.
이 부분에 대해서는 공부하고 조언을 얻을 필요가 있을 것 같다.
타이틀 씬, 일시정지 UI, 게임오버 UI 등 구현해야 할 기능이 많이 남았다.
이 부분에 대해 구현이 필요하다.
또한 이를 통합적으로 관리할 수 있는 UI 매니저 등에 대한 구현을 생각해보자.
(어제자와 변하지 않은 내용이나 하지 못한 일에 대해 메모용으로 남겨둔다)
1스테이지 맵은 얼추 다듬었으니, 본격적으로 게임성을 강조한 스테이지 맵을 만들 필요가 있다.
(어제자와 변하지 않은 내용이나 하지 못한 일에 대해 메모용으로 남겨둔다)
아무래도 슬슬 아이디어가 떨어져간다는 생각이 들었고, 아래와 같이 구현해야 할 사항을 추가해 보기로 했다.