게임프로그래밍 개발 수업/유니티 수업

유니티 09 HP BAR UI 만들기, 출혈, 게임매니저의 등장

막뇌 2023. 3. 27. 13:40

03-27 수업 내용입니다.

 

오늘은 어제 진행하던 내용에 이어서 HP Bar를 만들어 보겠습니다.

어제는 Canvs에 이어서 Panel 생성하는것까지 했었습니다.

 

Image파일을 Panel의 자식으로 하나 생성해줍니다.

 

2중으로 하위항목이 만들어 졌네요.

Image 크기를 설정 해주고 소스이미지가 될 이미지를 찾습니다.

fadeWhite라는 흰색 단색 이미지를 찾았습니다.

 

Texture Type을 Sprite (2D and UI) 으로 변경합니다.

이미지 항목에서 소스 이미지(Source Image)를 해당 Sprite로 선택합니다.

선택 완료, Image Type의 하위항목도 밑줄 친것과 같이 변경합니다.

오른쪽으로 채워지는 형식이고, 가로방향으로 채워지는 타입의 Image 입니다.

 

ZombieDamage 클래스를 열어서

선언부에 using 지시문을 추가합니다.

using UnityEngine.UI;

이 지시문이 있어야 아래의 Image 자료형을 사용할 수 있습니다.

 

클래스 변수 선언부에 아래 내용을 추가 합니다.

public Image HpBar;
 public int hpInit = 100;

HpBar 와 시작할 때 HP양이라는 뜻입니다. 시작 hp가 100이네요.

 

 

 

아래는 Image.fillAmount 라는 컴포넌트를 가져온것인데요, float 자료형을 가지고 있습니다.

밑줄 친 부분에 있는 컴포넌트 입니다. 1로 표기 되어있는데 float 자료형을 사용합니다.

 

HpBar.fillAmount = (float)hp / (float)hpInit;

 

위 코드를 추가, 자료형은 float값만 가능하여 명시적 형변환 형태를 만들어 주었습니다.

 

            if (HpBar.fillAmount <= 0.3f)
                HpBar.color = Color.red;
            else if(HpBar.fillAmount <= 0.5f)
                HpBar.color = Color.yellow;

위 내용도 추가 합니다.

 

현재 체력의 수준에 맞게 HpBar의 색상을 변경해줍니다.

 

여기서 빨간색을 먼저 서술한 이유는 if문에 해당되면 else if까지 읽지 않고 바로 if/else 문을 탈출하기 때문입니다.

            if (HpBar.fillAmount <= 0.5f)
                HpBar.color = Color.yellow;
            else if(HpBar.fillAmount <= 0.3f)
                HpBar.color = Color.red;

이런식으로 코딩하게 되면 빨간색으로 바뀌는건 영원히 볼 수 없게 되니 주의 합니다.

 

hp = Mathf.Clamp(hp, 0, 100);

위 문장은 hp의 최소, 최대값을 0~100으로 고정해주는 역할을 해주는 유니티 제공 함수입니다.

HP가 100을초과 하지도 100미만으로 내려가지도 않습니다.

 

 

또 좀비가 죽었을 때5초 후에 사라지게 해줍니다.

Destroy(this.gameObject, 5.0f);

 

혈흔 효과도 추가 할 예정입니다.

 

public GameObject bloodEffect;

혈흔 추가용 변수 선언 입니다. 컴포넌트에 추가합니다.

 

총알에 맞아 hp를 제거하는 시점에서 이 오브젝트를 호출합니다.

GameObject blood = Instantiate(bloodEffect, col.gameObject.transform.position, Quaternion.identity);
Destroy(blood, 1.5f);

 

Instantiate는 프리팹을 등장시키는 함수라고 배웠었죠?

매개변수로 프리팹본체(오브젝트), 등장시킬 위치, 등장시킬 Rotation

What무엇을 Where어디에 How어떻게? 라고 외웠던 기억이 납니다.

