Unity

[졸업작품, Unity] 결과창 제작

HK1206 2025. 3. 6. 23:19

오늘은 전투가 끝났을 때 보상을 받고 어떤 보상을 받았는지 확인 하는 결과창을 만들었다. 결과물은 다음과 같다. (아직 폴리싱을 안해서 안 예쁨)

 

 

HOW TO? (구현방법)

구현 방법을 설명하자면. 먼저 UI틀을 만들어준다. 사실 변경되는 내용들을 결과 창의 아이템들이다.

방법은 두가지가 있는데 미리 칸을 지정해두고 그 칸의 Image의 Sprite와 Text를 바꾸는 방식 직접 동적으로 Image와 Text를 생성하는 방법이다.

필자는 2번째 방법을 선택하였다.

 

 

CODE (코드 쪽 구현 방법)

결과물과 같지는 않아도 비슷한 형식의 UI를 만든다.

이렇게 생긴 UI다

여튼 만들었다면 Horizontal Layout Group과 Vertical LayOut Group를 활용하여

라인을 Vertical로 그리고 라인 마다 Horizontal를 넣어준다! 중요!!

그럼 알아서 칸 마다 정렬이 될꺼고 라인의 자식으로 이미지를 만든다면 자동으로 정렬 된다.

세팅이다.

 

이렇게 했다면 준비는 끝이다 코드를 작성해보자....

 

using System;
using System.Collections.Generic;
using System.Collections;
using BIS.Data;
using DG.Tweening;
using LJS.Item;
using TMPro;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UI;

namespace LJS.UI
{
    public enum RewardType
    {
        Item, Synergy    
    }
    
    public class RewardPanel : MonoBehaviour
    {
        [Header("Base Setting")]
        [SerializeField] private List<Transform> _lineList = new();

        [SerializeField] private RectTransform _itemCartalog;
        [SerializeField] private ToolTip _toolTipCompo;

        [Header("Icon Setting")] 
        [SerializeField] private float _iconWidth;
        [SerializeField] private float _iconHeight;

        [Header("Text Setting")] 
        [SerializeField] private TMP_FontAsset _font;
        [SerializeField] private float _nameTagWidth;
        [SerializeField] private float _nameTagHeight;
        [SerializeField] private float _fontSize;
        [SerializeField]
        [ColorUsage(true)]
        private Color _textColor;

        [SerializeField] private float _nameTagPosY;
        
        private float _panelWidth;
        private bool isOpen;
        
        public List<ScriptableObject> TestList;

        private void Start()
        {
            isOpen = false;
            _panelWidth = _itemCartalog.rect.width;

            OpenPanel(TestList);
        }

        private void Update()
        {
            if (isOpen &&Keyboard.current.escapeKey.wasPressedThisFrame)
            {
                ClosePanel();
            }
        }

        public void OpenPanel(List<ScriptableObject> list)
        {
            isOpen = true;
            WindowIn(() => StartCoroutine(OpenPanelCoro(list)));
        }

        public void ClosePanel()
        {
            isOpen = false;
            WindowOut();
            _toolTipCompo.ToolTipClose();
        }

        private IEnumerator OpenPanelCoro(List<ScriptableObject> list = null)
        {
            float iconWidthSum = 0;
            int lineNumber = 0;
            // 등장 효과 ex) 위에서 아래로 중아에서 크기 늘리기
            // 아이템 나올 때 효과.. 결과 창에 하나씩 딱딱 등장하는 느낌? 만들기
            foreach (var element in list)
            {
                ItemSOBase item = null;
                SynergySO synergy = null;
                if (element is ItemSOBase)
                    item = element as ItemSOBase;
                else
                    synergy = element as SynergySO;

                iconWidthSum += _iconWidth;
                
                if (iconWidthSum >= _panelWidth - 220)
                    // horizontal layOut Group의 spacing과 vertical // 의 padding 고려 수치. 대략적임 수정 필요
                {
                    iconWidthSum = 0;
                    lineNumber++;
                }

                #region Generate
                
                GameObject icon = new GameObject();
                ToolTipAvailableObject tooltipCompo = icon.AddComponent<ToolTipAvailableObject>();
                RectTransform rectTrm = icon.AddComponent<RectTransform>();
                Image imageCompo = icon.AddComponent<Image>();
                
                imageCompo.sprite = element is ItemSOBase ? item.icon : synergy.ItemIcon;
                rectTrm.sizeDelta = new Vector2(_iconWidth, _iconHeight);

                icon.transform.SetParent(_lineList[lineNumber]);

                GameObject nameTag = new GameObject();

                nameTag.transform.SetParent(icon.transform);
                tooltipCompo.SettingInfo(element, _toolTipCompo); // 기물도 추가해야 됨...

                RectTransform textTrm = nameTag.AddComponent<RectTransform>();
                TextMeshProUGUI textCompo = nameTag.AddComponent<TextMeshProUGUI>();

                textTrm.sizeDelta = new Vector2(_nameTagWidth, _nameTagHeight);
                textTrm.position = new Vector3(0, _nameTagPosY, 0);

                textCompo.font = _font;
                textCompo.text =  element is ItemSOBase ? item.name : synergy.ItemName;
                textCompo.fontSize = _fontSize;
                textCompo.color = _textColor;
                
                #endregion

                yield return new WaitForSeconds(0.3f);
            }
        }
        
