본문 바로가기

유니티

[ 유니티 ] UI Tool Kit 02 . 첫 런타임 UI 만들기

[첫 런타임 UI 만들기]

유니티 도큐먼드를 참고하였다 .

[UI ToolKit]

UI ToolKit을 사용하여 캐릭터 선택 화면을 만들어 본다 .

  • UI 요소 및 템플릿 만들기
  • 씬을 설정하는 방법
  • UI에 스크립팅 로직을 연결하기

등을 다루며 , USS 를 통한 스타일링은 다루지 않는다 .

 

[ 단계 ]

  1.  메인 UI 뷰 만들기
  2. 씬 설정
  3. 표시할 샘플 데이터 생성
  4. 메인 UI 뷰를 위한 컨트롤러 생성
  5. 리스트 엔트리 UI 템플릿 생성
  6. 리스트 엔트리 UI를 위한 컨트롤러 생성
  7. 사용자의 선택에 응답

[ 01 . 메인 UI 뷰 만들기 ]

[시스템]

최종화면은 두개의 개별 UI 템플릿 (UXML) 로 구성된다 . (메인 / 리스트 엔트리 템플릿)

메인 UI 뷰는 캐릭터 이름이 나열된 리스트 / 선택한 캐릭터의 세부 정보 / 선택 버튼으로 구성된다 .

UI 빌더를 사용하여 해당 UI 템플릿을 설정한다 .

 

[UI Builde 설정]

window  => UI ToolKit  => UI Builder로 UI Builder 창을 연다 .

File => New를 통해 새 UXML 문서를 만든다 .

 

 

게임 UI 개발시 UI 빌더 뷰포트 오른쪽 상단 Unity Default Runtime Theme을 선택하라고 권장한다 .

에디터 및 런타임 테마의 기본 폰트 크기와 컬러는 서로 다르며 , 이는 레이아웃에 영향을 미친다 .

해당 옵션이 처음에는 없을 수 있다 . PanelSettings 이 생성되면 같이 생성되는 듯 하다 .

없다면 직접 만들수도 있는데 Unity Default Runtime Theme만들기를 참고하자 .

 

 

Hirearchy의 새 UXML 파일을 선택 , Match Game View 체크박스를 활성화 하자.

(GameView의 해상도와 같은 크기로 만들어진다 .)

 

[UI 요소 만들기 - 캐릭터 리스트]

Library로부터 Visual Element를 드래그 하여 생성하자 .

 

UI 툴킷에서 모든 UI 요소는 VisualElement라는 기본 클래스에서 파생된다 . 주요 차이점은 VisualElement 클래스가 MonoBehaviour에서 파생되지 않는다는 것이다 . 그렇기에 게임 오브젝트에 시각적 요소를 연결할 수 없다.

 

새로운 요소는 화면 전체를 커버해야 하기에 flex - grow 프로퍼티를 1로 설정한다 .

flex-grow/shirink에 관하여 

 

해당 Visual Element의 모든 자식을 화면 중앙에 정렬하려면 Align 프로퍼티를 변경한다 .

Align Items와 Justify Content 모두 Center로 놓는다 . Align Items와 Justify Content에 관하여

 

Background => Color을 통해 색상을 지정한다 .

 

기존 Visual Element 아래에 새 Visual Element를 만든다 . 이는 UI 왼쪽 , 오른쪽 섹션을 위한 부모 컨테이너가 된다 .

 

새로운 요소의 flex - direction 프로퍼티를 row로 설정한다 .이는 자식의 배치 방향을 나타낸다 .

픽셀 높이 역시 350 픽셀로 고정한다 .  flex - direction에 관하여

 

캐릭터 이름의 리스트는 ListView를 통해 만든다 . CharacterList라는 이름을 할당해야 추후 엑세스가 가능하다.

 

 

리스트의 너비는 230으로 고정 , Margin의 설정으로 우측으로 6 정도의 간격을 둔다 . Margin과 Padding 에 관하여

 

해당 리스트에 배경 / 둥근 테두리를 설정하자 .

 

[UI 요소 만들기 -  캐릭터의 세부사항과 버튼]

Character List와 같은 부모에 새로운 Visual Element를 추가하자 . 해당 패널은 캐릭터 세부 사항 / 버튼을 포함한다 .

Align 폴드아웃 아래에서 Align Items 설정을 flex-end로, Justify Content space-between으로 변경한다.

 

해당 컨테이너에 새로운 Visual Element를 추가하자 . 이 패널은 캐릭터 세부 사항 패널이 될 것이다 .

