Notice
Recent Posts
Recent Comments
Link
«   2026/06   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
Tags
more
Archives
Today
Total
관리 메뉴

개발일기

유니티#15 UI 활용 본문

Unity

유니티#15 UI 활용

kimjw7815 2025. 1. 3. 01:59

UI... 만들어야겠지?
DamageTextCanvas를 만들어주고, Canvas 컴포넌트에서 Render Mode : World Space로 해준다 

UI의 배치 기준을 설정해주는 건데, 저게 Screen Space - Overlay로 되어있으면 화면을 기준으로 하고, World Space면 보통의 오브젝트처럼 소환이 된다는 거다

왼쪽 구석 밑이 실제 배치한 오브젝트들

저 대화창은 Screen Space - Overlay라서 그대로 화면에 투영되는 거고, 우리는 UI 오브젝트를 실제 저 위치( World Space )에 소환해주는 거다
 
이제 여기서 TakeDamage()가 있는 Enemy를 건드려주면 되는 건데 다음과 같다

//Enemy.cs
using UnityEngine;
using TMPro;
using UnityEngine.UI;
using System.Collections.Generic;

public class Enemy : MonoBehaviour
{
    public float speed = 1;
    Rigidbody2D rigid;
    public float maxHp = 100f;
    public float hp = 100f;
    public Canvas damageTextCanvas;
    public Canvas enemyHpSliderCanvas;

    // 데미지 텍스트 오브젝트
    private List<GameObject> tmpObject = new List<GameObject>();
    private List<TextMeshProUGUI> tmpText = new List<TextMeshProUGUI>();
    private List<float> textCreationTime = new List<float>();
    private List<Vector3> initialPosition = new List<Vector3>();
    // hp슬라이더 오브젝트
    private GameObject sliderObject;
    private Slider slider;


    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Start()
    {
        // 데미지 텍스트 오브젝트, Canvas 찾기
        GameObject canvasObject1 = GameObject.Find("DamageTextCanvas");
        damageTextCanvas = canvasObject1.GetComponent<Canvas>();
        // hp슬라이더 오브젝트, Canvas 찾기
        GameObject canvasObject2 = GameObject.Find("EnemyHpSliderCanvas");
        enemyHpSliderCanvas = canvasObject2.GetComponent<Canvas>();

        rigid = GetComponent<Rigidbody2D>();
    }

    // Update is called once per frame
    void Update()
    {
        if (Mathf.Abs(rigid.linearVelocity.x) < speed) {
            rigid.AddForce(new Vector2(Mathf.Sin(Time.deltaTime * 360 * Mathf.Deg2Rad), 0), ForceMode2D.Impulse);
        }

        // 오브젝트 업데이트
        for (int i = 0; i < tmpObject.Count; i++) {
            UpdateFloatingTextPosition(i);
        }
        UpdateSliderPosition();
    }

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

        CreateFloatingText(damage);
        CreateHpSlider();

        slider.value = hp / maxHp;