마지막 어떻게는 바라보는 방향이었죠. 총알 프리팹을 생성할 때 썼었죠.

등장시키고 1.5초만에 사라지게 만들었습니다.

 

 

 

최종 수정 코드는 아래와 같습니다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;   //UI 관련 라이브러리를 제공받는다.

public class ZombieDamage : MonoBehaviour
{
    public Animator animator;
    public Image HpBar;
    public int hpInit = 100;
    public int hp = 100;
    private int damage = 35;
    ZombieCtrl zombieCtrl;
    public bool isDie;
    void Start()
    {
        HpBar.color = Color.green;
        zombieCtrl = GetComponent<ZombieCtrl>();
        animator = GetComponent<Animator>();
        isDie = false;
    }

    
    //OncollisionEnter와 같은 함수를 콜백함수라 한다. 외부에서 변수 선언을 안해도 자동으로 본인을 선언하면서 쓸 수 있게됨.
    private void OnCollisionEnter(Collision col) //Block되면서충돌 감지,
    {
        if (col.gameObject.CompareTag("BULLET"))
        {
            Destroy(col.gameObject);   //충돌한 게임 오브젝트 삭제
            Debug.Log("총맞았다."); //디버그용 출력문
            animator.SetTrigger("IsHit");   //IsHit트리거를 당겨라
            zombieCtrl.StopZombie();    //좀비의 움직임을 멈추는 메소드를 실행해라
            hp -= damage;   //HP를 대미지 만큼 깎는다.
            HpbarColor();

            if (hp <= 0)    //HP가 0이하인경우
                Die();  //아래에 있는 Die()메소드를 실행한다.
        }

    }

    private void HpbarColor()
    {
        hp = Mathf.Clamp(hp, 0, 100);
        HpBar.fillAmount = (float)hp / (float)hpInit;   //맞는 순간 이미지의 HpBar 에서 fillAmount 컴포넌트를 우항과 같이 초기화 시킨다.
                                                        //float값으로만 받는다.
        if (HpBar.fillAmount <= 0.3f)
            HpBar.color = Color.red;
        else if (HpBar.fillAmount <= 0.5f)
            HpBar.color = Color.yellow;
    }

    void Die()
    {
        isDie = true;   //죽음 상태 반환을 위한 bool자료형 선언  
        animator.SetTrigger("IsDie");   //죽는 애니메이션 트리거 당겨라
        zombieCtrl.StopZombie();    //이동을 멈추는 메소드, 위에 맞았을때 호출하는것과 같다.
        GetComponent<CapsuleCollider>().enabled = false;    //충돌 효과도 없앤다.
        Destroy(this.gameObject, 5.0f);
    }
}

 

 

 

카메라 이동에 관련한 스크립트도 아래와 같이 구성했습니다.

using UnityEngine;

public class LookAtCamera : MonoBehaviour
{
    public RectTransform CanvTr;
    public Transform CameraTr;

    void Start()
    {
        CanvTr = GetComponent<RectTransform>();
        CameraTr = Camera.main.transform;
        //MainCamera 라는 태그를 가진 카메라를 찾아 준다. 카메라는 점연산자로 태그 지정 가능
    }

    void Update()
    {
        CanvTr.LookAt(CameraTr);   //LookAt 함수를 사용하면 오브젝트가 인자를 향하도록 만들 수 있다. 트랜스폼 정보를 제공하면 된다.
    }
}

Canvas 오브젝트에 위 스크립트를 달아줍니다

 

 

CanvTr.LookAt(CameraTr);

여기서는 이 문장이 중요한데요 A.LookAt(B); A가 B를 바라본다는 뜻입니다.

 

새로 배우게 된 내용은 여기까지이고,

 

