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

게임개발 중급(49) - Monster Killer(11)

by jyppro 2023. 6. 9.

Monster Killer(11)

저번에는 몬스터의 부위별 데미지 적용에 대해서 알아보았습니다. 이번에는 데미지를 시각적으로 표시하는 방법에 대해서 다뤄보겠습니다.

 

데미지 UI

이 게임을 만들어 나갈 초반에는 체력바에 깎이는 양만 봐도 괜찮다고 생각하며 만들었지만, 더 체력이 높아지고 몬스터들이 강해지면 유저의 입장에서 체력바로 데미지를 보는것은 한계가 있다고 생각이 들었습니다. 그래서 데미지를 화면에 표시해주는 UI를 만들어 보겠습니다.

 

일단 가장 먼저 Text UI를 만들어 주어야 합니다. TextMeshPro를 생성해 줍니다. 그 다음에는 이 생성된 UI를 프리팹으로 만들어 주겠습니다. 그 이유는 해당 UI를 사용해서 몬스터가 피격당한 위치에서 데미지를 받았다고 표시해줌과 동시에 애니메이션을 적용하여 데미지가 위로 천천히 이동하면서 서서히 사라지는 것을 구현할 것이기 때문입니다.

 

MonsterController

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

public class MonsterController : MonoBehaviour
{
    private Canvas canvas; // 캔버스
    private float maxHealth = 100f; // 최대 체력
    private float currentHealth = 100f; // 현재 체력
    public Slider healthSlider; // 체력바 슬라이더
    public Slider healthSliderCopy;
    public TextMeshProUGUI textHP; // 체력바 텍스트
    public TextMeshProUGUI textHPCopy;
    public GameObject damageTextPrefab; // 데미지 텍스트
    public GameObject damageTextPrefabCopy;
    private Animator animator; // 애니메이션
    public GameObject nextMonsterPrefab; // 다음 몬스터로 전환할 때 사용할 프리팹

    void Start()
    {
        canvas = FindObjectOfType<Canvas>(); // Canvas 찾기
        animator = GetComponent<Animator>(); // 애니메이터 연결
        healthSliderCopy = this.healthSlider;
        textHPCopy = this.textHP;
        damageTextPrefabCopy = this.damageTextPrefab;
        UpdateHealthSlider();
    }

    public void TakeDamage(int damage) // 데미지 주는 함수
    {
        currentHealth -= damage;
        if (currentHealth <= 0f)
        {
            currentHealth = maxHealth;
            // 체력이 전부 소진되면 다른 몬스터로 전환하고 체력 증가
            animator.SetBool("Death", true);
            Invoke("SpawnNextMonster", 3f);
        }
        UpdateHealthSlider();
    }

    private void SpawnNextMonster()
    {
        if (nextMonsterPrefab != null)
        {
            GameObject nextMonster = Instantiate(nextMonsterPrefab, transform.position, transform.rotation);
            MonsterController nextMonsterController = nextMonster.GetComponent<MonsterController>();
            nextMonsterController.healthSlider = healthSliderCopy;
            nextMonsterController.textHP = textHPCopy;
            nextMonsterController.damageTextPrefab = damageTextPrefabCopy;
            nextMonsterController.maxHealth = maxHealth;
            nextMonsterController.currentHealth = currentHealth;
            nextMonsterController.UpdateHealthSlider();
            nextMonsterController.IncreaseHealth();
        }
        Destroy(gameObject);
    }

    public void UpdateHealthSlider()
    {
        float decreaseHp = currentHealth / maxHealth;
        // 체력바 슬라이더의 값을 현재 체력 비율로 설정
        healthSlider.value = decreaseHp;
        textHP.text = $"{currentHealth} / {maxHealth}";
    }

    public void IncreaseHealth()
    {
        maxHealth = (maxHealth + 23f) * 2.2f;

        if (currentHealth != maxHealth)
        {
            currentHealth = maxHealth;
            UpdateHealthSlider();
        }
    }

    public void ShowDamageText(int damage, Vector3 position)
    {
        GameObject damageTextObject = Instantiate(damageTextPrefab, position, Quaternion.identity);
        TextMeshProUGUI textComponent = damageTextObject.GetComponent<TextMeshProUGUI>();
        textComponent.text = "-" + damage.ToString();

        // 텍스트의 부모를 Canvas로 설정
        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); // 애니메이션 종료 후 텍스트 오브젝트 제거
    }
}

뭔가가 많이 생겼습니다. 새로 생긴 것들에 대해 하나씩 살펴 보겠습니다.

우선 프리팹으로 만든 UI를 UI로 사용하기 위해선 캔버스를 거쳐야 합니다. 그래서 우선 캔버스를 선언해 연결해줍니다.

그리고 데미지 텍스트 프리팹을 연결할 변수를 만들고, 다른 UI 처럼 Copy버전도 만들어 줍니다. Copy는 다음 몬스터에게 넘겨주기 위해 만들어 주는 것입니다.

이제 함수를 살펴 보겠습니다. ShowDamageText()와 AnimateDamageText()가 새로 생겼습니다. 첫번째 것부터 보면 데미지를 띄워주는 역할을 하는 함수입니다. 데미지 수치와 위치정보를 매개변수로 받아와 데미지 텍스트 프리팹을 생성시켜줍니다. 그리고 텍스트 컴포넌트를 통해 데미지를 변경시켜 보여줍니다. 이때, 현재 데미지 텍스트는 프리팹으로 생성되어 하이어라키창에 존재하지 않는 상태입니다. 때문에 SetParent()를 통해 해당 프리팹을 캔버스 자식으로 넣어줘야 합니다.

이후에 코루틴을 통해 AnimateDamageText()를 실행시켜줍니다.

 

AnimateDamageText()는 말 그대로 애니메이션 작업을 해주는 함수입니다. 텍스트 컴포넌트를 가져와서 시작 포인트와 엔드포인트를 설정하여 1.5초 동안 시작에서 엔드로 벡터값을 조정하여 움직이도록 만들어 줍니다. 그리고 천천히 사라지게 만드는 방식도 같습니다. 현재 컬러와 투명컬러를 설정해준 뒤에 1.5초(Progress)동안 실행시키고 해당 텍스트 오브젝트는 파괴시킵니다.

 

이후에 WeaponController에 한줄만 추가해 주면 정상적으로 작동합니다.

collision.gameObject.transform.root.GetComponent<MonsterController>().ShowDamageText(damage, collision.GetContact(0).point);

이 코드는 충돌한 오브젝트의 루트위치에 있는 MonsterController의 ShowDamageText()를 해당되는 데미지와 위치에 띄우도록 실행시키는 코드입니다. OnCollisonEnter 마지막줄에 작성해주면 됩니다.

 

이제 실행시켜 보도록 하겠습니다.

 

게임실행

 

데미지-UI
데미지 UI

-30이라고 몬스터에게서 텍스트가 나오는 것을 확인할 수 있습니다. 해당 텍스트는 천천히 올라가면서 투명해집니다.

 

<NEXT>

이번에는 데미지 텍스트를 구현하는 과정에 대해 다뤄보았습니다. 다음에는 플레이어에게 스테이지에 대한 정보를 알려주기 위한 UI를 만드는 과정을 다뤄보겠습니다. 감사합니다.