본문 바로가기
Unity Portfolio/TinySwordDefense

3. 유니티 체력, 골드 (플레이어 스탯 생성), 적 체력 표시

by 첨부엉. 2024. 8. 2.

이번 글은 해당 영상을 보고 참고하여 작성하였습니다.

(2) [Unity 2D Game] Tower Defense #03 - 체력, 골드 시스템 - YouTube

 

 

적 체력 데이터 처리

EnemyHP.cs

더보기
using System.Collections;
using UnityEngine;
using UnityEngine.UI;

public class EnemyHP : MonoBehaviour
{
    [SerializeField]
    private float           maxHP;          // 최대 체력
    private float           currentHP;      // 현재 체력
    private bool            isDie = false;  // 적이 사망 상태이면 true
    private Enemy           enemy;          

    public float MaxHP => maxHP;
    public float CurrentHP => currentHP;

    private void Awake()
    {
        currentHP = maxHP;
        enemy = GetComponent<Enemy>();
        spriteRenderer = GetComponent<SpriteRenderer>();
    }

    public void TakeDamage(float damage)
    {
        // 적의 체력이 damage 만큼 감소해서 죽을 상황일 때 여러 타워의 공격을
        // 동시에 받으면 OnDie() 함수가 여러번 실행될 수 있음

        // 현재 적의 상태가 사망 상태이면 아래 코드 실행하지 않음
        if (isDie == true) return;

        // 현재 체력을 damage 만큼 감소
        currentHP -= damage;

        // 체력이 0이하 = 적 캐릭터 사망
        if(currentHP <=0)
        {
            isDie = true;
            // 적 캐릭터 사망
            enemy.OnDie(EnemyDestoryType.Kill);
        }
    }
}

 

적의 HP 스크립트를 생성하고 MaxHP는 5로 설정

 

TowerWeapon.cs

더보기
using System.Collections;
using UnityEngine;

// 공격 대상을 탐색하는 SearchTarget, 대상을 공격하는 AttackToTarget
public enum WeaponState { SearchTarget = 0, AttackToTarget }

public class TowerWeapon : MonoBehaviour
{
    [SerializeField]
    private GameObject      projectTilePrefab;                      // 발사체 프리팹
    [SerializeField]
    private Transform       spawnPoint;                             // 발사체 생성 위치
    [SerializeField]
    private float           attackRate = .5f;                       // 공격 속도
    [SerializeField]
    private float           attackRange = 2.0f;                     // 공격 범위
    [SerializeField]
    private int             attackDamage = 1;

    private WeaponState     weaponState = WeaponState.SearchTarget; // 타워 무기의 상태
    private Transform       attackTarget = null;                    // 공격 대상
    private EnemySpawner    enemySpawner;                           // 게임에 존재하는 적 정보 획득용

    [SerializeField]
    private Animator        archerAnimator;                         // 타워에 있는 아처 애니메이션
    private Quaternion      spawnArrowRot;

    public void Setup(EnemySpawner enemySpawner)
    {
        this.enemySpawner = enemySpawner;

        // 최초 상태를 WeaponState.SearchTarget 으로 설정
        ChangeState(WeaponState.SearchTarget);
    }

    public void ChangeState(WeaponState newState)
    {
        // 이전에 재생중이던 상태 종료
        StopCoroutine(weaponState.ToString());
        // 상태 변경
        weaponState = newState;
        // 새로운 상태 재생
        StartCoroutine(weaponState.ToString());
    }

    private void Update()
    {
        if(attackTarget != null)
        {
            RotateToTarget();
        }
    }