        private void WindowIn(Action endEvent)
        {
            // Lerp 이용해서 만들기 클릭 방지도 만들어야 할듯??
            transform.DOMoveY(transform.position.y - Screen.height, 1f)
                .SetEase(Ease.OutCubic).OnComplete(() => endEvent?.Invoke());
            
        }

        public void WindowOut() // 선택지 선택후 불러올 예정?
        {
            transform.DOMoveY(transform.position.y + Screen.height, 1f)
                .SetEase(Ease.OutCubic);
        }
    }
}

 

전에 만들었던 ItemSystem이랑 연결 된다. 일단 먼저 변수부터 살펴보겠다.

 

	[Header("Base Setting")]
        [SerializeField] private List<Transform> _lineList = new();

        [SerializeField] private RectTransform _itemCartalog;
        [SerializeField] private ToolTip _toolTipCompo;

        [Header("Icon Setting")] 
        [SerializeField] private float _iconWidth;
        [SerializeField] private float _iconHeight;

        [Header("Text Setting")] 
        [SerializeField] private TMP_FontAsset _font;
        [SerializeField] private float _nameTagWidth;
        [SerializeField] private float _nameTagHeight;
        [SerializeField] private float _fontSize;
        [SerializeField]
        [ColorUsage(true)]
        private Color _textColor;

        [SerializeField] private float _nameTagPosY;
        
        private float _panelWidth;
        private bool isOpen;
        
        public List<ScriptableObject> TestList;

가장 먼저 아이콘를 생성 해줄 라인들을 리스트로 선언해준다. 그리고 추후 할 연산 때문에 RectTransform 변수를 하나 선언 해준다. 아이콘를 클릭 했을 때 나올 .ToolTip도 선언 해준다. icon의 가로 넓이와 세로 넓이를 변수로 선언해준다.

그 다음은 텍스트 설정이다. 우리는 텍스트도 생성을 해줄 거라서 텍스트의 설정도 변수로 선언 해줄 필요가 있다.

폰트, 가로 세로 넓이, 폰트 크기, 폰트 색깔, 그리고 위치까지 필요하다.

_panelWidth는 Width을 번거롭게 가져오지 않기 위해서 미리 가져오는 용도로 선언하였다. isOpen은 현재 RewardPanel이 열려있는지 안 열려있는지 알려주는 bool 변수다. (TestList는 Test 용도임)

 

	private void Start()
        {
            isOpen = false;
            _panelWidth = _itemCartalog.rect.width;

            OpenPanel(TestList);
        }

        private void Update()
        {
            if (isOpen &&Keyboard.current.escapeKey.wasPressedThisFrame)
            {
                ClosePanel();
            }
        }

        public void OpenPanel(List<ScriptableObject> list)
        {
            isOpen = true;
            WindowIn(() => StartCoroutine(OpenPanelCoro(list)));
        }


        public void ClosePanel()
        {
            isOpen = false;
            WindowOut();
            _toolTipCompo.ToolTipClose();
        }

다음은 Start, Update와 panel이 열리고 닫힐 때 할 코드들이다. 

하나 씩 설명을 하자면

 

시작 할때 isOpen을 false로 바꿔준다. 그리고 _panelWidth에 itemCartalog의 width을 넣어준다. 그리고 OpenPanel을 실행 해주는데 이거는 Debug 용도여서 나중에 OpenPanel은 지울 예정임

Update는 Debug 현재 Panel이 열려 있을 때 Esc를 누른다면 닫게 되는데 지금 Update을 사용하지만 InputSystem을 활용하여 event 동기화 방식으로 변경하는 것이 좋아 보인다.

OpenPanel을 할때 SO(아이템 데이터)들을 매개변수로 받아오고 isOpen변수를 true로 변경해준다. 그리고 WindowIn함수를 실행시켜 Lerp효과을 적용시킨다. 그리고 StartCoroutine으로 icon생성을 시작한다.

