(주말공부)XR 플밍 - (13) 유니티 UI 구현하기 - 체력 바, 물체에 갖다대면 뜨는 커서 구현하기(4/26)

이형원·2025년 4월 26일
0

XR플밍

목록 보기
55/215
post-thumbnail

0. 들어가기에 앞서

다음주부터 진행 예정인 유니티 프로젝트에서 필요한 기능 구상 및 일부는 구현, 테스트 해 보는 시간을 갖기로 했다.

5명으로 구성된 팀 프로젝트로 제작할 게임은 탈출을 목적으로 하는 공포게임 류이다.
각각의 팀원에게 구성해야 할 과제를 미리 배분했으며, 내가 맡은 부분의 내용은 이러했다.

[트리거 이벤트 및 인게임 UI 담당]

1. 문(열린 상태)을 통해서 맵 이동을 한다거나, 구간별 트리거 이벤트
2. 스위치(문과 멀리 떨어져 있음)를 눌러서 문을 열고(e.g. 문 열림 vs 철창 열림) 탈출
3. 1차 목표는 맵을 뒤지고 탈출 문을 찾아서 탈출
4. 게임 UI(꾸밀지? 안꾸미고 할지?)
- 체력바
- 스테미너바
- (마우스 센터의 아이콘?)
- 아이템에 마우스 가져가면(거리에 따라서) 아이템 이름, 인터렉션 단축키 (표시 UI)
- 아이템 툴팁 UI : 거리 기반 fade-in/out이 중요, Canvas World Space + 거리 계산
* 다 완료되면 고려 사항 : World Streaming(지나간 곳은 일정 거리 이상 떨어지면 unload)