        // HP가 0 이하일 경우 에너미 오브젝트 삭제
        if (hp <= 0) {
            gameObject.layer=0;
            Destroy(gameObject,1f);
            if (sliderObject!=null && slider!=null) {
                Destroy(sliderObject);
            }
        }
    }

    private void CreateFloatingText(float damage)
    {
        GameObject newObject = new GameObject("DynamicTMPText");
        newObject.transform.SetParent(damageTextCanvas.transform, false);
        TextMeshProUGUI newText = newObject.AddComponent<TextMeshProUGUI>();

        newText.text = $"{damage}";
        newText.fontSize = 1;
        newText.alignment = TextAlignmentOptions.Center;

        Vector3 worldPosition = this.transform.position + new Vector3(0, 1, 0);
        RectTransform rectTransform = newObject.GetComponent<RectTransform>();
        rectTransform.sizeDelta = new Vector2(300, 100);
        rectTransform.position = worldPosition;
        rectTransform.position = new Vector3(rectTransform.position.x, rectTransform.position.y, -5f);


        tmpObject.Add(newObject);
        tmpText.Add(newText);
        initialPosition.Add(rectTransform.position);
        textCreationTime.Add(Time.time);


        Destroy(newObject, 1.0f);
    }


    private void UpdateFloatingTextPosition(int index)
    {
        if (tmpObject[index] != null && tmpText[index] != null) {
            float elapsedTime = Time.time - textCreationTime[index];
            Vector3 updatedPosition = tmpObject[index].transform.position;
            updatedPosition.y = initialPosition[index].y + Mathf.Log(elapsedTime * 2 + 1);
            tmpObject[index].transform.position = updatedPosition;

            Color color=tmpText[index].color;
            color.a = Mathf.Clamp01(1 - elapsedTime);
            tmpText[index].color = color;
        }
    }

    private void CreateHpSlider()
    {
        if (sliderObject!=null) {
            UpdateSliderPosition();
            return;
        }
        sliderObject = new GameObject("HpSlider");
        sliderObject.transform.SetParent(enemyHpSliderCanvas.transform, false);
        slider = sliderObject.AddComponent<Slider>();

        RectTransform rectTransform = sliderObject.GetComponent<RectTransform>();
        rectTransform.sizeDelta = new Vector2(160f, 20f);
        rectTransform.position = this.transform.position+new Vector3(0f,1f,0f);
        rectTransform.localScale = new Vector3(0.02f,0.02f,1f);

        slider.minValue = 0;
        slider.maxValue = 1;
        slider.value = hp / maxHp;
        slider.direction = Slider.Direction.LeftToRight;

        // Slider의 Background 설정
        GameObject background = new GameObject("Background");
        background.transform.SetParent(sliderObject.transform, false);
        Image bgImage = background.AddComponent<Image>();
        RectTransform bgRect = background.GetComponent<RectTransform>();
        bgRect.sizeDelta = new Vector2(160f,10f);
        bgImage.color = Color.white; // 기본 배경 색상 (변경 X)

        // Slider의 Fill Area 설정
        GameObject fillArea = new GameObject("Fill Area");
        fillArea.transform.SetParent(sliderObject.transform, false);
        RectTransform fillAreaRect = fillArea.AddComponent<RectTransform>();
        fillAreaRect.anchorMin = new Vector2(0, 0.5f);
        fillAreaRect.anchorMax = new Vector2(1, 0.5f);
        fillAreaRect.offsetMin = new Vector2(0, -5);
        fillAreaRect.offsetMax = new Vector2(0, 5);

        GameObject fill = new GameObject("Fill");
        fill.transform.SetParent(fillArea.transform, false);
        Image fillImage = fill.AddComponent<Image>();
        fillImage.color = Color.red; // Fill 색상 설정
        RectTransform fillRect = fill.GetComponent<RectTransform>();
        fillRect.anchorMin = Vector2.zero;
        fillRect.anchorMax = Vector2.one;
        fillRect.offsetMin = Vector2.zero;
        fillRect.offsetMax = Vector2.zero;

        slider.fillRect = fillRect; // Slider에 Fill 연결

        // Slider의 Handle Slide Area 비활성화
        GameObject handleSlideArea = new GameObject("Handle Slide Area");
        handleSlideArea.transform.SetParent(sliderObject.transform, false);
        handleSlideArea.SetActive(false);


        Destroy(sliderObject, 10f);
    }

    private void UpdateSliderPosition()
    {
        if (sliderObject!=null && sliderObject!=null) {
            sliderObject.transform.position=this.transform.position+new Vector3(0f,1f,0f);
        }
    }
}

Canvas는 Uniy editor에서 직접 끌어다 넣어주면 되는 거고
mpObjet는 리스트, 스크립트에서 생성한 text 오브젝트를 index에 따라 저장
그런데TextMeshProUGUI가 컴포넌트로 설정된
 
textCreationTime이랑 initialPosition, UpdateFloatingTextPosition()은 텍스트 생성 후
좀 이쁘라고 넣어주는 거니까 굳이 필요 없으면 빼주자
 
코드 보다보니까 궁금해진건데 slider는 리스트로 관리 안하는데 어케 관리하는 거지
어케 잘 되는갑다. 신기하네
어쨌든 slider도 똑같은데, slider에는 설정해줄 하위 오브젝트들이 많으니까 rectTransform 활용법만 제대로 알고 가자

오늘도 한층 뭐가 더 많아진 Hierarchy

이제 할 거

  • 한글 폰트 적용
  • 대화에 선택지 추가, 퀘스트 추가
  • 인벤토리, 아이템 추가
  • 스킬 쿨타임, HP, MP UI(시스템) 추가
  • 스킬 봉인, 해금 추가

 
+ 약간의 수정으로 좀 더 까리나게 만들 수 있다

//Enemy.cs
public void TakeDamage(float damage, int hitTime=1) //hitTime의 기본값은 1
{
    StartCoroutine(ExecuteDamageHits(damage, hitTime));
    // 그 외 생략...
}
// ...
private IEnumerator ExecuteDamageHits(float damage, int hitTime) {
    for (int i = 0; i < hitTime; i++) {
        CreateFloatingText(damage, i*0.5f);
        yield return new WaitForSeconds(0.1f); // 0.1초 간격으로 실행
    }
}

CreateFloatingText()가 있는 자리를 StartCoroutine으로 바꿔주고, ExecuteDamageHits를 선언해주고
skillHandler에서 약간의 설정을 바꿔주면

이거 완전 그건데