    /// <summary>
    /// 대상을 바라보게 하는 회전 함수
    /// </summary>
    private void RotateToTarget()
    {
        // 원점으로부터의 거리와 수평축으로부터의 각도를 이요해 위치를 구하는 극 좌표게 이용
        // 각도 = Arctan(y/x)
        // x, y 변위값 구하기
        float dx = attackTarget.position.x - transform.position.x;
        float dy = attackTarget.position.y - transform.position.y;

        // Archer가 왼쪽을 바라볼 수 있게 SpriteRenderer의 Filp.x 설정
        if(dx < 0){
            archerAnimator.gameObject.GetComponent<SpriteRenderer>().flipX = true;
            archerAnimator.SetFloat("Horizontal", -1);
        }
        else{
            archerAnimator.gameObject.GetComponent<SpriteRenderer>().flipX = false;
            archerAnimator.SetFloat("Horizontal", 1);
        }
        if (dy < 0)
            archerAnimator.SetFloat("Vertical", -1);
        else
            archerAnimator.SetFloat("Vertical", 1);

        // x, y 변위값을 바탕으로 각도 구하기
        // 각도가 radian 단위이기 때문에 Mathf.Rad2Deg를 곱해도 단위를 구함
        float degree = Mathf.Atan2(dy, dx) * Mathf.Rad2Deg;
        spawnArrowRot = Quaternion.Euler(0, 0, degree);

    }

    /// <summary>
    /// 현재 타워의 사정거리 안에 있으면서 가장 가까운 적의 거리를 저장
    /// </summary>
    /// <returns></returns>
    private IEnumerator SearchTarget()
    {
        while(true)
        {
            // 제일 가까이 있는 적을 찾기 위해 최초 거리를 최대한 크게 설정
            float closesDisSqr = Mathf.Infinity;
            // EnemySpawner의 EnemyList에 있는 현재 맵에 존재하는 적 검사
            for(int i =0; i<enemySpawner.EnemyList.Count; i++)
            {
                float distance = Vector3.Distance(enemySpawner.EnemyList[i].transform.position, transform.position);
                // 현재 감시중인 적과의 거리가 공격범위 내에 있고, 현재까지 감시한 적보다 거리가 가까우면
                if(distance <= attackRange && distance <= closesDisSqr)
                {
                    closesDisSqr = distance;
                    attackTarget = enemySpawner.EnemyList[i].transform;
                }
            }
            if(attackTarget != null)
            {
                ChangeState(WeaponState.AttackToTarget);
            }
            yield return null;
        }
    }

    /// <summary>
    /// 설정된 적을 공격하는 코루틴함수
    /// </summary>
    /// <returns></returns>
    private IEnumerator AttackToTarget()
    {
        while(true)
        {
            // 1. target이 있는지 검사(다른 발사제에 의해 제거, Goal 지점까지 이동해 삭제 등)
            if(attackTarget == null)
            {
                ChangeState(WeaponState.SearchTarget);
                break;
            }

            // 2. target이 공격 범위 안에 있는지 검사(공격 범위를 벗어나면 새로운 적 탐색)
            float distance = Vector3.Distance(attackTarget.position, transform.position);
            if(distance > attackRange)
            {
                attackTarget = null;
                ChangeState(WeaponState.SearchTarget);
                break;
            }
            // 3. attackRate 시간만큼 대기
            yield return new WaitForSeconds(attackRate);


            // 4. 공격 (발사체 생성)
            archerAnimator.SetTrigger("Attack");
            SpawnProjectTile();
        }
    }

    private void SpawnProjectTile()
    {
        GameObject clone = Instantiate(projectTilePrefab, spawnPoint.position, spawnArrowRot);

        // 생성된 발사체에게 공격대상(attackTarget) 정보 제공
        clone.GetComponent<Arrow>().Setup(attackTarget,attackDamage);
    }
}

타워의 공격력을 설정하고 발사체에게 타워의 공격력 정보를 매개변수로 전달함.

 

Arrow.cs

 

더보기
using UnityEngine;

/// <summary>
/// 타워가 발사하는 기본 발사체에 부착
/// </summary>
public class Arrow : MonoBehaviour
{
    private Movement2D  movement2D;
    private Transform   target;
    private int         damage;

    public void Setup(Transform target, int damage)
    {
        movement2D = GetComponent<Movement2D>();
        this.target = target;
        this.damage = damage;
    }