Close을 할때는 isOpen을 false로 변경해주고 WindowOut 효과를 적용시킨다. 그리고 ToolTip을 없애준다.

 

        private void WindowIn(Action endEvent)
        {
            // Lerp 이용해서 만들기 클릭 방지도 만들어야 할듯??
            transform.DOMoveY(transform.position.y - Screen.height, 1f)
                .SetEase(Ease.OutCubic).OnComplete(() => endEvent?.Invoke());
            
        }

        public void WindowOut() // 선택지 선택후 불러올 예정?
        {
            transform.DOMoveY(transform.position.y + Screen.height, 1f)
                .SetEase(Ease.OutCubic);
        }

 

WindowIn과 Out은 DotTween을 활용하여 Y축 이동의 Lerp 효과를 적용시켰다. WindowIn 같은 경우 자기 포지션에 스크린의 크기만큼 빼서 화면에 딱맞게 이동하도록 한다. 그리고 받아온 함수를 실행시켜준다. WindowOut은 반대로 해주면 된다. Ease효과는 다음 문서를 참고하면 좋다.

https://easings.net/ko

 

Easing Functions Cheat Sheet

Easing functions specify the speed of animation to make the movement more natural. Real objects don’t just move at a constant speed, and do not start and stop in an instant. This page helps you choose the right easing function.

easings.net

 

	private IEnumerator OpenPanelCoro(List<ScriptableObject> list = null)
        {
            float iconWidthSum = 0;
            int lineNumber = 0;
            // 등장 효과 ex) 위에서 아래로 중아에서 크기 늘리기
            // 아이템 나올 때 효과.. 결과 창에 하나씩 딱딱 등장하는 느낌? 만들기
            foreach (var element in list)
            {
                ItemSOBase item = null;
                SynergySO synergy = null;
                if (element is ItemSOBase)
                    item = element as ItemSOBase;
                else
                    synergy = element as SynergySO;

                iconWidthSum += _iconWidth;
                
                if (iconWidthSum >= _panelWidth - 220)
                    // horizontal layOut Group의 spacing과 vertical // 의 padding 고려 수치. 대략적임 수정 필요
                {
                    iconWidthSum = 0;
                    lineNumber++;
                }

                #region Generate
                
                GameObject icon = new GameObject();
                ToolTipAvailableObject tooltipCompo = icon.AddComponent<ToolTipAvailableObject>();
                RectTransform rectTrm = icon.AddComponent<RectTransform>();
                Image imageCompo = icon.AddComponent<Image>();
                
                imageCompo.sprite = element is ItemSOBase ? item.icon : synergy.ItemIcon;
                rectTrm.sizeDelta = new Vector2(_iconWidth, _iconHeight);

                icon.transform.SetParent(_lineList[lineNumber]);

                GameObject nameTag = new GameObject();

                nameTag.transform.SetParent(icon.transform);
                tooltipCompo.SettingInfo(element, _toolTipCompo); // 기물도 추가해야 됨...

                RectTransform textTrm = nameTag.AddComponent<RectTransform>();
                TextMeshProUGUI textCompo = nameTag.AddComponent<TextMeshProUGUI>();

                textTrm.sizeDelta = new Vector2(_nameTagWidth, _nameTagHeight);
                textTrm.position = new Vector3(0, _nameTagPosY, 0);

                textCompo.font = _font;
                textCompo.text =  element is ItemSOBase ? item.name : synergy.ItemName;
                textCompo.fontSize = _fontSize;
                textCompo.color = _textColor;
                
                #endregion

                yield return new WaitForSeconds(0.3f);
            }
        }

 

복잡해 보이는데 생각보다 간단하다.

변수부터 말하자면 iconWidthSum는 말그대로 icon들의 Width의 합이고 IineNumber는 현재 icon을 생성하는 line의 번호이다.

먼저 foreach를 돌면서 모든 SO데이터를 확인한다.

