유니티

[멋쟁이사자처럼 유니티 TIL] UI 슬라이더, 체력바 구현 (+ 델리게이트(delegate), 이벤트)

맛난과자 2024. 6. 11. 23:21

슬라이더

 

● 생성 방법

Hierarchy 에서 마우스 오른쪽 클릭  → UI → Slider 를 클릭

 

 


● 구성 요소

 

슬라이더를 생성하면 자식 개체 여러 개가 함께 생성된다.

 

- Background : 슬라이더를 비활성화 시켰을 때의 이미지. 슬라이더의 배경.

 

- Fill Area : 슬라이더를 활성화 시켰을 때의 이미지. Background를 채우는 이미지.

 

- Handle Slide Area : 슬라이더의 손잡이. 슬라이더를 마우스로 직접 작동시킬 때 필요한 버튼.

 

 


● 구현 예시

 

이동 속도를 조절하는 바를 만들어보자

 

① Slider를 생성한다. 이때 자동으로 Canvas도 함께 생성된다.

 

② 원하는 위치로 앵커를 설정한다. 본인의 경우 하단 가운데에 위치하도록 center의 bottom으로 지정하였다.

 

③ 슬라이더가 원하는 크기가 되도록 Width, Height 나 Scale을 설정한다.

이때 자식 개체가 아닌, 최상단에 있는 부모 객체의 크기를 먼저 수정하자.

그렇지 않으면 크기를 아무리 바꿔도 원하는 결과가 나오지 않는 불상사가 발생할 것이다.

나도 알고 싶지 않았다....

 

④ Background, Fill, Handle에 원하는 이미지를 넣는다.

이때 테두리 끝 부분을 깔끔하게 다듬고 싶다면,

Image Type을 Sliced로 바꾼 후 Pixels Per Unit Multiplier의 값을 올린다.

위: 적용 후 / 아래: 적용 전

 

⑤ 스크립트 컴포넌트를 생성 후, 아래와 같이 코드를 작성한다.

public PlayerController player;
private Slider slider;

void Start()
{
    slider = GetComponent<Slider>();
    
    // AddListener 기능으로 슬라이더 작동 시 함수 호출
    slider.onValueChanged.AddListener(OnValueChanged);
}

void OnValueChanged(float value)
{
    // 슬라이더의 값을 변수에 넣음
    player.moveSpeedFacter = value;
}

 

 

- onValueChanged

: 'Slider' 클래스의 이벤트. 슬라이더 값이 변경될 때마다 발생한다. 이 이벤트에 메서드를 추가하면 슬라이더 값이 변경될 때마다 그 메서드가 호출된다. 'UnityEvent<float>' 타입이라 추가하는 메서드가 받는 매개변수는 float 타입이어야 한다.

 

- AddListener

: 'UnityEvent' 클래스의 메서드. 이벤트에 리스너(메서드)를 추가하는 역할을 한다.

 

※ 인스펙터 창에서 On Value Changed에 +를 눌러 함수를 직접 추가할 수도 있지만, 비추천

※ onValueChanged에 에러가 뜬다면 using에 UIElements -> UI로 수정

 

 

⑥ 플레이어 스크립트에 아래 코드를 추가한다.

//외부에서 세팅은 가능하나 값을 읽어가진 못하게 막음
public float moveSpeedFacter { private get; set; }
    
// 함수로 막는 방법
// public float GEtMoveSpeedFactor()
// {
//     return moveSpeedFacter;
// }

private void Awake()
{
    moveSpeedFacter = 0.0f;
}

void FixedUpdate()
{
    if (direction != Vector3.zero) 
    {
        // 기존의 speed에 moveSpeedFactor(0 ~ 1) 값 곱함 
        Vector3 nextPosition = Vector3.MoveTowards(transform.position, transform.position +
        direction * 1000.0f, speed * moveSpeedFacter * Time.fixedDeltaTime);

        GetComponent<Rigidbody>().MovePosition(nextPosition);
    }
}

 

 

 

결과

 

스피드 바에 따라 이동 속도가 달라지는 것을 확인할 수 있다.

 

 


 

 

체력바 구현

 

강사님은 깔끔한 UI를 위해 슬라이더 대신 mask를 사용하여 구현하는 법을 알려주셨다.

 