요소의 너비를 276픽셀로 고정하고, Align Items Justify Content center로 전환한다. (가운데 정렬)

또한 자식들이 컨테이너 테두리와 최소한의 거리를 유지하도록 요소에 8픽셀 너비의 패딩을 추가한다 .

 

 

내부 개별 컨드롤 UI를 추가한다 . 캐릭터의 초상은 배경 프레임과 전경 이미지 두 요소로 구성된다 .

먼저 배경 프레임을 위해 캐릭터 세부 사항 컨테이너에 새 VisualElement를 추가한다.

120x120픽셀의 고정된 크기를 할당하고, 포함된 이미지가 테두리에 직접 닿지 않도록 4픽셀의 패딩을 설정한다.

 

 

배경 프레임에 새 Visual Element를 자식으로 추가 , 엑세스를 위해 CharacterPortrait로 이름을 지정한다 .

이미지가 가용 공간을 모두 사용하게 Flex-Grow는 1로 설정 , 종횡비를 유지하며 확대 축소를 하도록

Background > Scale Mode에서 확대/축소 모드를 scale-to-fit으로 설정한다 .

 

다음으로 CharacterName CharacterClass를 생성하자 . 원한다면 Text의 크기와 굵기의 변경이 가능하다 .

 

버튼을 추가한다 . 추후 컨트롤러 스크립트에서 이 버튼에 액세스하고 캐릭터가 선택되거나 선택 취소되면 이 버튼을 활성화 또는 비활성화하게 된다. 버튼의 이름을 SelectCharButton으로 지정하고 너비를 150픽셀로 고정한다.

또한 버튼의 레이블 텍스트를 Select Character로 지정한다.

완성된 과정

이제 완성된 UXML을  Assets/UI/MainView.uxml로 저장한다.

 

[ 02 . 씬 설정 ]

[씬 설정]

생성된 UI 템플릿을 런타임 시 게임에서 로드하고 표시하는 방법을 알아보자 .PanelSettings 에셋을 만들어야 한다.

이 에셋은 확대/축소 모드와 렌더링 순서와 같은 화면 설정을 정의하며,

UI 툴킷 디버거에 UI가 어떤 이름으로 표시되는지도 결정합니다.

 

MainViewUI.uxml을 표시하려면 씬에서 게임오브젝트의 생성이 필요하다 . 

해당 게임 오브젝트에 UIDoument 컴포넌트를 부탁한다 .

해당 컴포넌트는 Unity에서 플레이 모드에 진입시 할당된 VisualTreeAsset을 자동으로 로드한다 . 이는 UXML 템플릿이다 .

MainView.uxml과 새로운 GameUI_Panel 패널 설정을 컴포넌트에 할당하자 .

참고

  • PanelSettings 에셋을 UI Document 컴포넌트에 할당하지 않으면 프로젝트를 자동으로 검색하여 처음으로 찾은 패널 설정 에셋을 자동으로 사용한다. 에셋의 이름을 변경하거나 에셋을 이동할 때 이 점에 유의해야한다 .
  • 씬에 여러개의 UI 문서가 있는 경우 모든 UI 문서에 같은 패널 설정을 할당 할 수 있다 . 이 결과 UI가 같은 패널에 렌더링 되어 성능이 최적화 된다 .

[ 03 . 표시할 샘플 데이터 생성 ]

[Scriptable 스크립트 생성]

 UI의 캐릭터 리스트에 데이터를 채우는 데 사용되는 샘플 데이터를 만든다 .

새 ScriptableObject 스크립트 Assets/Scripts/CharacterData.cs를 만들자

using UnityEngine;

public enum ECharacterClass
{
    Knight, Ranger, Wizard
}

[CreateAssetMenu]
public class CharacterData : ScriptableObject
{
    public string m_CharacterName;
    public ECharacterClass m_Class;
    public Sprite m_PortraitImage;
}

 

CharacterData 인스턴스를 몇개 만든후 Resources/Characters 폴더에 넣자 .

후 이 폴더에서 모든 캐릭터 데이터를 자동으로 파싱하고 로드하는 스크립트를 작성할 것이다.

 

[ 04 . 리스트 앤트리 UI 템플릿 생성 ]

[템플릿 생성]

리스트의 개별 엔트리를 위한 UI 템플릿을 만든다.

런타임 시 컨트롤러 스크립트가 각 캐릭터에 대해 이 UI의 인스턴스를 만들고 리스트에 추가한다. 

 

배경을 위한 VisualElement를 추가하고, 높이를 41픽셀로 고정한다.