그리고 현재 아이템(포션)을 주는지 기물을 주는 구분을 하기 위해서 is 연산자를 활용하였다.(https://learn.microsoft.com/ko-kr/dotnet/csharp/language-reference/operators/is)

아이템이라면 element 현재 SO를 as 연산자를 통해 Item으로 변경하고 기물이라면 synergy로 변경해준다.

그리고 Sum에 icon Width 만큼 더해준다.

if (iconWidthSum >= _panelWidth - 220)
    // horizontal layOut Group의 spacing과 vertical // 의 padding 고려 수치. 대략적임 수정 필요
{
    iconWidthSum = 0;
    lineNumber++;
}

이 부분이 라인을 변경하는 기준을 정해주는 코드인데 그냥 Width을 넘는다고 변경하는 것이 아닌 LayOutGroup에 padding과 spacing을 고려해서 구현해야 하는데... 사실 너무 하드 코딩한 감이 있긴하다. 코드를 LayOutGroup를 받아와 계산하는 식으로 개선이 필요해 보인다.

여튼 Sum이 _panelWidth를 넘는다면 Sum을 0으로 초기화 하고 lineNumber를 늘려준다. 그럼 다음 line에서 생성이 된다...

 

GameObject icon = new GameObject();
ToolTipAvailableObject tooltipCompo = icon.AddComponent<ToolTipAvailableObject>();
RectTransform rectTrm = icon.AddComponent<RectTransform>();
Image imageCompo = icon.AddComponent<Image>();

imageCompo.sprite = element is ItemSOBase ? item.icon : synergy.ItemIcon;
rectTrm.sizeDelta = new Vector2(_iconWidth, _iconHeight);

icon.transform.SetParent(_lineList[lineNumber]);

GameObject nameTag = new GameObject();

nameTag.transform.SetParent(icon.transform);
tooltipCompo.SettingInfo(element, _toolTipCompo); // 기물도 추가해야 됨...

RectTransform textTrm = nameTag.AddComponent<RectTransform>();
TextMeshProUGUI textCompo = nameTag.AddComponent<TextMeshProUGUI>();

textTrm.sizeDelta = new Vector2(_nameTagWidth, _nameTagHeight);
textTrm.position = new Vector3(0, _nameTagPosY, 0);

textCompo.font = _font;
textCompo.text =  element is ItemSOBase ? item.name : synergy.ItemName;
textCompo.fontSize = _fontSize;
textCompo.color = _textColor;

#endregion

이 부분이 생성 부분인데 요약 하면 빈 게임 오브젝트 생성하고 AddCompo 해서 icon을 만들고 빈 게임 오브젝트 만들고 icon의 자식으로 넣고 AddCompo하고 설정 변경해서 Text로 만들면 끝!

 

다음은 ToolTip 부분이다.

using System.Collections.Generic;
using KHJ.SO;
using LJS.Item;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

namespace LJS.UI
{
    public class ToolTipAvailableObject : MonoBehaviour, IPointerDownHandler
    {
        private ItemSOBase _itemInfo;
        private SynergySO _synergyInfo;
        
        private GraphicRaycaster _caster;
        private PointerEventData _pointerData;
        private List<RaycastResult> _raycastResult;
        private ToolTip _toolTipCompo;

        private RewardType _rewardType;

        public void SettingInfo(ScriptableObject item, ToolTip toolTipCompo, Canvas canvas)
        {
            if (item is ItemSOBase)
            {
                _itemInfo = item as ItemSOBase;
                _rewardType = RewardType.Item;
            }
            else
            {
                _synergyInfo = item as SynergySO;
                _rewardType = RewardType.Synergy;
            }
            
            _toolTipCompo = toolTipCompo;
            _caster = canvas.GetComponent<GraphicRaycaster>();
            _raycastResult = new();
        }

        public void OnPointerDown(PointerEventData eventData)
        {
            _pointerData = new PointerEventData(EventSystem.current);
            _pointerData.position = Input.mousePosition;
            _caster.Raycast(_pointerData, _raycastResult);

            if (_raycastResult.Count <= 0) return;
            
            foreach (var hit in _raycastResult)
            {
                if (hit.gameObject == gameObject)
                {
                    // 툴팁 띄우기
                    if(_rewardType == RewardType.Item)
                        _toolTipCompo.ToolTipOpen(_itemInfo.itemName, _itemInfo.description);
                    else
                        _toolTipCompo.ToolTipOpen(_synergyInfo.ItemName, _synergyInfo.ItemDescription);
                }
            }
        }
    }
}

아까 봤던 코드랑 이어진다.

코드를 설명하기 전 IPointerDownHandler에 대해서 알 필요가 있다. 이 인터페이스는 마우스 클릭 이벤트가 작동하였을 때 실행되는 함수를 가진 인터페이스이다. 자세한 설명은 공식문서를 참고하자..

https://docs.unity3d.com/kr/560/Manual/SupportedEvents.html

 

지원되는 이벤트 - Unity 매뉴얼

이벤트 시스템은 다수의 이벤트를 지원하며 사용자가 작성한 입력 모듈을 통해 한층 더 효율적으로 커스터마이징할 수 있습니다.

docs.unity3d.com

여튼 변수 부분 부터 설명하겠다.

private ItemSOBase _itemInfo;
private SynergySO _synergyInfo;

private GraphicRaycaster _caster;
private PointerEventData _pointerData;
private List<RaycastResult> _raycastResult;
private ToolTip _toolTipCompo;

private RewardType _rewardType;

item 또는 synergy를 ToolTip에 띄우기 때문에 변수로 미리 선언 해두고 타입에 맞게 대입 해준다.

GraphicRayCaster는 UI의 RayCast 작업에 필요해서 선언하였다.  PointerEventData는 GraphicCaster가 Cast 할때 현재 EventSystem을 통해서 PointerEvent를 받아올 필요가 있는데 그 때문에 미리 선언하였다. RayCast List는 GraphicRayCast의 결과값을 받기 위해서 필요하다. ToolTip 변수는 생성 될때 Component을 받기 위해서 선언하였다. _rewardType은 아까 RewardPanel에 있었다. Item과 Synergy를 분리하기 위해서 필요하다.

public enum RewardType
{
    Item, Synergy    
}

 

함수 부분이다.

    public void SettingInfo(ScriptableObject item, ToolTip toolTipCompo, Canvas canvas)
    {
        if (item is ItemSOBase)
        {
            _itemInfo = item as ItemSOBase;
            _rewardType = RewardType.Item;
        }
        else
        {
            _synergyInfo = item as SynergySO;
            _rewardType = RewardType.Synergy;
        }

        _toolTipCompo = toolTipCompo;
        _caster = canvas.GetComponent<GraphicRaycaster>();
        _raycastResult = new();
    }

    public void OnPointerDown(PointerEventData eventData)
    {
        _pointerData = new PointerEventData(EventSystem.current);
        _pointerData.position = Input.mousePosition;
        _caster.Raycast(_pointerData, _raycastResult);

        if (_raycastResult.Count <= 0) return;

        foreach (var hit in _raycastResult)
        {
            if (hit.gameObject == gameObject)
            {
                // 툴팁 띄우기
                if(_rewardType == RewardType.Item)
                    _toolTipCompo.ToolTipOpen(_itemInfo.itemName, _itemInfo.description);
                else
                    _toolTipCompo.ToolTipOpen(_synergyInfo.ItemName, _synergyInfo.ItemDescription);
            }
        }
    }
}