지금까지 배운 내용을 종합해서 같은 방법으로 Monster 라는 프리펩을 만들어 보았습니다.

 

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class MonsterCtrl : MonoBehaviour
{
    public Transform playerTr;
    public Transform monsterTr;
    public NavMeshAgent agent;
    public Animator animator;

    public float attackDist = 3.5f;
    public float traceDist = 20f;

    private MonsterDamage monsterDamage;	//MonsterDamage클래스 호출


    // Start is called before the first frame update
    void Start()
    {
        animator = GetComponent<Animator>();
        monsterDamage = GetComponent<MonsterDamage>();
        monsterTr = transform;
        playerTr = GameObject.FindWithTag("Player").transform;
        agent = GetComponent<NavMeshAgent>();
        agent.isStopped = true;
    }

    // Update is called once per frame
    void Update()
    {
        if (monsterDamage.isDie == true)
            return;

        float dist = Vector3.Distance(playerTr.position, monsterTr.position);


        if (dist <= attackDist)
        {
            animator.SetBool("IsAttack", true);

            agent.isStopped = true;
        }
        else if (dist <= traceDist)
        {
            animator.SetBool("IsAttack", false);
            animator.SetBool("IsWalk", true);
            agent.destination = playerTr.position;
            agent.isStopped = false;
        }
        else
        {
            agent.isStopped = true;
            animator.SetBool("IsWalk", false);
        }

    }

    public void MonsterStop()
    {
        agent.isStopped = true;
        agent.velocity = Vector3.zero;  
   

    }

}

움직임 애니메이션을 구현한 클래스 입니다 

NavMeshAgent를 사용해야 하기 때문에 using 지시문으로 아래 문장이 나오게 됩니다.

using UnityEngine.AI;

Update 함수에서 플레이어와 몬스터간 거리를 기준으로 움직임을 결정하게 됩니다.

isDie (죽었는지) 여부만 bool 형태로 MonsterDamage 클래스에서 호출합니다.

isDie 가 true 일 경우 움직임에 관련된 모든 Update 함수는 호출할 필요가 없어지기에 return 으로 빠져나옵니다.

MonsterStop() 메소드를 만들어서 움직임을 멈추어야 하는 순간에 호출할 수 있도록 하였습니다.

 

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

public class MonsterDamage : MonoBehaviour
{
    public int hp;
    public int initHp = 100;
    public int damage = 25;
    public Animator animator;
    public bool isDie = false;
    public Image hpBar;
    public GameObject bloodEffect;

    private MonsterCtrl monsterCtrl;

    // Start is called before the first frame update
    void Start()
    {        
        hp = initHp;
        hpBar = GameObject.Find("Image-mHpBar").GetComponent<Image>();
        hpBar.color = Color.green;
        animator = GetComponent<Animator>();
        monsterCtrl = GetComponent<MonsterCtrl>();
    }

    // Update is called once per frame
    void Update()
    {
        
    }

    private void OnCollisionEnter(Collision col)
    {
        if (col.gameObject.CompareTag("BULLET"))
        {
            Debug.Log("총맞음");
            Destroy(col.gameObject);
            monsterCtrl.MonsterStop();
            animator.SetTrigger("IsHit");
            hp -= damage;
            hp = Mathf.Clamp(hp, 0, 100);

            GameObject blood = Instantiate(bloodEffect, col.gameObject.transform.position, Quaternion.identity);
            Destroy(blood, 1.5f);

            M_HpBarColor();

            if (hp <= 0)
            {
                Debug.Log("죽음");
                IsDie();
            }
        }

    }

    private void M_HpBarColor()
    {        
        hpBar.fillAmount = (float)hp / (float)initHp;
        Debug.Log(hpBar.fillAmount);
        if (hpBar.fillAmount <= 0.25)
        {
            hpBar.color = Color.red;
        }
        else if (hpBar.fillAmount <= 0.5)
        {            
            hpBar.color = Color.yellow;
        }
    }

    public void IsDie()
    {
        isDie = true;   //죽음을 알리는 불리언값 true
        animator.SetTrigger("IsDie");   //죽기 트리거 발동
        monsterCtrl.MonsterStop();  //이동 멈추기
        GetComponent<Collider>().enabled = false;   //충돌 효과 제거
        Destroy(this.gameObject, 5.0f);
    }

}