    /// <summary>
    /// 타겟이 존재하면 타겟 방향으로 이동, 타겟이 존재하지 않으면 삭제
    /// </summary>
    private void Update()
    {
        if(target != null)
        {
            // 발사체를 target의 위치로 이동
            Vector3 direction = (target.position - transform.position).normalized;
            transform.rotation = Quaternion.FromToRotation(Vector3.up, direction);

            movement2D.MoveTo(direction);
        }
        else
        {
            // 발사체 오브젝트 삭제
            Destroy(gameObject);
        }
    }

    /// <summary>
    /// 타겟으로 설정된 적과 부딪혔을 때 둘다 삭제
    /// </summary>
    /// <param name="collision"></param>
    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (!collision.CompareTag("Enemy")) return; // 적이 아닌 대상과 부딪히면
        if (collision.transform != target) return;  // 현재 target인 적이 아닐 때

        collision.GetComponent<EnemyHP>().TakeDamage(damage);    // 적 사망 함수 호출
        Destroy(gameObject);                            // 발사체 오브젝트 삭제
    }

}

Setup 함수에 데미지를 매개변수로 받고 OnTrigger 함수에서 EnemyHP 체력을 깎는다.

 

 

적 체력 정보를 받기 위해 SliderUI를 생성한다. 이때 캔버스와 이벤트시스템도 함께 생성됨

 

핸드폰 해상도는 보통 19:10 이기 때문에 Scale With Screen Size 로 변경하고 1900 * 1000 으로 설정한다.

 

슬라이더의 이미지와 이름은 아래와 같이 변경하고 

Health-Bar.png
0.00MB

 

 

 

이미지를 9-Slice 하여 이미지를 가로로 늘려도  깨지거나 늘어나지 않도록 설정한다.

Background 이미지로 설정하도록 하고

Image Type은 Sliced 로 설정한다음 Pixcel Per Unit Multiple을 적절히 설정한다.

 

이렇게 만든 슬라이더를 프리팹으로 생성하고 하이러키뷰의 슬라이더는 삭제한다.

 

SliderPositionAutoSetter.cs

더보기
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SliderPositionAutoSetter : MonoBehaviour
{
    [SerializeField]
    private Vector3         distance = Vector3.up * 20;
    private Transform       targetTransform;
    private RectTransform   rectTransform;

    public void Setup(Transform target)
    {
        // Slider UI가 쫓아다닐 target 설정
        targetTransform = target;
        // RectTransform 컴포넌트 정보 얻어오기
        rectTransform = GetComponent<RectTransform>();
        return;
    }

    private void LateUpdate()
    {
        // 적이 파괴되어 쫓아다닐 대상이 사라지면 Slider UI도 삭제
        if(targetTransform == null)
        {
            Destroy(gameObject);
            return;
        }

        // 오브젝트의 위치가 갱신된 이후에 Slider UI도 함께 위치를 설정하도록 하기 위해 LateUpdate()에서 호출

        // 오브젝트의 월드 좌표를 기준으로 화면에서의 좌표 값을 구현
        Vector3 screenPosition = Camera.main.WorldToScreenPoint(targetTransform.position);
        // 화면내에서 좌표 + distance만큼 떨어진 위치를 Slider UI의 위치로 설정
        rectTransform.position = screenPosition + distance;
        // 
    }
}

EnemyHPViewer.cs

더보기
using UnityEngine;
using UnityEngine.UI;

public class EnemyHPViewer : MonoBehaviour
{
    private EnemyHP enemyHP;
    private Slider hpSlider;

    public void Setup(EnemyHP enemyHP)
    {
        this.enemyHP = enemyHP;
        hpSlider = GetComponent<Slider>();
    }

    private void Update()
    {
        hpSlider.value = enemyHP.CurrentHP / enemyHP.MaxHP;
    }
}

EnemySpawner.cs

