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

게임개발 중급(51) - Monster Killer(13)

by jyppro 2023. 6. 12.

Monster Killer(13)

이전에는 간단한 스테이지 진행도 UI에 대해서 이야기 하였습니다. 이번에는 플레이어의 체력과 몬스터의 공격을 만들어 보도록 하겠습니다.

 

플레이어 체력 만들기

플레이어에게 체력바를 만들어 주겠습니다. 몬스터와 똑같이 슬라이더로 만들어 줍니다. 과정은 생략하겠습니다.

플레이어-체력
플레이어 체력

플레이어 체력이 생겼습니다. 이제 몬스터가 공격을 하고, 그 공격에 따라 플레이어의 체력이 깎이도록 만들어 보겠습니다.

 

공격 애니메이션 설정

가장 먼저, 몬스터가 공격을 하는 애니메이션을 설정해 주어야 합니다. 애니메이터의 컨트롤러에 들어가서 공격을 하는 애니메이션에 트랜잭션을 만들어 줍니다. 그리고 공격으로 넘어가게 해줄 파라미터로 Attack이란 이름의 트리거를 생성하겠습니다.

애니메이터-설정
애니메이터 설정

이제 Attack 트리거를 사용하여 애니메이션을 제어해 주어야 합니다. Attack 트리거가 활성화 되면, Idle에서 Basic Attack 애니메이션으로 넘어가도록 트랜잭션에 조건을 설정해 줍니다. 그리고 Idle -> Basic Attack 트랜잭션의 설정의 HasExit를 체크해제 시켜줍니다. duration과 나머지 설정들은 0으로 통일해줍니다. Basic Attack -> Idle은 HasExit은 그대로 둔 채로 수치만 0으로 변경해줍니다.

애니메이션-설정2
트랜잭션 설정

이제 스크립트로 넘어가서 작업을 마무리 해주겠습니다.

 

PlayerHP

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

public class PlayerHP : MonoBehaviour
{
    private float PlayerMaxHealth = 100f; // 최대 체력
    private float PlayerCurrentHealth = 100f; // 현재 체력
    public Slider PlayerhealthSlider; // 체력바 슬라이더
    public TextMeshProUGUI PlayerHPText; // 체력바 텍스트

    // Start is called before the first frame update
    void Start()
    {
        PlayerhealthSlider = this.GetComponent<Slider>();
        UpdateHealthSlider();
    }

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

    public void TakeDamage(float monsterPower)
    {
        PlayerCurrentHealth -= monsterPower;
        PlayerCurrentHealth = Mathf.Clamp(PlayerCurrentHealth, 0f, PlayerMaxHealth); // 체력이 음수가 되지 않도록 클램핑

        UpdateHealthSlider();

        if (PlayerCurrentHealth <= 0f)
        {
            // 플레이어가 사망한 경우 처리할 내용 추가
            // 예를 들어 게임 오버 화면을 표시하거나 게임을 재시작하는 등의 동작을 수행할 수 있습니다.
        }
    }
}