① 이미지를 생성하여 안에 이미지와 텍스트를 넣고, 이미지 안에 또 다른 이미지를 넣는다.

 

HpBar : 배경 역할

maskHp : mask 역할. 이 객체의 사이즈를 조절하여 게이지를 표현할 것 이다.

Image : Fill Area 역할. 

 

- mask 란?

이미지의 특정 부분만 보이게 하는 것. mask와 겹쳐진 이미지만 보이게 한다.

 

 

② 최상단 부모 객체의 크기와 앵커를 설정한다.

 

③ maskHp와 Image 객체의 앵커를 left, middle로 설정하고, Anchors와 Pivot의 x값을 모두 0으로 설정한다.

게이지가 줄어들 때 오른쪽에서 왼쪽으로 줄어들게 하기 위함이다.

 

④ 스크립트 컴포넌트를 추가하고 아래와 같이 입력한다.

public Image _background;
public Image _mask;
public TextMeshProUGUI _hpStringState;
public PlayerController playerController;
// 5초가 가장 쫀득한 것 같다
public float lerpSpeed = 5f;
    
void Update()
{
    UpdateHpBarStatus();
}

void UpdateHpBarStatus()
{
    float currentHp = playerController.currentHp;
    float maxHp = playerController.maxHp;

    _hpStringState.text = string.Format("{0} / {1}", currentHp, maxHp);

    // mask의 height는 변하지 않음.
    float height = _mask.GetComponent<RectTransform>().sizeDelta.y;

    // background의 width값이 x가 최대 크기이므로 fullWidth라 명명.
    float fullWidth = _background.GetComponent<RectTransform>().sizeDelta.x;
	
    // 목표값
    float targetWidth = (currentHp / maxHp) * fullWidth;
    // 현재 값
    float currentWidth = _mask.GetComponent<RectTransform>().sizeDelta.x;
        
    // 게이지 부드럽게 이동
    float newWidth = Mathf.Lerp(currentWidth, targetWidth, Time.deltaTime * lerpSpeed);

    _mask.GetComponent<RectTransform>().sizeDelta = new Vector2(newWidth, height);
}

 

 

- sizeDelta 

: RectTransform의 속성. Canvas와의 크기 차이.

부모 앵커가 중심일 경우, 자식 RectTransform의 실제 크기 (rect.size = sizeDelta)

부모 앵커가 자식의 모서리에 있을 경우, 기본에서 추가되거나 빠지는 값 (sizeDelta.x = - Canvas.width + rect.width)

 

주로 자식 요소의 크기를 동적으로 변경할 때 사용한다.

 

rect.size는 RectTrasform의 실제 크기. 읽기 전용이라 직접 설정할 수 없다.

 

 

- Lerp

: 선형보간법. 이동을 부드럽게 하기 위해 많이 쓰인다.

 

위 코드의 경우 lerpSpeed는 값이 그대로이지만 Time.deltaTime 값이 변하면서 부드럽게 움직이게 된다.

Time.deltaTime이 점점 증가. (Time.deltaTime 0.016초 * lerpSpeed 5 = 0.08)

 

변수(lerpSpeed)의 값이 클수록 빠르게 도착한다. => 즉각적인 반응 / 부드러움이 줄어듬

변수의 값이 작을수록 느리게 도착한다. => 애니메이션이 부드럽고 자연스러움 / 반응 느림

 

'쫀득함'은 애니메이션이 부드러우면서도 즉각적으로 반응하는 상태를 의미하므로, 적절한 값을 찾는 것이 중요하다.

고 챗지피티가 말했다.

 

 

⑤ 인스텍터 창에서 스크립트에 해당 객체들을 바인딩해준다.

 

 

 

결과

 

체력이 깎일 때마다 체력바가 부드럽게 줄어드는 것을 보이면 성공이다.

 

 

캐릭터 위에 체력바 구현

체력바가 캐릭터 위를 따라다닌다.

 

위의 방식과 동일하게 하되, 캐릭터의 자식개체로 Canvas를 넣는다.

Render Mode를 World Space로 설정하고 

Event Camera에 Main Camera를 넣어주면 된다.

 

 


 

 

델리게이트(delegate)

: 대리자. 같은 시그니처(매개변수와 반환 타입)을 가지는 메서드에 대한 참조 역할을 한다.

즉, 무언가를 대신하는 형식이다.

 

