시작하기에 앞서 글씨체를 바꿔줬다.
Control_Button을 먼저 좀 정리해주자. 두 이벤트의 스타일 추가, 삭제를 모두 Toggle로 바꿔준 다음 델리게이트를 호출하도록 바꿔줬다. 이러면 다른 스크립트도 델리게이트에 메소드를 붙여서 실행할 수 있어진다.
using System;
using System.Collections.Generic;
using UnityEngine.Rendering;
using UnityEngine.UIElements;
public class Control_Button : VisualElement
{
internal new class UxmlFactory : UxmlFactory<Control_Button, UxmlTraits> { }
internal new class UxmlTraits : VisualElement.UxmlTraits {
private readonly UxmlIntAttributeDescription m_CardNum
= new UxmlIntAttributeDescription { name = "card_Number", defaultValue = 4 };
private readonly UxmlStringAttributeDescription m_ButtonLabel
= new UxmlStringAttributeDescription { name = "button_Label", defaultValue = "Menu Button" };
public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription
{
get {yield break;}
}
public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
{
base.Init(ve, bag, cc);
((Control_Button)ve).CardNumber = m_CardNum.GetValueFromBag(bag, cc);
((Control_Button)ve).ButtonLabel = m_ButtonLabel.GetValueFromBag(bag, cc);
}
}
public int CardNumber { get; set; }
public string ButtonLabel {
get { return _label.text; }
set{ _label.text = value; }
}
private VisualElement _fill;
private VisualElement _icon;
private Label _label;
public event Action<Control_Button> OnHover;
public event Action<Control_Button, int> OnSelect;
public Control_Button()
{
// 버튼 클릭할 떄
_fill = new VisualElement();
Add(_fill);
// 버튼 아이콘
_icon = new VisualElement();
Add(_icon);
// 버튼 레이블
_label = new Label();
Add(_label);
this.name = "Background";
_fill.name = "Fill";
_label.name = "Label";
_icon.name = "Icon";
// Preventing Clicks
_fill.pickingMode = PickingMode.Ignore;
_label.pickingMode = PickingMode.Ignore;
_icon.pickingMode = PickingMode.Ignore;
// Load and apply the stylesheet
this.AddToClassList("background--normal");
_fill.AddToClassList("fill--normal");
_icon.AddToClassList("icon--normal");
_label.AddToClassList("label--normal");
// On Mouse Enter event
this.RegisterCallback<MouseEnterEvent>(OnMouseEventControlButton);
this.RegisterCallback<MouseLeaveEvent>(OnMouseEventControlButton);
// On Mouse Click Event
this.RegisterCallback<ClickEvent>(OnMouseEventControlButton);
}
public void OnMouseEventControlButton(EventBase evt)
{
// Mouse Enter Control Button
if (evt.eventTypeId == MouseEnterEvent.TypeId() || evt.eventTypeId == MouseLeaveEvent.TypeId()) {
ToggleHoverStyle(this);
OnHover?.Invoke(this);
}
// Mouse Click Control Button
if (evt.eventTypeId == ClickEvent.TypeId()) {
ToggleSelectStyle(this, CardNumber);
OnSelect?.Invoke(this, CardNumber);
}
}
// Hover Style Toggle Method
public void ToggleHoverStyle(Control_Button m_button)
{
m_button.ToggleInClassList("background--hover");
m_button._icon.ToggleInClassList("icon--hover");
m_button._label.ToggleInClassList("label--hover");
}
// Select Style Toggle Method
public void ToggleSelectStyle(Control_Button m_button, int m_cardNum) {
m_button._fill.ToggleInClassList("fill--select");
m_button._icon.ToggleInClassList("icon--select");
m_button._label.ToggleInClassList("label--select");
}
}
이제 하나를 클릭하면 다른 버튼의 Select가 풀리도록 구현해보자. Control_Button 내부에서 다른 버튼을 접근하는 것 보다는 관리자를 하나 새로 만들어서 관리하도록 하는게 좋을 것 같다.
using System.Collections;
using System.Collections.Generic;
using Sirenix.OdinInspector;
using UnityEngine;
using UnityEngine.UIElements;
public class Control_Button_Manager : MonoBehaviour
{
[SerializeField] private UIDocument UI_Doc;
[SerializeField] private List<Control_Button> controlButtons;
[SerializeField] private Control_Button selectedButton;
// Odin - Tap Define
[TabGroup("Tap","Debug",SdfIconType.CodeSlash, TextColor="red")]
[TabGroup("Tap","Debug")]
[Button("Init Control Button List")]
void Init() {
var root = UI_Doc.rootVisualElement;
// Clear Control Button List
if (controlButtons != null && controlButtons.Count > 0)
controlButtons.Clear();
// Load Control Button in UI_Doc
controlButtons = new List<Control_Button>();
var buttons = root.Query<Control_Button>().ToList();
foreach (var button in buttons) {
button.OnSelect += OnSelectControlButton;
controlButtons.Add(button);
}
Debug.Log($"[Control Button Manager] {controlButtons.Count} Buttons Initialized");
}
void OnSelectControlButton(Control_Button currentButton, int cardNumber) {
Debug.Log($"[Control Button Manager] {currentButton.GetLabelText()} Button Select");
if (selectedButton == currentButton) {
selectedButton = null;
return;
}
if (selectedButton != null)
selectedButton.ToggleSelectStyle(selectedButton, cardNumber); // Untoggle SelectedButton
selectedButton = currentButton; // change selected Button
}
#if UNITY_EDITOR
[TabGroup("Tap","Debug")]
[Button("Check Selected Control Button")]
void CheckSelectedControlButton() {
if (selectedButton != null)
Debug.Log($"[Control Button Manager] {selectedButton.GetLabelText()} Button Selected Now!");
else
Debug.Log($"[Control Button Manager] No buttons are selected.");
}
#endif
}
실행한 후에 Init Cutron Button List 버튼을 누르면 이제 로직이 잘 작동한다. (나중에 Start나 OnEnable로 수정해주면 된다.)
이제 SelectStyle이 잘 적용된다.
이제 버튼을 클릭하면 선택 가능한 카드가 옆으로 나오는 부분을 구현해보자. 일단 먼저 UXML 만들어보자.
그 다음 Control Card 스크립트를 작성해보자.
using System;
using System.Collections.Generic;
using UnityEngine.UIElements;
public class Control_Card : VisualElement
{
internal new class UxmlFactory : UxmlFactory<Control_Card, UxmlTraits> { }
internal new class UxmlTraits : VisualElement.UxmlTraits {
private readonly UxmlStringAttributeDescription m_CardLabel
= new UxmlStringAttributeDescription { name = "card_Label", defaultValue = "" };
private readonly UxmlStringAttributeDescription m_ButtonLabel
= new UxmlStringAttributeDescription { name = "button_Label", defaultValue = "Menu Button" };
public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription
{
get { yield break;}
}
public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
{
base.Init(ve, bag, cc);
((Control_Card)ve).CardLabel = m_CardLabel.GetValueFromBag(bag, cc);
((Control_Card)ve).ButtonLabel = m_ButtonLabel.GetValueFromBag(bag, cc);
}
}
public string CardLabel {
get { return _card_Label.text; }
set{ _card_Label.text = value; }
}
public string ButtonLabel {
get { return _button_label.text; }
set{ _button_label.text = value; }
}
public int CardNumber;
private VisualElement _card_BackGround;
private VisualElement _card_Image;
private Label _card_Label;
private VisualElement _card_Button;
private VisualElement _button_icon;
private Label _button_label;
public event Action<Control_Card> OnHover;
public event Action<Control_Card, int> OnSelect;
public Control_Card()
{
// 카드 뒷배경
_card_BackGround = new VisualElement { name = "Card_Background" };
Add(_card_BackGround);
// 카드 이미지
_card_Image = new VisualElement { name = "Card_Image" };
_card_BackGround.Add(_card_Image);
// 카드 레이블
_card_Label = new Label { name = "Card_Label" };
_card_BackGround.Add(_card_Label);
// 카드 버튼
_card_Button = new VisualElement { name = "Card_Button" };
Add(_card_Button);
// 버튼 아이콘
_button_icon = new VisualElement { name = "Button_Icon" };
_card_Button.Add(_button_icon);
// 버튼 레이블
_button_label = new Label { name = "Button_Label" };
_card_Button.Add(_button_label);
// Preventing Clicks
_card_BackGround.pickingMode = PickingMode.Ignore;
_card_Image.pickingMode = PickingMode.Ignore;
_card_Label.pickingMode = PickingMode.Ignore;
_card_Button.pickingMode = PickingMode.Ignore;
_button_icon.pickingMode = PickingMode.Ignore;
_button_label.pickingMode = PickingMode.Ignore;
// Load and apply the stylesheet
this.AddToClassList("control__card--normal");
_card_BackGround.AddToClassList("card__background--normal");
_card_Image.AddToClassList("card__image--normal");
_card_Label.AddToClassList("card__label--normal");
_card_Button.AddToClassList("card__button--normal");
_button_icon.AddToClassList("button__icon--normal");
_button_label.AddToClassList("button__label--normal");
// On Mouse Enter event
this.RegisterCallback<MouseEnterEvent>(OnMouseEventControlButton);
this.RegisterCallback<MouseLeaveEvent>(OnMouseEventControlButton);
// On Mouse Click Event
this.RegisterCallback<ClickEvent>(OnMouseEventControlButton);
}
public void OnMouseEventControlButton(EventBase evt)
{
// Mouse Enter Control Button
if (evt.eventTypeId == MouseEnterEvent.TypeId() || evt.eventTypeId == MouseLeaveEvent.TypeId()) {
ToggleHoverStyle(this);
OnHover?.Invoke(this);
}
// Mouse Click Control Button
if (evt.eventTypeId == ClickEvent.TypeId()) {
ToggleSelectStyle(this, CardNumber);
OnSelect?.Invoke(this, CardNumber);
}
}
// Hover Style Toggle Method
public void ToggleHoverStyle(Control_Card m_card)
{
}
// Select Style Toggle Method
public void ToggleSelectStyle(Control_Card m_card, int m_cardNum) {
}
}
이제 Hover 애니메이션 넣어보자. 먼저 Hover 스타일 클래스를 하나 만들어주고 스크립트를 업데이트한다.
// Hover Style Toggle Method
public void ToggleHoverStyle(Control_Card m_card)
{
_card_Image.AddToClassList("card__image--hover");
_card_Button.AddToClassList("card__button--hover");
_button_icon.AddToClassList("button__icon--hover");
_button_label.AddToClassList("button__label--hover");
}
카드 메뉴를 관리할 UI Manager로 합쳐서 작성해보자.
using System.Collections.Generic;
using Sirenix.OdinInspector;
using UnityEngine;
using UnityEngine.UIElements;
public class UI_Manager : MonoBehaviour
{
[SerializeField] private UIDocument UI_Doc;
[SerializeField] private List<Control_Button> controlButtons;
[SerializeField] private List<Control_Card> controlCards;
[SerializeField] private Control_Button selectedButton;
[SerializeField, TabGroup("Tap","Control Buttons")] private int seletedTabNumber = 0;
// Odin - Tap Define
[TabGroup("Tap","Control Buttons",SdfIconType.CodeSlash, TextColor="red")]
void Start() {
//InitButtons();
}
[TabGroup("Tap","Control Buttons")]
[Button("Init Control Button List"), GUIColor(0,1,0)]
void InitButtons() {
var root = UI_Doc.rootVisualElement;
// Clear Control Button List
if (controlButtons != null && controlButtons.Count > 0)
controlButtons.Clear();
// Load Control Button in UI_Doc
controlButtons = new List<Control_Button>();
var buttons = root.Query<Control_Button>().ToList();
int _tabNumber = 1;
foreach (var button in buttons) {
button.OnSelect += OnSelectControlButton;
button.TabNumber = _tabNumber++;
controlButtons.Add(button);
}
Debug.Log($"[UI Manager] {controlButtons.Count} Buttons Initialized");
}
[TabGroup("Tap","Control Cards")]
[Button("Init Control Button List"), GUIColor(0,1,0)]
void InitCards() {
var root = UI_Doc.rootVisualElement;
// Clear Control Card List
if (controlCards != null && controlCards.Count > 0)
controlCards.Clear();
// Load Control Card in UI_Doc
controlCards = new List<Control_Card>();
var cards = root.Query<Control_Card>().ToList();
int cardIndex = 1;
foreach (var card in cards) {
card.AddToClassList($"card-{cardIndex++}");
controlCards.Add(card);
}
Debug.Log($"[UI Manager] {controlCards.Count} Cards Initialized");
}
void OnSelectControlButton(Control_Button currentButton, int tabNumber) {
if (selectedButton == currentButton) {
Debug.Log($"[UI Manager] Control Button Unselect.");
seletedTabNumber = 0;
selectedButton = null;
return;
}
if (selectedButton != null)
selectedButton.ToggleSelectStyle(selectedButton, selectedButton.TabNumber); // Untoggle SelectedButton
// change selected Button
selectedButton = currentButton;
seletedTabNumber = currentButton.TabNumber;
Debug.Log($"[UI Manager][{tabNumber}] {currentButton.GetLabelText()} Button Select");
}
#if UNITY_EDITOR
[TabGroup("Tap","Control Buttons")]
[Button("Check Selected Control Button")]
void CheckSelectedControlButton() {
if (selectedButton != null)
Debug.Log($"[Control Button Manager][{selectedButton.TabNumber}] {selectedButton.GetLabelText()} Button Selected Now!");
else
Debug.Log($"[Control Button Manager] No buttons are selected.");
}
#endif
}
이제 인스펙터로 Control Buttons를 초기화해주면 자동으로 Card-1,2,3,4 스타일이 추가되면서 카드가 숨겨진다.
이제 Card를 Show, Hide하는 함수를 하나 만들어준다.
[TabGroup("Tap","Control Cards")]
[Button("Toggle Show/Hide Card Menus")]
void ToggleShowHideCards() {
for (int i = 0; i < controlCards.Count; i++) {
controlCards[i].ToggleInClassList($"card-{i + 1}--out");
}
}
이제 탭 메뉴에 따라 카드 이미지와 라벨을 업데이트하는 것만 추가해보자
using System;
using UnityEngine;
[Serializable]
public class CardData {
public Texture2D card_image;
public string card_label_text;
}
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(fileName = "Tab_Data", menuName = "Tab_Data", order = 0)]
public class TabData : ScriptableObject
{
[SerializeField]
public List<CardData> tabData = new List<CardData>();
}
이제 탭 별로 카드 데이터들을 등록해준다.
동일하게 5개의 탭 데이터를 만들어준다.
이제 카드 데이터들을 탭 번호에 따라 업데이트 해주는 스크립트를 작성해주자.
[TabGroup("Tap","Control Cards")]
[Button("Cards Update")]
private UniTask<int> CardsUpdate(int tabNumber) {
List<CardData> selectedTabData = tabDatas[tabNumber].tabData;
for (int i = 0; i < selectedTabData.Count; i++) {
controlCards[i].set_card_Image(selectedTabData[i].card_image);
controlCards[i].set_card_Label(selectedTabData[i].card_label_text);
}
return UniTask.FromResult(selectedTabData.Count);;
}
이때 selectedTabData의 Count를 반환해주는 이유는 가지고 있는 카드 데이터 수의 카드만 Show 해줘야하기 때문에 Count를 넘겨준다.
UniTask로 해준 이유 : Card Update가 완료될때까지 기다리고(await) 카드를 Show해주기 위해서 UniTask로 작성했다.
async void OnSelectControlButton(Control_Button currentButton, int currentTabNumber) {
if (selectedTabNumber != -1) {
HideCards();
await UniTask.Delay(TimeSpan.FromSeconds(0.2f));
if (selectedTabNumber == currentTabNumber) {
Debug.Log($"[UI Manager][{selectedTabNumber} = {currentTabNumber}] Unselect");
selectedButton = null;
selectedTabNumber = -1;
return;
}
selectedButton.ToggleSelectStyle(selectedButton, selectedTabNumber);
Debug.Log($"[UI Manager][{selectedTabNumber} → {currentTabNumber}] Button Select");
}
// change selected Button
selectedButton = currentButton;
selectedTabNumber = currentButton.TabNumber;
// Cards Update (using Tab Number)
int countCards = await CardsUpdate(currentTabNumber);
// Show Updated Cards
ShowCards(countCards);
}
이제 TabData를 탭 메뉴 개수만큼 만들어준 후에 인스펙터로 초기화해준다.
아래와 같은 결과를 얻을 수 있다!
아래는 배틀필드 2042의 하단 UI 이미지이다. 거의 유사한 느낌으로 구현을 완료했다!