SettingInfo는 말 그대로 정보 초기화 작업을 한다. RewardPanel Script와 마찬가지로 is 연산자를 사용하여 SO를 분리 한다.

IPoinerDownHandler의 함수인 OnPoinerDown이다 RayCast작업을 위해서 위쪽에 현재 마우스의 위치를 받고 캐스트 해준다.

만약 캐스트에 걸린 오브젝트가 없다면 반환 해준다.

있다면 foreach를 돌며 자신과 같은 GameObject를 찾고 자신이 가지고 있는 정보를 ToolTip Script에 넘긴다.

 

ToolTip Script다.

using System;
using System.Numerics;
using DG.Tweening;
using TMPro;
using UnityEngine;
using UnityEngine.InputSystem;
using Vector3 = UnityEngine.Vector3;

namespace LJS.UI
{
    public class ToolTip : MonoBehaviour
    {
        [SerializeField] private TextMeshProUGUI _nameTextField;
        [SerializeField] private TextMeshProUGUI _descriptionTextField;

        private RectTransform _rectTrm;

        private void Start()
        {
            transform.localScale = Vector3.zero;
        }

        private void Update()
        {
            if (Mouse.current.rightButton.wasPressedThisFrame)
            {
                ToolTipClose();
            }
        }

        public void ToolTipOpen(string name, string description)
        {
            transform.localScale = Vector3.zero;
            transform.DOScale(Vector3.one, 0.2f);
            _rectTrm = GetComponent<RectTransform>();
            Vector3 mousePos = Input.mousePosition;
            
            transform.position = 
                new Vector3(mousePos.x + _rectTrm.rect.width / 2, mousePos.y + _rectTrm.rect.height / 2, 0);
            _nameTextField.text = name;
            _descriptionTextField.text = description;
        }

        public void ToolTipClose()
        {
            transform.DOScale(Vector3.zero, 0.2f);
        }
    }
}

짧아서 간단하게 설명하겠다. TextMeshProUGUI 변수 2개를 선언한다. (아이템 이름, 아이템 설명)

스케일 줄여주고 Open이 불렸다면 DoScale(DOTWEEN)로 크기 키워주고 마우스 오른쪽 대각선 위쪽 위치로 옮긴다. 그리고 내용 매개변수로 받은 String으로 변경 하면 끝! 닫힐 때는 크기 줄여주고 마우스 우클릭 감지해서 Close 불러와 준다.

 

이럼 아까 봤던 UI의 기능은 전부 설명했다.