생각보다 쉽지 않은 업무를 맡게 되어 난감했다. 특히나 UI라는 특성상 코드로 구현해야 하는 부분 뿐만 아니라 디자인이나 시각적으로 보이는 부분까지 신경 써야 하다 보니, 작업이 생각보다 오래 걸릴 것도 염두에 둬야 했다.
(아트팀이 따로 없는 프로그래밍 분반이다 보니, 디자인 부분은 아무래도 에셋에 의존해야 할 수밖에 없다.
그래서 미리 준비할 생각을 하고 있었지만, 에셋을 구하는 시간과 기본적인 기반이 되는 테스트용 코드를 미리 작성해보고자 하였다.

1. 설계 단계 작성

원래 실무에서의 설계 단계는 상급자가 전체적으로 틀을 만들어 팀원들이 해당 업무를 수행하는 방식으로 이루어지는 게 일반적이라고 한다.
하지만 이번 팀 프로젝트의 경우에는 팀장의 요청으로 각자 맡은 바에 관해서 각자가 설계 단계를 먼저 만들어보길 권했다.

지금 단계에서는 아직 구체적으로 정해진 내용이 적고 아무래도 브레인스토밍 정도로 끝난 상태였기 때문에, 구체적으로 작성하기에는 어려움이 있었다.
가능하다면 클래스 다이어그램으로 설계 단계를 작성해보려고 했지만, 생각보다 내용 작성이 어려웠다. 하지만 코드로 작성할 수 있는 내용으로 우선 설계단계를 작성해 보았다.

위 내용에 따라 할 일을 생각해 보자.

  • Switch
  1. IInteractable 인터페이스의 경우는 아무래도 아이템 클래스와 같이 쓰게 되는 인터페이스가 될 것 같으며, 이를 Switch와 같이 쓰게 하여 기능을 구현해야 할 것이다. 이 부분은 아이템 담당자와 인터페이스에 대한 상의가 필요하다.

  2. 그러면 지금 개인적으로 해결할 수 있는 문제는 문을 열 수 있는 효과적인 방법을 찾는 것이다. 또한, 개인적인 아이디어로 한 번에 여러 문의 작동을 제어하는 스위치를 만든다면, 이를 이용해 퍼즐 요소를 만들 수 있을 것이라 생각했다. 해당 기능을 구현하기 위한 생각이 필요하다.

  • Trigger Event
  1. 이 부분이 현재 내용 중 제일 상의가 많이 필요한 부분일 것이다. 구체적으로 어디에 무슨 이벤트를 발생시킬 건지에 대한 것을 결정하기 위해선, 우선 맵과 스테이지 디자인이 완성되어야 한다. 아무래도 미리 작업하기에는 어렵고, 어떤 기능을 구현해야 할지 모호하기 때문에 우선은 저렇게만 내용을 적었다.

  2. 생각할 수 있는 이벤트는, 특정 구간을 넘기면 나타나는 몬스터 혹은 특정 구간에서 갇히게 하여 몬스터와의 추격전을 벌이는 등의 일이다.

  • Item 표시 UI
  1. 우선은 회의 단계에서 해당 내용을 아이템을 찾으러 커서를 갖다댈 때 해당 아이템의 이름을 표시하는 UI를 표시하라는 의도로 받아들였다. 즉 마우스 커서의 좌표를 이용해 UI를 알맞게 띄워야 하는 과제다.

  2. 당장 작업할 수 있는 건 마우스 커서를 갖다댔을 때 적절한 위치에 UI를 띄우는 것이다. 또한 아이템 담당자와 잘 확인해서, 아이템 이름 및 상세정보를 받아 출력할 수 있는 방법을 고민해 봐야 한다.

  • Status UI
  1. 미리 손을 대 볼 작업 중에서는 그나마 쉬워보이는 축으로 보인다. 이미 닷지 때 HP바를 구현해본 바가 있고, 다만 UI 부문에서 조금 더 신경써야 할 것으로 보인다.

  2. 닷지 때와는 다른 방식으로 구현해보고자 한다. 좀 더 그래픽적으로 어울리는 UI를 만들기 위해, 어울리는 에셋을 사용하거나 직접 만드는 방식 또한 고민해보고 있다.

이와 같이 내용을 정리하고서 사전에 구현해본 바를 정리해보고자 한다.

2. Status UI 구현 및 테스트

인게임 Status UI로서 구현해보려고 하는 디자인에 관해서는, 사실 이미 아이디어가 있었다.

2월달에 수강했던 4주짜리 온라인 Unity 강의에서, 2D 슈팅 게임에서의 HP바를 구현했던 방식이 있었고 그 디자인이 상당히 좋다고 생각했었다.
그 당시 작성했던 코드를 그대로 갖다쓴다는 생각 대신, 코드도 정리해 보고 이런 기술도 있었구나 리뷰하는 방식으로 다시 테스트 해 보고자 한다.

2.1 UI 이미지 만들기

우선 해당 UI를 구현하기 위해 준비물이 필요하다. 아래와 같이 그림판으로 UI 이미지 두 장을 만들었다.

  • Status 백그라운드 UI

  • Status Inner Ui

백그라운드 Ui로 틀을 만들어주고, 안의 Inner UI에 스크립트를 붙여 색과 게이지 표시를 할 것이다.
다만 이렇게 디자인했지만 곤란하게도, 그림판으로 작성하니 저 하얀색으로 표기된 배경이 투명 처리가 아니라 흰색 처리가 되는 것이다.

(이렇게 되면 안되는데...)
UI를 만들기 위해서는 배경 투명화 작업을 해야 할 것 같다. 포토샵이나 그런 그림 편집 프로그램은 없다 보니 다른 방법이 없나 많이 걱정을 했는데, 다행히도 웹사이트로 배경 지워주는 사이트 같은 것이 쉽게 떴다.

이를 이용해 아래와 같이 UI 이미지 두 개를 만들 수 있었다.

  • Status 백그라운드 UI
  • Status Inner Ui

이제 이 두 이미지를 넣어서 UI를 만들어 보자. 여기서 유니티 UI에 외부 이미지를 쓸 때의 유의사항에 대해 언급하고자 한다.

예시로 등록해본 이미지를 살펴 보면 이와 같이 초기설정이 되어있다. 하지만 이 상태로는 UI 이미지로 변환이 불가능하다.

보다시피 Image는 Sprite 형식으로 받기 때문에, 저 이미지 그대로 사용해선 안되고 Sprite로 변환해야 한다.

스프라이트로 적용한 후 아래에서 Apply 버튼까지 눌러주면 UI 이미지로 사용할 수 있는 Sprite로 변환이 가능하다.


이제 UI를 틀에 맞게 잘 만들어주면 된다.

2.2 UI 스크립트 만들기

이제 Inner이미지에 스크립트를 추가해주면 된다. 간단하게 HealthUI를 추가해준다.
UI와 관련된 작업을 해야하므로, using UnityEngine.Ui를 추가해준다.

스크립트 내용은 아래와 같이 작성하였다.

using UnityEngine;
using UnityEngine.UI;

public class HealthUI : MonoBehaviour
{
    Image hpImage;

	// 플레이어의 체력은 나중에 플레이어 제작자와 협의해 연결해야 하지만,
    // 우선 테스트용으로 진행하였다.
    // 또한 HP가 float일 필요는 없지만 바를 통해 조절해보는 시도를 위해 임시로 float로 표기했다.
    [SerializeField] float playerMaxHP;
    [Range(0f, 10f)]
    [SerializeField] float playerCurrHP;
    Color HPColor;

    void Awake()
    {
        hpImage = GetComponent<Image>();
    }

    void Update()
    {
        float toFill;
        toFill = playerCurrHP / playerMaxHP;

        HPColor.r = 1 - toFill;
        HPColor.g = toFill;
        HPColor.b = 0;
        HPColor.a = 1;

        hpImage.fillAmount = toFill;
        hpImage.color = HPColor; 
    }
}

이와 같이 세팅한 후 인스펙터 창에서도 세팅을 해야 한다.


이미지 초기 세팅은 위와 같이 되어 있지만 Image Type를 바꿔야 한다. 이미지 타입을 Filled로 바꿔주면 아래와 같이 추가적인 세팅이 가능하다.

내가 선택한 방법은 Filled 방식으로 Horizontal - 가로형, 그리고 왼쪽부터 차는 방식을 선택했다.

이와 같이 세팅하고 UI를 작동시키면 아래와 같이 출력된다.

3. 특정 물건에 마우스를 갖다대면 UI 출력

유니티에서 아이템과 상호작용하고, 아이템에 다가갔을 때 '특정 키로 집으세요'를 출력하는 UI를 만드는 간단한 방법으로서 자료를 공유받았다. 하지만 단순하게 이것만 따라서 만드는 방법보다는, 좀 더 코드적으로 좋게 구현한 내용이면서 더 시각적으로 어필할 수 있는 UI를 만들고 싶었다.

그래서 내가 목표로 한 것으로 아래와 같은 특징을 가진 UI를 만들고자 하였다.

  • 마우스 커서를 갖다대면 그 마우스 끝을 따라서 이동하는 UI를 만들어보자
  • 그 상태에서 특정 키를 눌렀을 경우 UI를 통해 아이템 상세 정보를 만들 수 있는 UI를 만들어보자.

아이템과 관련된 부분이 아직 구현되지 않았고, 아이템 담당자와 협의해야 할 부분이겠지만 UI 부분을 구현하는 건 당장 할 수 있는 과제였다.
이에 따라 많은 자료를 찾아보기 시작했다.

3.1 RectTransformUtility.ScreenPointToLocalPointInRectangle

해당 내용에 대해 알아보기 앞서 Canvas는 Screen Space-Camera로 세팅하자

커서의 움직임을 캐치해 UI를 띄우는 방법에서 거의 대부분이 이 메소드를 사용했다는 걸 알게 되었다.
이것이 뭔지 한 번 알아보도록 하자.

유니티 메뉴얼에서는 해당 내용을 이렇게 설명하고 있다. 그 자체적으로는 bool값을 가지며, Vector2 값을 반환하는 함수이다.

이걸 이용해서 초기에 구현했던 코드는 아래와 같았다.

public GameObject panel;
RectTransform panelpos;

public void Awake()
{
    panelpos = panel.GetComponent<RectTransform>();
}

private void OnMouseOver()
{ 
panel.SetActive(true);
Vector3 point = Camera.main.ScreenToWorldPoint(new Vector3(Input.mousePosition.x,
        Input.mousePosition.y, -Camera.main.transform.position.z));  
Vector2 localPos = Vector2.zero; 
RectTransformUtility.ScreenPointToLocalPointInRectangle(panelpos, Input.mousePosition, Camera.main, out localPos
panelpos.anchoredPosition = localPos;
}

private void OnMouseExit()
{
    panel.SetActive(false);
}

하지만 이렇게 구현하니, UI가 마우스의 위치는 다소 이상한 거리로 생성되고, 심지어 카메라 메인의 위치가 계속 이상하게 이동하면서 버버벅거리면서 UI가 이동하는 현상이 나타났다.

변수를 고치고 온갖 방법을 동원해봐도 고쳐지지가 않았는데, 한참 뒤에 내용을 구현한 뒤에 생각해보니 원인을 알 것 같았다. 많은 글과 코드를 참고하여 결국 최종 완성한 코드는 아래와 같다.

public class ItemInfoUI : MonoBehaviour
{
	// 캔버스
    public Canvas canvas;
    // 패널
    public GameObject panel;
    RectTransform panelpos;

    public void Awake()
    {
    	// 패널을 통해 패널의 RectTransform 가져오기
        panelpos = panel.GetComponent<RectTransform>();
    }

	// 마우스가 물체에 올려져 있을 경우
    private void OnMouseOver()
    {   
    	// 패널이 켜진다
        panel.SetActive(true);
        
        // localPos 초기화
        Vector2 localPos = Vector2.zero;        

		// canvas의 rectTransform 받음
        RectTransform rectTransform = canvas.transform as RectTransform;
        // mouse의 좌표
        Vector3 mousePos = new Vector3(Input.mousePosition.x, Input.mousePosition.y, 0);

		// 캔버스 좌표 기준, 마우스 위치, 캔버스카메라 기준, 좌표 반환
        RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, mousePos, canvas.worldCamera, out localPos);
        // 패널의 위치는 좌표에 해당
        panelpos.anchoredPosition = localPos;
    }

	// 마우스가 물체에서 떨어지면
    private void OnMouseExit()
    {
    	// 패널이 꺼진다.
        panel.SetActive(false);
    }
}

이와 같이 세팅하고 물체에 스크립트를 붙이면 아래와 같이 출력된다.

(일부로 앞에 장애물을 두고, 장애물과 마우스가 닿았을 때에는 패널이 출력되지 않는 것도 확인하였다.)

3.2 UI가 뜬 상태에서 특정 키를 누르면 상세내용 표시하기

이후 텍스트 변경 내용에 대한 코드는 아래와 같이 간략하게 작성했다.

public class ItemInfoPrinter : MonoBehaviour
{
    bool inputKey = false;
    [SerializeField] TMP_Text textUI;
    void Update()
    {
        InputKeyI();
        if(inputKey)
        {
            textUI.text = "Item Description : ~~~~";
        }
        else
        {
            textUI.text = "Item";
        }
    }

    private void InputKeyI()
    {
        if (Input.GetKeyDown(KeyCode.I))
        {
            inputKey = inputKey? false : true;
        }
    }
}

해당 정보를 입력하기 위해서는 아이템 담당자와 협의를 해서 정보를 넣을 수 있는 방식을 생각해보고자 한다.
또한 UI의 크기를 조절하는 방법에 대해서도 추가적으로 구현 시도를 해 보고자 한다.

3.3 의도치 않은 오류의 원인은?

처음에 작성한 코드대로 작성했을 때에는, 패널이 이상하게 월드 스페이스와 캔버스 스페이스를 자꾸 왔다갔다 했었다. 이걸 가만 생각해보니, 최종 코드와 비교했을 때 놓친 점이 있었다.

월드 스페이스를 비추는 카메라 / 메인 카메라가 있고, 캔버스 필드를 비추는 카메라가 따로 있다.

이것 때문에 캔버스에 표시하는 것 때문에 오류가 발생했다고 생각한다.
캔버스를 비추는 카메라가 따로 있을 거라고는 생각지도 못했는데, 카메라를 바꾸고 나서 해당 문제가 해결되었다.

3.4 Anchor의 이용

처음에 해당 기능을 구현했을 때 문제점이 있었다. 바로 UI가 마우스 중앙에 켜진다는 문제점이었다.
이를 위해 처음에는 mousePos를 조정할 추가 변수를 넣어 진행했었다.

하지만 가만 생각해보니 굳이 좌표를 옮기지 않을 방법이 있었다.

패널의 좌표를 위와 같이 세팅하니, 따로 추가적인 변수 조정 없이 마우스 우측 상단에 UI가 출력되는 것을 확인했다.

profile
게임 만들러 코딩 공부중

0개의 댓글