더보기
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EnemySpawner : MonoBehaviour
{
    [Header("EnemySpawn")]
    [SerializeField]
    private GameObject      enemyPrefab;
    [SerializeField]
    private GameObject      enemyHPSliderPrefab;
    [SerializeField]
    private Transform[]     myPoints;
    [SerializeField]
    private float           spawnTime;

    [Header("UI")]
    [SerializeField]
    private Transform       canvasTransform;  // UI를 표현하는 Canvas 오브젝트의 Transform

    [Header("PlayerStats")]
    [SerializeField]
    private PlayerHP        playerHP;
    [SerializeField]
    private PlayerGold      playerGold;

    private List<Enemy>     enemyList;

    // 적의 생성과 삭제는 EnemySpawner에서 하기 때문에 Set은 필요 없음
    public List<Enemy> EnemyList => enemyList;


    private void Awake()
    {
        // 적 리스트 메모리 할당
        enemyList = new List<Enemy>();
        // 적 생성 코루틴 함수 호출
        StartCoroutine("SpawnEnemy");
    }

    /// <summary>
    /// 적 스폰 코루틴 함수
    /// </summary>
    /// <returns></returns>
    private IEnumerator SpawnEnemy()
    {
        while(true)
        {
            GameObject clone = Instantiate(enemyPrefab);
            Enemy enemy = clone.GetComponent<Enemy>();

            enemy.Setup(this,myPoints);
            enemyList.Add(enemy);   // 리스트에 방금 생성된 적 정보 저장

            SpawnEnemyHPSlider(clone);  

            yield return new WaitForSeconds(spawnTime);
        }
    }

    public void DestoryEnemy(EnemyDestoryType type, Enemy enemy,int gold)
    {
        // 적이 목표지점까지 도착했을 때
        if(type == EnemyDestoryType.Arrive){
            playerHP.TakeDamage(enemy.GetComponent<EnemyHP>().CurrentHP);
        }
        else if(type==EnemyDestoryType.Kill){
            // 적의 종류에 따라 사망 시 골드 획득
            playerGold.CurrentGold += gold;
        }
        // 리스트에서 사망하는 적 정보 삭제
        enemyList.Remove(enemy);

        // 적 오브젝트 삭제
        Destroy(enemy.gameObject);
    }

    public void SpawnEnemyHPSlider(GameObject enemy)
    {
        // 적 체력을 나타내는 Slider UI 생성
        GameObject sliderClone = Instantiate(enemyHPSliderPrefab);
        // Slider UI 오브젝트를 parent("Canvas"오브젝트)의 자식으로 설정
        sliderClone.transform.SetParent(canvasTransform);
        // 계층 설정으로 바뀐 크기를 다시 (1,1,1)으로 설ㅊ정
        sliderClone.transform.localScale = Vector3.one;

        // Slider UI가 쫓아다닐 대상을 본인으로 설정
        sliderClone.GetComponent<SliderPositionAutoSetter>().Setup(enemy.transform);
        // Sldier UI에 자신의 체력 정보를 표시하도록 설정
        sliderClone.GetComponent<EnemyHPViewer>().Setup(enemy.GetComponent<EnemyHP>());
    }
}

 

플레이어 체력 관리

PlayerHP.cs 

플레이어의 체력을 외부에서 볼 수 있도록 프로퍼티 생성

더보기
using UnityEngine;
using UnityEngine.UI;
using System.Collections;

public class PlayerHP : MonoBehaviour
{
    [SerializeField]
    private Image imageScreen;
    [SerializeField]
    private float maxHP = 100;
    [SerializeField]
    private float currentHP;

    public float MaxHP => maxHP;
    public float CurrentHP => currentHP;

    private void Awake()
    {
        currentHP = maxHP;
    }

    public void TakeDamage(float damage)
    {
        currentHP -= damage;

        StopCoroutine(HitAlphaAnimation());
        StartCoroutine(HitAlphaAnimation());

        // 0이 되면 게임오버
        if(currentHP <=0)
        {

        }
    }

