유니티 RPG - 54. 보스몬스터 스킬구현과 UI 매니저 구현
Intro
이번 포스팅에서 추가되는 기능은 다음과 같습니다.
- 보스몬스터 스킬공격
- 카메라 흔들림 연출
- UI Manager
- Esc 키 입력으로 UI 하나씩 끄기
- Menu 창 UI 활성화
- 자동저장
보스몬스터 스킬공격
두가지 공격을 구현해보았습니다.
- Thunderbolt
- 장판형 스킬 : 범위안에있는 적을 지속적으로 데미지
- 카메라 흔들림 연출 : VirtualCamera를 활용하여 스킬공격시 땅이 울리는듯한 느낌이 들도록
- Explosion
- 플레이어 타겟팅 : 플레이어의 위치에 공격
- 공중에서 운석이 떨어지는 연출과함께 피격시 밀려나는 공격을 구현
Grunt.cs
다음 공격을 결정하는 부분에서 추가되는 공격을 추가해주어야합니다.
두가지 스킬공격은 코루틴으로 작성하여 공격 연출 타이밍을 맞춰주었습니다.
// 다음 공격 정하기
private IEnumerator DecideNextAttack()
{
nav.isStopped = true;
isAttacking = true;
anim.SetFloat(hashSpeed, 0);
int randomAct = Random.Range(0, 4);
// 공격사이 간격
yield return new WaitForSeconds(1f);
// 다음 공격을 랜덤하게 결정
switch(randomAct)
{
case 0:
PlaySFX("Grunt_Attack01");
anim.SetInteger(hashAttackType, randomAct);
anim.SetTrigger(hashAttackTrigger);
prevAttack = 0;
break;
case 1:
anim.SetInteger(hashAttackType, randomAct);
anim.SetTrigger(hashAttackTrigger);
prevAttack = 1;
break;
case 2:
if(prevAttack == 2)
{
StopCoroutine(DecideNextAttack());
StartCoroutine(DecideNextAttack());
}
else
{
PlaySFX("Grunt_Attack03");
anim.SetInteger(hashAttackType, randomAct);
anim.SetTrigger(hashAttackTrigger);
StartCoroutine(Thunderbolt());
prevAttack = 2;
}
break;
case 3:
if(prevAttack == 3)
{
StopCoroutine(DecideNextAttack());
StartCoroutine(DecideNextAttack());
}
else
{
PlaySFX("Grunt_Attack04");
anim.SetInteger(hashAttackType, randomAct);
anim.SetTrigger(hashAttackTrigger);
StartCoroutine(Explosion());
prevAttack = 3;
}
break;
}
yield return null;
}
// 스킬 공격1
private IEnumerator Thunderbolt()
{
thunderboltEffect.TryGetComponent<GruntThunderbolt>(out GruntThunderbolt hitbox);
if (hitbox == null)
{
hitbox = thunderboltEffect.AddComponent<GruntThunderbolt>();
hitbox.damage = damage;
}
noise.m_AmplitudeGain = 1f; // 카메라 흔들림 ON
thunderboltEffect.SetActive(true);
yield return new WaitForSeconds(3f);
noise.m_AmplitudeGain = 0f; // 카메라 흔들림 Off
thunderboltEffect.SetActive(false);
}
// 스킬 공격2
private IEnumerator Explosion()
{
explosionAttacker.TryGetComponent<GruntExplosion>(out GruntExplosion hitbox);
if(hitbox == null)
{
hitbox = explosionAttacker.AddComponent<GruntExplosion>();
hitbox.damage = damage * Random.Range(0.3f, 0.7f);
}
explosionEffect.transform.position = GameManager.Instance.player.transform.position;
explosionEffect.SetActive(true);
yield return new WaitForSeconds(1f);
explosionAttacker.transform.position = GameManager.Instance.player.transform.position + GameManager.Instance.player.transform.up * 2f;
explosionAttacker.SetActive(true);
yield return new WaitForSeconds(1f);
explosionAttacker.SetActive(false);
explosionEffect.SetActive(false);
EndAttack();
}
- 똑같은 스킬 공격을 연속으로 사용하지 않도록하였습니다.
- 각 스킬의 공격판정은 클래스로 만들어 구현하였습니다.
- GruntThunderbolt.cs
- GruntExplosion.cs
GruntThunderbolt.cs
Thunderbolt는 플레이어가 공격의 범위내에 있을때 데미지를 입도록 하였습니다.
/*
GruntThunderbolt
- 보스몬스터 Grunt 스킬 공격
- 장판형 지속스킬
*/
public class GruntThunderbolt : MonoBehaviour
{
public float damage;
public float attackInterval = 0.5f; // 공격판정주기
private float timer = 0f;
private void OnTriggerStay(Collider ohter)
{
if (ohter.CompareTag("Player"))
{
timer += Time.deltaTime;
if (timer >= 0.5f)
{
DataManager.Instance.GetPlayerData().GetDamaged(damage * Random.Range(0.1f, 0.4f));
timer = 0;
}
}
}
}
GruntExplosion.cs
Explosion은 플레이어의 Ridigbody의 Addforce를 이용하여 피격을 구현하였습니다.
public class GruntExplosion : MonoBehaviour
{
public float damage;
public float knocebackForce = 7f;
public float duration = 0.2f;
private void OnTriggerEnter(Collider other)
{
if(other.CompareTag("Player"))
{
Debug.Log("플레이어 피격");
DataManager.Instance.GetPlayerData().GetDamaged(damage);
}
Rigidbody rigid = other.GetComponent<Rigidbody>();
if(rigid != null)
{
Vector3 knockbackDir = (other.transform.forward).normalized;
rigid.AddForce(knockbackDir * knocebackForce, ForceMode.Impulse);
}
}
}
UI Manager
UI Manager 클래스는 게임내 UI들을 활성화 및 비활성화시키는 역할을 합니다.
또한, 활성화된 UI들을 “Escape” 키 입력으로 비활성화 시키는 작업을 수행합니다.
이때 비활성화되는 순서는 가장 최근에 열린 UI부터입니다. 따라서 Stack을 사용해서 구현하였습니다.
활성화 되어있는 UI가 없을경우에 “Escape”키 입력을통해 Menu를 활성화시킬 수 있습니다.
/*
UIManager
- UI 활성화
- Escape 키 : 가장 최근에 열린 UI 비활성화(Stack 활용)
*/
public class UIManager : Singleton<UIManager>
{
[SerializeField] private GameObject menuUI;
private Stack<GameObject> uiStack = new();
protected override void Awake()
{
base.Awake();
}
private void Update()
{
if(Input.GetKeyDown(KeyCode.Escape))
{
if(IsAnyUIOpen())
{
CloseTopUI();
}
else
{
ToggleMenuUI();
}
}
}
public void OpenUI(GameObject ui)
{
if (ui.activeSelf) return;
ui.SetActive(true);
uiStack.Push(ui);
}
public void CloseUI(GameObject ui)
{
if (!ui.activeSelf) return;
ui.SetActive(false);
if(uiStack.Contains(ui))
{
// 스택에서 해당 UI 제거
Stack<GameObject> tempStack = new();
while(uiStack.Count > 0)
{
GameObject top = uiStack.Pop();
if (top == ui) break;
tempStack.Push(top);
}
while(tempStack.Count>0)
{
uiStack.Push(tempStack.Pop());
}
}
}
// UI 활성/비활성화 토글
public void ToggleUI(GameObject ui)
{
if(ui.activeSelf)
{
CloseUI(ui);
}
else
{
OpenUI(ui);
}
}
// 가장 최근에 활성화된 UI 비활성화
public void CloseTopUI()
{
if (uiStack.Count == 0) return;
GameObject topUI = uiStack.Pop();
topUI.SetActive(false);
}
public bool IsAnyUIOpen()
{
return uiStack.Count > 0;
}
private void ToggleMenuUI()
{
menuUI.SetActive(!menuUI.activeSelf);
}
}
-
인벤토리, 프로필등 기존에 클래스 자체에서 UI를 활성화 및 비활성화하던 부분을 UI Manager를 통해 처리하도록 하였습니다.
-
또한 “Escape” 키를 입력하면 한번에 모든 UI들이 닫히던 점을 보완하였습니다.
-
활성화되어있는 UI가 없을때 “Escape” 키 입력을 통해 Menu 창을 활성화할 수 있습니다.
- Menu UI에는 게임 볼륨세팅, 게임종료, 취소버튼이 있습니다.
Menu UI
메뉴 UI는 다음과같이 구성하였습니다.
- Settings 버튼 : 누르면 게임의 볼륨을 조절할 수 있는 UI가 활성화됩니다.
- Exit 버튼 : 누르면 게임이 종료됩니다.
- Cancel 버튼 : 누르면 Menu UI가 비활성화됩니다.
Settings UI는 다음과 같이 구성하였습니다.
- 전체볼륨, BGM볼륨, SFX볼륨을 조절할 수 있습니다.
- Quit 버튼 클릭을통해 메뉴로 돌아갈 수 있습니다.
// MenuUI.cs
public class MenuUI : MonoBehaviour
{
[Header("Connected UI")]
[SerializeField] private GameObject settingsButtonGo;
[SerializeField] private GameObject exitButtonGo;
[SerializeField] private GameObject cancelButtonGo;
[SerializeField] private GameObject settingsGo;
// 세팅 버튼 클릭 이벤트
public void OnSettingsButtonClicked()
{
// 볼륨 세팅 UI 활성화
UIManager.Instance.CloseUI(this.gameObject);
UIManager.Instance.OpenUI(settingsGo);
}
// 게임 종료 버튼 클릭 이벤트
public void OnExitButtonClicked()
{
Application.Quit();
}
// 취소 버튼 클릭 이벤트
public void OnCancelButtonClicked()
{
// 메뉴창 비활성화
UIManager.Instance.CloseUI(this.gameObject);
}
}
- MenuUI 클래스는 메뉴UI에서 각 버튼을 눌렀을때의 이벤트를 정의합니다.
- Settings 버튼 클릭 : Menu UI를 비활성화하고, 볼륨세팅UI를 활성화시킵니다.
- Exit 버튼 클릭 : 게임을 종료합니다.
- Cancel 버튼 클릭 : Menu UI를 비활성화합니다.
볼륨세팅
볼륨을 조절하는 Settings 옵션을 구현하기위해 AudioMixer를 사용했습니다.
AudioMixer를 생성하고, BGM과 SFX 그룹을 생성합니다.
생성한 Master, BGM, SFX 항목을 인스펙터창에서 열어 Attenuation 밑의 Volume 을 우클릭하여
아래 항목을 선택해주어야합니다.
이 작업을 통해 각 그룹을 스크립트상에서 접근할 수 있습니다.
AudioManager 클래스에서 AudioMixer를 사용하기위한 작업을 수행합니다.
public class AudioManager : Singleton<AudioManager>
{
[SerializeField] private AudioMixer audioMixer;
[SerializeField] private AudioMixerGroup bgmGroup;
[SerializeField] private AudioMixerGroup sfxGroup;
private const string MASTER_VOL = "MasterVolume";
private const string BGM_VOL = "BGMVolume";
private const string SFX_VOL = "SFXVolume";
// 초기화
private void InitAudioSources()
{
// BGM
//...
bgmSource.outputAudioMixerGroup = bgmGroup;
// SFX
//...
for (int i = 0; i < sfxPoolSize; i++)
{
AudioSource source = sfxObject.AddComponent<AudioSource>();
source.outputAudioMixerGroup = sfxGroup;
sfxSource.Add(source);
}
}
// 마스터 볼륨제어
public void SetMasterVolume(float value)
{
// 0.0001~1 => -80db~0db
float volume = Mathf.Clamp(value, 0.0001f, 1f); // 최소값 제한
float dB = Mathf.Log10(volume) * 20;
audioMixer.SetFloat(MASTER_VOL, dB);
}
// BGM 볼륨제어
public void SetBGMVolume(float value)
{
// 0.0001~1 => -80db~0db
float volume = Mathf.Clamp(value, 0.0001f, 1f); // 최소값 제한
float dB = Mathf.Log10(volume) * 20;
audioMixer.SetFloat(BGM_VOL, Mathf.Log10(value) * 20);
}
// SFX 볼륨제어
public void SetSFXVolume(float value)
{
// 0.0001~1 => -80db~0db
float volume = Mathf.Clamp(value, 0.0001f, 1f); // 최소값 제한
float dB = Mathf.Log10(volume) * 20;
audioMixer.SetFloat(SFX_VOL, Mathf.Log10(value) * 20);
}
}
- AudioMixer와 AudioMixerGroup을 인스펙터창에서 등록해주어야합니다.
- MASTER_VOL, BGM_VOL, SFX_VOL : Expose한 그룹의 이름 -> AudioMixer에서 Rename가능
-
초기화코드에서 bgm 과 sfx를 연결해줍니다.
- 볼륨을 제어하는 부분은 SetVolume 메서드에서 수행합니다.
- UI Slider를 통해 볼륨을 조절할것입니다. Slider의 value는 0~1의 값입니다.
- 오디오 믹서는 -80dB ~ 0dB로 구성되어 있기 때문에 log(value) * 20을 합니다.
- 따라서 Slider의 min값은 0.001, max값은 1이 되어야합니다.
- Mathf.Log10은 밑이 10인 로그함수입니다.
VolumeSetter 클래스는 UI 슬라이더의 On Value Changed 이벤트에 등록할 내용을 정의합니다.
// VolumeSetter.cs
public class VolumeSetter : MonoBehaviour
{
[SerializeField] private Slider masterVolumeSlider;
[SerializeField] private Slider bgmVolumeSlider;
[SerializeField] private Slider sfxVolumeSlider;
[SerializeField] private Button quitButton;
[SerializeField] private GameObject menuUIGo;
private void Start()
{
OnMasterVolumeChanged();
OnBgmVolumeChanged();
OnSfxVolumeChanged();
}
// 전체 볼륨 조절(슬라이더로 조절)
public void OnMasterVolumeChanged()
{
AudioManager.Instance.SetMasterVolume(masterVolumeSlider.value);
bgmVolumeSlider.value = masterVolumeSlider.value;
sfxVolumeSlider.value = masterVolumeSlider.value;
}
// BGM 볼륨 조절(슬라이더로 조절)
public void OnBgmVolumeChanged()
{
AudioManager.Instance.SetMasterVolume(bgmVolumeSlider.value);
}
// SFX 볼륨 조절(슬라이더로 조절)
public void OnSfxVolumeChanged()
{
AudioManager.Instance.SetSFXVolume(sfxVolumeSlider.value);
}
// 나가기 버튼 클릭 이벤트
public void OnQuitButtonClicked()
{
// 메뉴창으로 되돌아가기
UIManager.Instance.CloseUI(this.gameObject);
UIManager.Instance.OpenUI(menuUIGo);
}
}
- 슬라이더를 조절할 때 각 메서드가 호출이되어 AudioManager의 SetVolume() 메서드를 호출합니다.
자동저장
플레이어가 수동적으로 게임내용을 저장하는것이 아니라, 자동으로 저장되도록 하였습니다.
DataManager에 다음 내용을 추가하였습니다.
private void Start()
{
// 5초마다 자동 저장
InvokeRepeating("SavePlayerData", 5f, 5f);
}
InvokeRepeating 함수를 사용하여 SavePlayerData() 메서드를 5초에 한번씩 호출해서 자동으로 저장하도록 했습니다.
댓글남기기