엔트리 내 텍스트는 왼쪽으로 정렬하여 요소 중앙에 배치해야 하므로, Align 폴드아웃을 열고 Align Items left로, Justify Content center로 설정한다.

또한 10픽셀의 왼쪽 패딩을 설정하여 레이블이 프레임의 왼쪽 테두리와 최소한의 간격을 유지하도록 한다.

 

기존 VisualElement의 자식으로 레이블을 추가하고 향후 컨트롤러 스크립트에서 액세스할 수 있도록 이름을 CharacterName으로 지정하자. Font Style bold로 설정하고 폰트 크기는 18로 설정한다 .

 

해당 UXML 템플릿을 Assets/UI/ListEntry.uxml로 저장하자.

 

[ 05 . 리스트 앤트리를 위한 컨트롤러 생성 ]

[컨트롤러 생성]

리스트 앤트리의 UI에 캐릭터 인스턴스 데이터를 표시할 것이다 .

 Assets/Scripts/UI/CharacterListEntryController.cs를 만들고 다음 코드를 붙여넣는다.

Label에 관하여

using UnityEngine.UIElements;

public class CharacterListEntryController
{
    Label m_NameLabel;
	
    //ListEntry UI 템플릿의 인스턴스인 시각적 요소 수신하여 CharacterName에 접근
    public void SetVisualElement(VisualElement visualElement)
    {
    	//Find와 같은 듯 하다
        m_NameLabel = visualElement.Q<Label>("CharacterName");
    }
	
    //CharacterName의 값 변경
    public void SetCharacterData(CharacterData characterData)
    {
        m_NameLabel.text = characterData.m_CharacterName;
    }
}

 

[ 06 . MainView 를 위한 컨트롤러 생성 ]

[컨트롤러 생성]

메인뷰의 캐릭터 리스트를 위한 컨트롤러 스크립트를 만들고 , 이러한 컨트롤러 스크립트를 인스턴스화 하고 

시각적 트리에 할당하는 Mono 스크립트를 만들 것이다 .

 

이름, USS 클래스, 타입 또는 이의 조합으로 개별 UI 컨트롤을 검색해서 가져오려면 UQuery API 패밀리를 사용하자.

UQuery에 관하여

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;

public class CharacterListController
{
    // list Entry의 프리팹 UXML
    VisualTreeAsset m_ListEntryTemplate;

    // UI 요소
    //캐릭터 리스트의 리스트 뷰
    ListView m_CharacterList;
    //캐릭터 클래스 라벨
    Label m_CharClassLabel;
    //캐릭터 이름 라벨
    Label m_CharNameLabel;
    //캐릭터의 초상화
    VisualElement m_CharPortrait;
    //선택 버튼
    Button m_SelectCharButton;
    //모든 캐릭터 데이터
	List<CharacterData> m_AllCharacters;
    
    public void InitializeCharacterList(VisualElement root, VisualTreeAsset listElementTemplate)
    {
    	EnumerateAllCharacters();
        
        // 전달받은 리스트 템플릿을 넣어준다
        m_ListEntryTemplate = listElementTemplate;

        // 캐릭터 리스트를 찾는다. 자식 생성을 위해서인듯
        m_CharacterList = root.Q<ListView>("CharacterList");

        // UI를 찾는다
        m_CharClassLabel = root.Q<Label>("CharacterClass");
        m_CharNameLabel = root.Q<Label>("CharacterName");
        m_CharPortrait = root.Q<VisualElement>("CharacterPortrait");
        m_SelectCharButton = root.Q<Button>("SelectCharButton");
        
        // 아이템 선택시 등록 액션
        m_CharacterList.onSelectionChange += OnCharacterSelected;
        
        FillCharacterList();
    }
    
    //모든 캐릭터 데이터를 얻어온다
    void EnumerateAllCharacters()
    {
        m_AllCharacters = new List<CharacterData>();
        m_AllCharacters.AddRange(Resources.LoadAll<CharacterData>("Characters"));
    }
    