    private IEnumerator HitAlphaAnimation()
    {
        // 전체 화면 크기로 배치된 imageScreen의 색상을 color 변수에 저장
        // imageScreen의 투명도를 30% 설정
        Color color = imageScreen.color;
        color.a = 0.3f;
        imageScreen.color = color;

        // 투명도가 0이 될때까지 감소
        while(color.a>=0.0f)
        {
            color.a -= Time.deltaTime;
            imageScreen.color = color;

            yield return null;
        }
        
    }
}

적이 사라질 때 플레이어에 의해 사망했는지, 끝 지점에 다다라서 사라졌는지 알기 위한 열거형 변수를 생성

Enemy.cs

더보기
using System.Collections;
using UnityEngine;

public enum EnemyDestoryType { Kill =0, Arrive }

public class Enemy : MonoBehaviour
{
    private int             wayPointCount;      // 이동 경로 개수
    private Transform[]     wayPoints;          // 이동 경로 정보
    private int             currentIndex = 0;   // 현재 목표지점 인덱스
    private Movement2D      movement2D;         // 오브젝트 이동 제어
    private EnemySpawner    enemySpawner;       // 적의 삭제를 본인이 하지 않고  EnemySpawner에 알려서 삭제

    [SerializeField]
    private int gold = 10;  // 적 사망 시 획득 가능한 골드


    public void Setup(EnemySpawner enemySpawner, Transform[] wayPoints)
    {
        movement2D = GetComponent<Movement2D>();
        this.enemySpawner = enemySpawner;

        // 적 이동 경로 wayPoint 정보 저장
        wayPointCount = wayPoints.Length;
        this.wayPoints = new Transform[wayPointCount];
        this.wayPoints = wayPoints;

        // 적의 위치를 첫번째 wayPoint 위치로 설정
        transform.position = wayPoints[currentIndex].position;

        StartCoroutine(OnMove());
    }

    /// <summary>
    /// 적 이동/목표지점 설정 코루틴
    /// </summary>
    /// <returns></returns>
    private IEnumerator OnMove()
    {
        NextMoveTo();

        while(true)
        {
            // 적의 현재위치와 목표위치의 거리가 MoveSpeed 보다 적을 때 if 조건문 실행
            // Tip. MoveSpeed 를 곱해주는 이유는 속도가 빠르면 전 프레임에 0.02보다 크게 움직이기 때문에
            // if 조건문에 걸리지 않고 경로를 탈주하는 오브젝트가 발생할 수 있음
            if(Vector3.Distance(transform.position,wayPoints[currentIndex].position)<0.02f * movement2D.MoveSpeed)
            {
                NextMoveTo();
            }
            yield return null;
        }
    }

    /// <summary>
    /// 다음 이동 방향 설정
    /// </summary>
    private void NextMoveTo()
    {
        // 아직 이동할 wayPoints 가 남아있다면
        if(currentIndex < wayPointCount - 1)
        {
            // 적의 위치를 정확하게 목표 위치로 설정
            transform.position = wayPoints[currentIndex].position;
            // 이동 방향 설정 => 다음 목표지점(wayPoints)
            currentIndex++;
            Vector3 direction = (wayPoints[currentIndex].position - transform.position).normalized;
            movement2D.MoveTo(direction);
        }
        // 현재 위치가 마지막 wayPoints 라면
        else
        {
            // 목표지점에 도달해서 사망할 때는 돈을 주지 않도록 적용
            gold = 0;
            // 적 오브젝트 삭제
            OnDie(EnemyDestoryType.Arrive);
        }
    }

    public void OnDie(EnemyDestoryType type)
    {
        // EnemySpawner에서 리스트로 적 정보를 관리하기 때문에 Destory()를 직접하지 않고
        // EnemySpawner에게 본인이 삭제될 때 필요한 처리를 하도록 DestoryEnemy()함수 호출
        enemySpawner.DestoryEnemy(type,this,gold);
    }
}

OnDie 함수에는 EenmyDestoryType 매개변수를 받아서 처리

 

플레이어 스탯 UI

 

PannerlInfoUI 에 해당 이미지를 9-slice 해서 넣고

