본문 바로가기
게임 프로그래밍/게임개발 중급

게임개발 중급(54) - Monster Killer(16)

by jyppro 2023. 6. 14.

Monster Killer(16)

저번시간에는 사운드의 기본적인 사용과 1차 코드정리에 대해 다루었습니다. 오늘은 했던 내용에 이어서 다양한 사운드 적용 및 2차 코드정리에 대해 이야기 해보겠습니다.

 

다양한 사운드 사용 및 적용

이전에는 기본적으로 사운드를 사용하는 방법에 대해 알아보았습니다. 이번에는 다양한 동작에 맞춘 각각의 사운드를 실행하는 방법에 대해 알아보겠습니다. 가장 먼저 사용할 사운드를 정해야 합니다. 이전에 이미 말씀드렸지만, 저는 몬스터 소환, 공격, 피격, 사망 이렇게 총 4개의 사운드를 사용해 보겠습니다.

 

Monster Controller

using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using TMPro;

public class MonsterController : MonoBehaviour
{
    [SerializeField]
    private Slider healthSlider; // 몬스터 체력바 UI
    [SerializeField]
    private Slider StageBar; // 스테이지 UI
    [SerializeField]
    private TextMeshProUGUI textHP; // 체력바 텍스트 UI
    [SerializeField]
    private GameObject damageTextPrefab; // 데미지 텍스트 UI
    [SerializeField]
    private GameObject nextMonsterPrefab; // 다음 몬스터로 전환할 때 사용할 프리팹
    [SerializeField]
    private AudioClip[] Clips; // 사용할 오디오 클립의 배열
    private AudioSource MonsterAudio; // 몬스터 사운드
    private Animator animator; // 애니메이터
    private Canvas canvas; // 캔버스
    private bool isAlive = true; // 몬스터 생사 여부
    private float MonsterMaxHealth = 100f; // 몬스터 최대 체력
    private float MonsterCurrentHealth = 100f; // 몬스터 현재 체력
    private float MonsterPower = 5f; // 몬스터 공격력
    private float attackInterval = 5f; // 공격 간격

    private float MonsterPowerCopy;
    private Slider healthSliderCopy;
    private Slider StageBarCopy;
    private TextMeshProUGUI textHPCopy;
    private GameObject damageTextPrefabCopy;

    void Start()
    {
        canvas = FindObjectOfType<Canvas>(); // Canvas 연결
        animator = GetComponent<Animator>(); // 애니메이터 연결
        MonsterAudio = GetComponent<AudioSource>(); // 오디오 소스 연결
        MonsterAudio.clip = Clips[0]; // 몬스터 소환 효과음
        MonsterAudio.Play();

        // 복사본 저장
        MonsterPowerCopy = this.MonsterPower;
        healthSliderCopy = this.healthSlider;
        StageBarCopy = this.StageBar;
        textHPCopy = this.textHP;
        damageTextPrefabCopy = this.damageTextPrefab;

        UpdateHealthSlider();
        InvokeRepeating("MonsterAttack", attackInterval, attackInterval);
    }

    private void MonsterAttack() // 몬스터 공격 애니메이션 실행 및 플레이어에게 데미지 주기
    {
        PlayerHP playerHP = FindObjectOfType<PlayerHP>();
        if (playerHP && animator != null && isAlive == true)
        {
            animator.SetTrigger("Attack");
            MonsterAudio.clip = Clips[1]; // 몬스터 공격 효과음
            MonsterAudio.Play();
            playerHP.TakeDamage_P(MonsterPower);
        }
    }

    public void TakeDamage_M(int damage) // 몬스터에게 데미지를 주는 함수
    {
        MonsterCurrentHealth -= damage;
        MonsterAudio.clip = Clips[2]; // 몬스터 피격 효과음
        MonsterAudio.Play();

        // 체력이 전부 소진되면 다음 몬스터 소환
        if (MonsterCurrentHealth <= 0f)
        {
            isAlive = false;
            MonsterCurrentHealth = MonsterMaxHealth;
            animator.SetBool("Death", true);
            MonsterAudio.clip = Clips[3]; // 몬스터 사망 효과음
            MonsterAudio.Play();
            Invoke("SpawnNextMonster", 3f);
        }
        UpdateHealthSlider();
    }

    private void SpawnNextMonster() // 다음 몬스터를 소환하는 함수
    {
        if (nextMonsterPrefab != null)
        {
            GameObject nextMonster = Instantiate(nextMonsterPrefab, transform.position, transform.rotation);
            MonsterController nextMonsterController = nextMonster.GetComponent<MonsterController>();
            nextMonsterController.MonsterPower = MonsterPowerCopy;
            nextMonsterController.healthSlider = healthSliderCopy;
            nextMonsterController.StageBar = StageBarCopy;
            nextMonsterController.textHP = textHPCopy;
            nextMonsterController.damageTextPrefab = damageTextPrefabCopy;
            nextMonsterController.MonsterMaxHealth = MonsterMaxHealth;
            nextMonsterController.MonsterCurrentHealth = MonsterCurrentHealth;
            nextMonsterController.UpdateHealthSlider();
            nextMonsterController.IncreaseHealth();
            nextMonsterController.MonsterPower += 5f;
        }
        StageBar.value += 0.33f;
        if(StageBarCopy.value >= 1f) { StageBarCopy.value = 0f; }
        Destroy(gameObject);
    }