주로 이벤트 처리나 콜백, 또는 이를 통한 UI 업데이트 등에 사용한다.

메서드 자체를 매개변수로 넘겨주고 싶을 경우 사용한다.

 

 

● 사용법

 

 

- 형식

 

지정자 delegate 반환형 델리게이트변수 (매개변수)

public delegate void ExampleDelegate(float value1, int value2);

 

 

'+=''-=' 연산자를 이용하여 델리게이트에 메서드를 추가하거나 제거할 수 있다.

연산자 오버로딩과는 관련이 없다.

DelegateExample delegateEx = new DelegateEx(ExampleMethod1);  // 객체로 선언
delegateEx += ExampleMethod2;  // 델리게이트에 메서드 구독

 

구독 (Subscription)

: 델리게이트 또는 이벤트에 메서드를 추가하여 해당 델리게이트나 이벤트가 호출될 때 메서드가 실행되도록 하는 것.

말그대로 하나의 채널에 구독하는 느낌

 

델리게이트 체이닝(Chaining)

: 여러 메서드를 델리게이트에 추가하여 순차적으로 호출되도록 하는 것.

델리게이트에 여러 메서드를 묶어놓은 느낌

이벤트

 

이벤트에 메서드 추가 방법

 

 

AddListener 

 

: 이벤트에 메서드를 추가하는 메서드

slider.onValueChanged.AddListener(OnValueChanged);

 

 

Unity 에디터

 

특수한 경우, 유니티 에디터에서 이벤트 핸들러를 설정할 수 있다.

UI의 경우, 인스펙터 창에서 이벤트 리스너를 추가할 수 있다.

 

 

Delegate

 

델리게이트에 원하는 이벤트를 구독함으로써 이벤트를 발생시킬 수 있다.

public delegate void ValueChangedDelegate(float value);  // 델리게이트 정의
public event ValueChangedDelegate OnValueChanged;  // 이벤트 정의

OnValueChanged += MyValueChangedMethod;  // 이벤트 델리게이트에 구독

OnValueChanged?.Invoke(newValue);  // 이벤트 발생

 

이 외에도 다양한 방법이 있다고 한다.

 

이를 이용하여 체력바를 다시 구현해보자

 


 

 

델리게이트와 이벤트를 이용한 체력바 구현

 

● 체력바 스크립트

[SerializeField]
private TextMeshProUGUI hpText;

[SerializeField] private RectTransform maskRect;
[SerializeField] private RectTransform backgroundRect;
[SerializeField] private PlayerController playerController;
private float maxWidth;
private float maxheight;

private void Awake()
{
    maxWidth = backgroundRect.sizeDelta.x;
    maxheight = backgroundRect.sizeDelta.y;
        
    // 델리게이트 메서드 구독 -> hpStatusBroadCast 호출될 때마다 UpdateHpStatus 메서드 호출
    playerController.hpStatusBroadCast += UpdateHpStatus;
}

public void UpdateHpStatus(float currentHp, float maxHp)
{
    float factor = 1.0f; 
        
    if (maxHp != 0.0f)
    {
        factor = currentHp / maxHp;
    }
        
    maskRect.sizeDelta = new Vector2(factor * maxWidth, maxheight);
}

 

 

● 플레이어 스크립트

private float CurrentHp;
private float MaxHp;
    
// hp바가 변했을 때 정보를 얻고자 하는 것들 다 부름
// 델리게이트의 인스턴스 생성
public HpStatusBroadCast hpStatusBroadCast;
    
[field: SerializeField]
public float currentHp
{
    get => CurrentHp;
    set
    {
        CurrentHp = value; 
        // currentHp 가 새로운 값으로 설정될 때마다 델리게이트 호출되어
        //현재 hp와 최대 hp를 매개변수로 전달함.
        hpStatusBroadCast?.Invoke(currentHp, maxHp);
    }
}

[field: SerializeField]
public float maxHp
{
    get => MaxHp;
    set
    {
        MaxHp = value;
        hpStatusBroadCast?.Invoke(currentHp, maxHp);
    } 
}

void Start()
{
    currentHp = 100;
    maxHp = 100;
}

 

 


 

 

 

델리게이트... 솔직히 아직 잘 모르겠다.

강사님이 내일 자세히 알려주신다고 하셨다. 

내일은 델리게이트 마스터할 수 있겠지?