몬스터가 피해를 입었을 때, HP 를 깎고,  맞았을 때 애니메이션이 동작하게 합니다.

몬스터 움직임에 관련된 IsStop() 메소드는 MonsterCtrl 클래스에서 가져옵니다.

HP가 0 이하가 되었을때, 사망 애니메이션과 충돌효과 제거, Destroy함수 호출 등 사망에 관련한 모든 것은 IsDie()메소드에 정리 되어있습니다.

 

HpBar 색상변경에 대한 알고리즘은 M_HpBarColor 메소드로 정리해놓았네요.

 

 

 

 

다음으로 랜덤 스폰 포인트를 만들어 보겠습니다.

Class 이름을 GameManager라고 해서 C#Script를 생성해줍니다.

 

다른 C#스크립트와는 구별되는 아이콘으로 GameManager가 만들어졌습니다.

GameManager는 게임 개발에 있어 자주 사용되는 형태의 소스이기 때문에 이렇게 이름만  바꿔도 유니티에서 아 이녀석이 게임매니저로구나 하고 인식하고 아이콘을 특별하게 바꿔줍니다.

 

게임 매니저에서 부활 포인트를 랜덤하게 주고, 부활하게 만들어 보았습니다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//1. 좀비랑 스켈레톤 태어나기 : 좀비 스켈레톤 프리팹
//2. SpawnPoint transform 배열선언해서 담기 : 스폰포인트 위치 가져오기
//3. 3초 간격으로 태어나기 : 리스폰 시간 변수 선언
//

public class GameManager : MonoBehaviour
{
    public GameObject zombiePrefab;
    public GameObject skeletonPrefab;
    public GameObject monsterPrefab;
    public Transform[] Points;
    private float timePrev;
    private float timePrev2;
    private float timePrev3;

    // Start is called before the first frame update
    void Start()
    {
        Points = GameObject.Find("SpawnPoints").GetComponentsInChildren<Transform>();
        //SpawinPoints라는 오브젝트명을 찾고 그 하위객체들의 트랜스폼을 Points 배열에 담는다.
        timePrev = Time.time;
        timePrev2 = Time.time;
        timePrev3 = Time.time;
    }

    // Update is called once per frame
    void Update()
    {
        if (Time.time - timePrev >= 3.0f)   //시간간격 3초마다 한번씩
        {
            timePrev = Time.time;
            CreateZombie();
        }

        if (Time.time - timePrev2 >= 5.0f)  //5초마다 한번씩
        {
            CreateSkeleton();
            timePrev2 = Time.time;
        }
        if(Time.time - timePrev3 >= 10.0f)
        {
            CreateMonster();
            timePrev3 = Time.time;
        }
    }
    void CreateSkeleton()
    {        
        int idx = Random.Range(1, Points.Length);   // [0,1,2,3,4,5,~] 배열 중에 0을 제외 하고 1부터 ~까지 중에서 1개의 숫자를 랜덤하게 뽑는다.
        Debug.Log(idx); //디버깅용
        Instantiate(skeletonPrefab, Points[idx].position, Points[idx].rotation);    //프리팹 생성, 랜덤좌표(1에서 ~번째 좌표 중에서 한개), 해당 rotation 값으로 생성한다.
    }
    void CreateZombie()
    {
        int idx = Random.Range(1, Points.Length);
        Instantiate(zombiePrefab, Points[idx].position, Points[idx].rotation);

        //부모는 빼고, 자식들의 배열 길이만 idx에 대입
    }
    void CreateMonster()
    {
        int idx = Random.Range(1, Points.Length);
        Instantiate(monsterPrefab, Points[idx].position, Points[idx].rotation);

    }
}

 

 

오늘은 체력 게이지를 UI로 표현하는 방법에 대해 배우게 되었습니다.