    private void UpdateHealthSlider() // 체력바 업데이트
    {
        // 체력바 슬라이더의 값을 현재 체력 비율로 설정
        float decreaseHp = MonsterCurrentHealth / MonsterMaxHealth;
        healthSlider.value = decreaseHp;
        textHP.text = $"{MonsterCurrentHealth} / {MonsterMaxHealth}";
    }

    private void IncreaseHealth() // 다음몬스터 소환 시 체력 증가
    {
        MonsterMaxHealth = (MonsterMaxHealth + 23f) * 2.2f; // 임의로 설정한 수치
        if (MonsterCurrentHealth != MonsterMaxHealth) { MonsterCurrentHealth = MonsterMaxHealth; }
        UpdateHealthSlider();
    }

    public void ShowDamageText(int damage, Vector3 position) // 몬스터가 받는 데미지 보여주기
    {
        GameObject damageTextObject = Instantiate(damageTextPrefab, position, Quaternion.identity);
        TextMeshProUGUI textComponent = damageTextObject.GetComponent<TextMeshProUGUI>();
        textComponent.text = "-" + damage.ToString();
        damageTextObject.transform.SetParent(canvas.transform, false);
        StartCoroutine(AnimateDamageText(textComponent));
    }

    private IEnumerator AnimateDamageText(TextMeshProUGUI textComponent) // 데미지에 애니메이션 적용
    {
        float duration = 1.5f; // 애니메이션 지속 시간
        float elapsedTime = 0f;

        Vector3 startPosition = textComponent.transform.position;
        Vector3 endPosition = startPosition + Vector3.up * 50f; // 위로 올라갈 위치
        Color startColor = textComponent.color;
        Color endColor = new Color(startColor.r, startColor.g, startColor.b, 0f); // 투명도 조정

        while (elapsedTime < duration)
        {
            float progress = elapsedTime / duration;
            textComponent.transform.position = Vector3.Lerp(startPosition, endPosition, progress); // 위치 이동
            textComponent.color = Color.Lerp(startColor, endColor, progress); // 투명도 조정
            elapsedTime += Time.deltaTime;
            yield return null;
        }
        Destroy(textComponent.gameObject); // 애니메이션 종료 후 텍스트 오브젝트 제거
    }
}

사운드가 사용될 스크립트 내용입니다. 오디오 소스는 지난시간에 추가해 주었으니, 이제 코드에서 다룰 수 있도록 선언 및 연결을 해줍니다. 그리고 사용할 오디오 클립을 오디오 소스 컴포넌트에 연결을 해 주어야 하는데, 해당 과정을 배열을 생성하여 사용할 클립을 선택해 각 인덱스에 넣어준 뒤에 특정 동작부분에서 해당 클립을 넣어 실행시켜주는 방식입니다.

배열-사용
배열 사용

해당 사진은 현재 몬스터 프리팹에 들어가 있는 MonsterController 스크립트의 모습입니다. 배열을 만들고 사용할만큼 Element를 만들어 오디오 클립을 지정해줘서 사용합니다.

 

해당 작업을 하는 도중에 몬스터 소환되는 부분의 소리가 Idle 애니메이션과는 어울리지 않다고 생각하여, 게임 시작 시 한번 플레이 되는 Scream 애니메이션을 추가해 주었습니다.

 

애니메이션-트리
애니메이션 트리

속도를 소리와 맞추기 위해 Scream의 속도를 1.6배로 설정해 주었습니다. 이렇게 해서 게임을 플레이 하면, 몬스터가 소환되면서 울음소리를 내고, 몬스터가 공격받으면 피격음, 플레이어를 공격하면 공격음 그리고 몬스터가 죽으면 사망음에 해당하는 효과음이 나게 됩니다. 소리는 영상을 찍지 않는 이상 보여드릴 방법이 없으므로 글로만 설명하겠습니다.

 

2차 코드정리

앞서 MonsterController 코드를 보시면 아시겠지만, 2차 코드정리를 하였습니다. 기존의 모든 변수와 함수들이 대부분 public으로 접근 제한자가 설정되어 있었는데, 이는 좋은 방법이 아닙니다. 다른 위치에서 사용이 되어야 한다면 public이 요구되지만, 그렇지 않다면 private로 설정하는 것이 더 좋습니다. 그래서 가능한 대부분의 변수 및 함수를 private처리 하였고, 인스펙터 창에서 볼 필요가 있는 것들은 [SerializeField]를 사용하여 다룰 수 있도록 해주었습니다. 추가로 주석과 코드의 위치 또한 정리해주었습니다.

 

<NEXT> 

오늘은 다양한 사운드 추가 및 2차 코드정리를 진행하였습니다. 다음은 죽고 사라지기 전에 나온 데미지가 사라지지 않는 버그를 고치는 작업과 함께 3차 코드정리를 진행하도록 하겠습니다. 감사합니다.