    //캐릭터 리스트를 생성한다
    void FillCharacterList()
    {
        // Set up a make item function for a list entry
        m_CharacterList.makeItem = () =>
        {
            // 리스트 템플릿을 생성한다
            var newListEntry = m_ListEntryTemplate.Instantiate();

            // 리스트 엔트리의 초기화를 위한 컨트롤러 생성
            var newListEntryLogic = new CharacterListEntryController();

            // 해당 컨트롤러를 넣어주고 추후 스크립트에 액세스 가능하다 .
            newListEntry.userData = newListEntryLogic;

            // 해당 엔트리의 Label을 찾아준다 .
            newListEntryLogic.SetVisualElement(newListEntry);

            // 생성된 리스트를 넘긴다
            return newListEntry;
        };
        
        // 메모리 및 성능 최적화를 위해 리스트의 엔드리마다 별도의 요소를 인스턴스화 하는 대신
        //리스트 요소를 재사용 한다 . 보이는 만큼 시각적 요소만 만들고 , 스크롤 하게 된다면
        //시각적 요소를 모아 재사용 한다 .
        //따라서 데이터의 인스턴스(Character Data)를 개별 리스트 요소에 바인드하는
        //bindItem 콜백의 제공이 필요하다
        
        //더하여 , 리스트 엔트리의 시각적 트리를 위한 루트 시각적 요소의 레퍼런스를 수신 , 데이터의 인덱스도
        //수신한다
        m_CharacterList.bindItem = (item, index) =>
        {
        (item.userData as CharacterListEntryController).SetCharacterData(m_AllCharacters[index]);
        };
        
        // Set a fixed item height : 높이 설정
        m_CharacterList.fixedItemHeight = 45;

        // 리스트를 소스에 저장
        m_CharacterList.itemsSource = m_AllCharacters;
    }
    
    void OnCharacterSelected(IEnumerable<object> selectedItems)
    {
        // Get the currently selected item directly from the ListView
        var selectedCharacter = m_CharacterList.selectedItem as CharacterData;

        // Handle none-selection (Escape to deselect everything)
        if (selectedCharacter == null)
        {
            // Clear
            m_CharClassLabel.text = "";
            m_CharNameLabel.text = "";
            m_CharPortrait.style.backgroundImage = null;

            // Disable the select button
            m_SelectCharButton.SetEnabled(false);

            return;
    	}
        
        // Fill in character details
        m_CharClassLabel.text = selectedCharacter.m_Class.ToString();
        m_CharNameLabel.text = selectedCharacter.m_CharacterName;
        m_CharPortrait.style.backgroundImage = new StyleBackground(selectedCharacter.m_PortraitImage);

        // Enable the select button
        m_SelectCharButton.SetEnabled(true);
	}
}

 

[MainView에 컨트롤러 스크립트 부착]

CharacterListController는 MonoBehaviour가 아니기에 , 다른 방식으로 시각적 트리에 부착해야 한다 .

 Assets/Scripts/UI/MainView.cs를 만들고 다음 코드를 붙여넣으십시오 . CharacterListController를 인스턴스화 할 것이다

UI가 리로드되면 UIDocument 컴포넌트를 포함하는 같은 게임 오브젝트의 컴패니언 MonoBehaviour 컴포넌트가 리로드 전에 비활성화된 다음 리로드 후 다시 활성화된다.

따라서 이러한 MonoBehaviour의 OnEnable  OnDisable 메서드에 UI와 상호작용하는 코드를 넣는 것이 좋습니다.

using UnityEngine;
using UnityEngine.UIElements;

public class MainView : MonoBehaviour
{
	//리스트의 프리팹
    [SerializeField]
    VisualTreeAsset m_ListEntryTemplate;

    void OnEnable()
    {
        // The UXML is already instantiated by the UIDocument component
        var uiDocument = GetComponent<UIDocument>();

        // Initialize the character list controller
        var characterListController = new CharacterListController();
        //인자로 MainView.UXML 과 ListEntry.UXML을 넘긴다 .
        characterListController.InitializeCharacterList(uiDocument.rootVisualElement, m_ListEntryTemplate);
    }
}

 

[ 07 . 사용자 선택에 응답 ]

[응답]

사용자가 캐릭터를 선택하면 캐릭터의 세부 사항, 즉 초상, 성명과 클래스가 화면 오른쪽의 캐릭터 세부 사항 섹션에 표시되어야 한다.

또한 캐릭터가 선택되면 선택 버튼이 활성화되어야 한다. 선택된 캐릭터가 없으면 버튼이 다시 비활성화되어야 한다.

 

선택 및 강조 표시 기능은 ListView 컨트롤의 일부이기에 이미 리스트의 캐릭터를 클릭하고 선택할 수 있다.

사용자가 리스트의 선택 항목을 변경하면 응답하는 콜백 함수만 추가하면 된니다.

 ListView 컨트롤은 이를 위한 onSelectionChange 이벤트를 포함한다. 해당 작업은 위에 추가되어 있다 .

다음과 같이 선택이 가능해진다 .

 

출처