아래와 같이 적절히 이미지를 삽입

 

에셋 내에는 돈 이미지가 있어서 쉽게 사용할 수 있었는데

하트는 없어서 챗 gpt를 사용하여 이미지 생성함

이 이미지 맘에 안들긴 하지만 작게하면 괜찮아 보여서 일단 사용 중

 

 

글씨체는 에셋 사이트에서 사용중인 글씨체와 비슷한 느낌을 내기 위해 Dafont 에서 찾은 

Comic Book 이라는 글씨체 사용

 

TextTMPViewer.cs

더보기
using UnityEngine;
using TMPro;

public class TextTMPViewer : MonoBehaviour
{
    [Header("TextMeshPro")]
    [SerializeField]
    private TextMeshProUGUI textPlayerHP;
    [SerializeField]
    private TextMeshProUGUI textPlayerGold;

    [Header("PlayerStats")]
    [SerializeField]
    private PlayerHP playerHP;
    [SerializeField]
    private PlayerGold playerGold;

    private void Update()
    {
        textPlayerHP.text = playerHP.CurrentHP + "/" + playerHP.MaxHP;
        textPlayerGold.text = playerGold.CurrentGold.ToString();
    }
}

패널에 연결하여 사용

ImageScreen 을 생성하고 빨간색으로 색상 변경 후 Raycast Target 은 체크 해제

 

오브젝트의 투명도를 0으로 설정하고 코드로 작성하여 

플레이어가 데미지를 입으면 30% 의 투명도를 냈다가 다시 0 으로 줄어느는 효과를 냄

 

플레이어 골드 관리

PlayerGold.cs

더보기
using UnityEngine;

public class PlayerGold : MonoBehaviour
{
    [SerializeField]
    private int currentGold = 100;

    public int CurrentGold
    {
        set => currentGold = Mathf.Max(0, value);
        get => currentGold;
    }
}

외부에서 변수에 접근할 수 있도록 프로퍼티 생성

 

TowerSpawner.cs

더보기
using UnityEngine;

/// <summary>
/// 타워 생성 제어
/// </summary>
public class TowerSpawner : MonoBehaviour
{
    [SerializeField]
    private GameObject      towerPrefab;
    [SerializeField]
    private int             towerBuildGold = 50;    // 타워를 건설할 때 사용되는 골드
    [SerializeField]
    private EnemySpawner    enemySpawner;  // 현재 맵에 존재하는 적 리스트 정보를 얻기 위해
    [SerializeField]
    private PlayerGold      playerGold;

    /// <summary>
    /// 매개변수의 위치에 타워 생성
    /// </summary>
    /// <param name="tileTransform"></param>
    public void SpawnTower(Transform tileTransform)
    {
        if (towerBuildGold > playerGold.CurrentGold)
            return;

        Tile tile = tileTransform.GetComponent<Tile>();

        // 타워 건설 가능 여부 확인
        // 현재 타일의 위치에 이미 타워가 건설되어 있으면 타워건설 x
        if (tile.IsBuildTower == true)
        {
            return;
        }
        // 타워가 건설되어 있음으로 설정
        tile.IsBuildTower = true;
        // 타워 건설 골드만큼 플레이어 골드를 감소
        playerGold.CurrentGold -= towerBuildGold;
        // 선택한 타일의 위치에 타워 건설
        GameObject clone = Instantiate(towerPrefab, tileTransform.position, Quaternion.identity);
        // 타워 무기에  enemySpawner 정보 전달
        clone.GetComponent<TowerWeapon>().Setup(enemySpawner);
    }
}

타워를 설치할 때 50 골드씩 차감될 수 있도록 설정

 

위의 강의 영상과는 다르게 플레이어의 체력이 감소하는 방법은

적의 남아있는 체력만큼 깎이도록 설정하였다

 

적의 체력이 3만큼 남아있는데 홈에 들어왔다면 -3 

적의 체력이 1만큼 남았있는데 홈에 들어왔다면 -1