6 분 소요

Intro

이번 포스팅에서 추가되는 기능은 다음과 같습니다.

  • 보스몬스터 스킬공격
    • 카메라 흔들림 연출
  • UI Manager
    • Esc 키 입력으로 UI 하나씩 끄기
    • Menu 창 UI 활성화
  • 자동저장

보스몬스터 스킬공격

두가지 공격을 구현해보았습니다.

  1. Thunderbolt
    • 장판형 스킬 : 범위안에있는 적을 지속적으로 데미지
    • 카메라 흔들림 연출 : VirtualCamera를 활용하여 스킬공격시 땅이 울리는듯한 느낌이 들도록
  2. 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에는 게임 볼륨세팅, 게임종료, 취소버튼이 있습니다.

메뉴 UI는 다음과같이 구성하였습니다.

image

  1. Settings 버튼 : 누르면 게임의 볼륨을 조절할 수 있는 UI가 활성화됩니다.
  2. Exit 버튼 : 누르면 게임이 종료됩니다.
  3. Cancel 버튼 : 누르면 Menu UI가 비활성화됩니다.

Settings UI는 다음과 같이 구성하였습니다.

image

  • 전체볼륨, 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 그룹을 생성합니다.

image


생성한 Master, BGM, SFX 항목을 인스펙터창에서 열어 Attenuation 밑의 Volume 을 우클릭하여

아래 항목을 선택해주어야합니다.

image

이 작업을 통해 각 그룹을 스크립트상에서 접근할 수 있습니다.


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인 로그함수입니다.

image


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초에 한번씩 호출해서 자동으로 저장하도록 했습니다.

댓글남기기