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

게임개발 중급(55) - Monster Killer(17)

by jyppro 2023. 6. 15.

Monster Killer(17)

어제는 다양한 사운드 추가 및 2차 코드정리를 하였습니다. 오늘은 몬스터가 죽고 난 뒤에 몬스터를 공격해서 나오는 데미지 UI가 사라지지 않는 버그를 수정하고, 3차 코드정리를 진행해보도록 하겠습니다.

 

데미지 UI 버그 수정

현재 발생하는 문제점은 몬스터가 체력이 0이 되어 사망하면서, 죽는 애니메이션을 3초간 실행 후에 사라집니다. 하지만, 그 3초동안 몬스터를 때릴 수 있는데 몬스터가 사라지기 전에 화면에 남아있던 데미지 UI는 사라지지 않고 그 자리에 그대로 남아있는 것입니다. 사진을 통해 좀 더 쉽게 이해할 수 있도록 보여드리겠습니다.

데미지UI-버그
데미지UI 버그

이전에 몬스터 킬러 12번째 글에서 사용했던 사진입니다. 테스트를 진행하던 중, 해당 버그가 발생하여 몬스터가 이미 다 죽었는데도 불구하고, 데미지가 사라지지 않고 계속 남아있는 모습을 볼 수 있습니다.

 

해당 문제는 결국 몬스터가 사라지면서 데미지UI의 디스트로이가 실행되지 않았기 때문에 발생하는 문제입니다. 이제 해결하는 방법에 대해 알아보겠습니다.

 

MonsterController

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

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 List<GameObject> damageTextObjects = new List<GameObject>(); // 데미지 텍스트 리스트

    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;
        }
         // 이전 몬스터의 데미지 텍스트 UI 오브젝트 제거
        foreach (GameObject damageTextObject in damageTextObjects) { Destroy(damageTextObject); }
        damageTextObjects.Clear(); // 리스트 초기화
        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);
    damageTextObjects.Add(damageTextObject); // 리스트에 추가
    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); // 애니메이션 종료 후 텍스트 오브젝트 제거
    }
}

상당히 많은 기능을 내포하고 있는 몬스터 컨트롤러에 다시 돌아왔습니다. 일단 기존 코드에서 삭제할 부분은 딱히 없습니다. 왜냐하면 몬스터가 존재할 때, 디스트로이와 몬스터가 존재하지 않을 때는 다르게 처리하기 때문입니다.

먼저 데미지UI의 리스트를 만들어주고, 데미지 텍스트를 화면에 생성할 때, 해당 리스트에 추가시켜 줍니다. 그리고 몬스터가 죽으면 3초뒤에 SpawnNextMonster가 실행되는데, 이를 이용해서 스폰 되는 시점에 리스트에 들어있는 모든 데미지UI를 삭제시켜 주는 코드를 추가 작성하였습니다. 제대로 작동하는 지 테스트 해보겠습니다.

 

사망-후-데미지UI
몬스터 사망 후 데미지UI 생성

몬스터가 죽은 후에 몬스터를 공격해 사라지기 직전에 데미지UI를 띄웠습니다.

 

다음-스테이지
다음 스테이지

다음 스테이지에서 데미지UI가 남아있지 않고 사라진 모습을 볼 수 있습니다.

 

3차 코드정리

지난번에 이어서 3차 코드정리를 하겠습니다. 이전에 넣어주었던 [SerializeField]를 옆으로 배치해 코드의 길이를 조금 더 짧게 만들어주고, 코드 최상단에 선언되는 using문에서 필요없는 것들을 정리해주었습니다. 그리고 WeaponController의 파츠별 데미지 부분에서 각 부위마다 동일한 코드가 반복되는 현상이 보여 다음과 같이 수정해 주었습니다.

 

WeaponController 일부

private void OnCollisionEnter(Collision collision) //다른 물체와 충돌하는 순간
    {
        GetComponent<Rigidbody>().isKinematic = true; //중력 무시
        GetComponent<ParticleSystem>().Play(); // 파티클 실행

        if (!collision.gameObject.CompareTag("terrain"))
        {
            if (collision.gameObject.CompareTag("Head")) //몬스터의 각 파츠별 충돌 데미지
            { damage = Random.Range(30,50); }
            else if (collision.gameObject.CompareTag("L_Leg"))
            { damage = Random.Range(25,40); }
            else if (collision.gameObject.CompareTag("R_Leg"))
            { damage = Random.Range(25,40); }
            else if (collision.gameObject.CompareTag("Body"))
            { damage = Random.Range(20,35); }
            else if (collision.gameObject.CompareTag("Tail"))
            { damage = Random.Range(10,50); }
            collision.gameObject.transform.root.GetComponent<MonsterController>().TakeDamage_M(damage);
            collision.gameObject.transform.root.GetComponent<MonsterController>().ShowDamageText(damage, collision.GetContact(0).point);
            Destroy(gameObject, 0.5f);
        }
        else { Destroy(gameObject); }
    }

if문에서는 데미지의 수치만 판단하도록 하고, 데미지의 적용은 충돌부분에서 동시에 처리해줍니다. 하지만, 코드를 수정하기 전에는 사실 오류가 발생했었습니다. 데미지를 충돌부분에서 처리해주게 되면, terrain에 닿았을 때 몬스터와 충돌한 게 아니므로, 제대로 작동하지 않게 됩니다. 따라서 부위를 판단하기 전에 terrain을 판단하는 if문을 하나 더 걸어주었습니다.

 

<NEXT>

오늘은 데미지UI 버그수정 및 3차 코드정리를 진행하였습니다. 내일은 여태 사용하던 한 종류의 몬스터 말고 다른 몬스터를 추가로 연결시키는 과정에 대해 다뤄보겠습니다. 감사합니다.