우선, 플레이어의 체력바를 다루기 위해 PlayerHP 스크립트를 만들어 플레이어 체력바 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 float MonsterPower = 5f; // 몬스터 공격력
    public float MonsterPowerCopy;
    public float attackInterval = 5f; // 공격 간격
    public Slider healthSlider; // 체력바 슬라이더
    public Slider healthSliderCopy;
    public Slider StageBar; // 스테이지를 나타내는 UI
    public Slider StageBarCopy;
    public TextMeshProUGUI textHP; // 체력바 텍스트
    public TextMeshProUGUI textHPCopy;
    public GameObject damageTextPrefab; // 데미지 텍스트
    public GameObject damageTextPrefabCopy;
    private Animator animator; // 애니메이션
    public GameObject nextMonsterPrefab; // 다음 몬스터로 전환할 때 사용할 프리팹
    private bool isAlive = true; // 몬스터가 살아있는지 여부

    void Start()
    {
        canvas = FindObjectOfType<Canvas>(); // Canvas 찾기
        animator = GetComponent<Animator>(); // 애니메이터 연결
        MonsterPowerCopy = this.MonsterPower;
        healthSliderCopy = this.healthSlider;
        StageBarCopy = this.StageBar;
        textHPCopy = this.textHP;
        damageTextPrefabCopy = this.damageTextPrefab;
        UpdateHealthSlider();
        InvokeRepeating("MonsterAttack", attackInterval, attackInterval);
    }

    public void MonsterAttack()
    {
        PlayerHP playerHP = FindObjectOfType<PlayerHP>();
        // 공격 애니메이션 실행
        if (playerHP && animator != null && isAlive == true)
        {
            animator.SetTrigger("Attack");
            // 플레이어에게 데미지를 줌
            playerHP.TakeDamage(MonsterPower);
        }
    }

    public void TakeDamage(int damage) // 데미지 주는 함수
    {
        currentHealth -= damage;
        if (currentHealth <= 0f)
        {
            isAlive = false;
            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.MonsterPower = MonsterPowerCopy;
            nextMonsterController.healthSlider = healthSliderCopy;
            nextMonsterController.StageBar = StageBarCopy;
            nextMonsterController.textHP = textHPCopy;
            nextMonsterController.damageTextPrefab = damageTextPrefabCopy;
            nextMonsterController.maxHealth = maxHealth;
            nextMonsterController.currentHealth = currentHealth;
            nextMonsterController.UpdateHealthSlider();
            nextMonsterController.IncreaseHealth();
            nextMonsterController.MonsterPower += 5f;
        }
        StageBar.value += 0.33f;
        if(StageBarCopy.value >= 1f)
        {
            StageBarCopy.value = 0f;
        }
        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); // 애니메이션 종료 후 텍스트 오브젝트 제거
    }
}

몬스터 컨트롤러의 스크립트가 상당히 길어졌는데, 저의 역량이 부족하다는 반증인 것 같습니다. 그래도 일단 변경사항을 살펴보자면, 몬스터의 공격력과 다음 몬스터에게 전달해줄 copy, 몬스터의 공격간격 그리고 몬스터의 생사여부를 체크하는 isAlive가 생겼습니다.

매커니즘은 간단합니다. MonsterAttack()이 AttackInterval에 맟춰 실행되면 동시에 플레이어의 체력을 몬스터의 공격력만큼 깎는 것입니다.

그리고 isAlive가 생긴이유는, 몬스터가 체력이 0이 되어 죽었을 때, 함수의 실행시간은 여전히 진행되므로, 몬스터가 죽고 사라지기 전에 데미지가 들어오지 않도록 막아주기 위해 조건을 걸어주었습니다. Copy는 역시 다음몬스터에게 더 강해진 공격력을 주기위해 추가되었습니다.

 

이제 플레이를 살펴보겠습니다.

게임플레이

몬스터의-공격
몬스터의 공격

몬스터가 5초간격으로 애니메이션을 실행하며 플레이어를 공격합니다. 플레이어는 현재 첫번째 몬스터가 가진 공격력인 5의 데미지를 입었습니다. 첫번째 몬스터를 잡고 다음 몬스터로 넘어가 보겠습니다.

 

몬스터의-공격2
두번째 몬스터의 공격

이전 몬스터에게 5의 체력이 깎인 상태에서 두번째 몬스터의 공격을 받아 10의 데미지를 받았습니다. 공격력이 정상적으로 증가하는 것을 확인할 수 있었고, 첫번째 몬스터의 체력이 0이 된 이후, 몬스터의 공격 데미지가 들어오지 않는 것을 확인하였습니다.

 

<NEXT>

오늘은 플레이어의 체력을 만들고, 몬스터가 공격하도록 애니메이션을 설정하고, 데미지를 만들어 주었습니다. 이제 이전에 점검했던 내용들을 전부 구현하였습니다. 다음시간은 두번째 점검시간을 갖도록 하겠습니다. 감사합니다.