901 lines
55 KiB
C#
901 lines
55 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using TMPro;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
using UVC.Locale; // 다국어 처리 네임스페이스
|
|
using UVC.Log;
|
|
|
|
namespace UVC.UI.Menu
|
|
{
|
|
/// <summary>
|
|
/// 상단 메뉴 UI의 표시 및 상호작용을 관리하는 클래스입니다.
|
|
/// MenuItemData를 기반으로 실제 Unity UI 요소들을 생성하고 배치하며, 사용자 입력을 처리합니다.
|
|
/// 이 클래스는 상속을 통해 특정 프로젝트의 요구사항에 맞게 커스터마이징될 수 있도록 설계되었습니다.
|
|
/// </summary>
|
|
/// <example>
|
|
/// <code>
|
|
/// <summary>
|
|
/// TopMenuView를 상속받아 특정 기능을 커스터마이징하는 예제 클래스입니다.
|
|
/// 예를 들어, 다른 프리팹 경로를 사용하거나 메뉴 아이템 레이아웃 방식을 변경할 수 있습니다.
|
|
/// </summary>
|
|
/// public class CustomTopMenuView : TopMenuView
|
|
/// {
|
|
/// /// <summary>
|
|
/// /// 커스텀 메뉴 아이템 프리팹의 Resources 폴더 내 경로입니다.
|
|
/// /// </summary>
|
|
/// protected override string MenuItemPrefabPath => "Prefabs/UI/Menu/CustomMenuItem"; // 다른 프리팹 경로로 변경
|
|
///
|
|
/// /// <summary>
|
|
/// /// 커스텀 하위 메뉴 아이템 프리팹의 Resources 폴더 내 경로입니다.
|
|
/// /// </summary>
|
|
/// protected override string SubMenuItemPrefabPath => "Prefabs/UI/Menu/CustomSubMenuItem"; // 다른 프리팹 경로로 변경
|
|
///
|
|
/// /// <summary>
|
|
/// /// MonoBehaviour의 Awake 메시지입니다.
|
|
/// /// 부모 클래스의 Awake를 호출한 후, 추가적인 커스텀 초기화 로직을 수행합니다.
|
|
/// /// </summary>
|
|
/// protected override void Awake()
|
|
/// {
|
|
/// // 부모 클래스(TopMenuView)의 Awake 로직을 먼저 실행합니다.
|
|
/// // 이렇게 하면 프리팹 로드, 기본 설정 등이 먼저 처리됩니다.
|
|
/// base.Awake();
|
|
///
|
|
/// // 여기에 CustomTopMenuView만의 추가적인 초기화 코드를 작성할 수 있습니다.
|
|
/// ULog.Debug("CustomTopMenuView Awake 실행됨. 커스텀 프리팹 경로가 사용됩니다.");
|
|
///
|
|
/// // 예: 특정 조건에 따라 메뉴 아이템 간 간격을 변경
|
|
/// // this.menuItemSpace = new Vector2(40, 5);
|
|
/// }
|
|
///
|
|
/// /// <summary>
|
|
/// /// UI 요소(메뉴 아이템 또는 구분선)의 위치와 크기를 설정합니다.
|
|
/// /// 이 예제에서는 기본 레이아웃 로직을 그대로 사용하지만,
|
|
/// /// 필요에 따라 완전히 새로운 레이아웃 로직을 구현할 수 있습니다.
|
|
/// /// 예를 들어, 1차 메뉴 아이템들을 수직으로 정렬하도록 변경할 수 있습니다.
|
|
/// /// </summary>
|
|
/// protected override void LayoutMenuItem(GameObject uiElement, MenuItemData itemData, int depth, int itemIndex, List<MenuItemData> siblingItems)
|
|
/// {
|
|
/// // 기본 레이아웃 로직을 그대로 사용하려면 base.LayoutMenuItem을 호출합니다.
|
|
/// base.LayoutMenuItem(uiElement, itemData, depth, itemIndex, siblingItems);
|
|
///
|
|
/// // 여기에 추가적인 레이아웃 조정 로직을 넣을 수 있습니다.
|
|
/// // 예: 특정 아이템 ID를 가진 메뉴만 특별한 스타일을 적용 (색상 변경 등 - 여기서는 RectTransform만 다룸)
|
|
/// if (itemData.ItemId == "special_menu_item")
|
|
/// {
|
|
/// RectTransform rt = uiElement.GetComponent<RectTransform>();
|
|
/// if (rt != null)
|
|
/// {
|
|
/// // 예시: 특정 아이템의 높이를 약간 늘림 (실제로는 크기 변경 시 다른 아이템 위치도 재조정 필요)
|
|
/// // rt.sizeDelta = new Vector2(rt.sizeDelta.x, rt.sizeDelta.y + 10);
|
|
/// ULog.Debug($"Special item '{itemData.ItemId}' layout customized.");
|
|
/// }
|
|
/// }
|
|
/// }
|
|
///
|
|
/// /// <summary>
|
|
/// /// 메뉴 아이템이 클릭되었을 때 호출되는 이벤트 핸들러를 커스터마이징 할 수 있습니다.
|
|
/// /// (참고: OnMenuItemClicked 이벤트는 TopMenuController에서 구독하여 주로 처리합니다.
|
|
/// /// View에서 직접 처리해야 하는 로직이 있다면 이와 유사한 방식으로 메서드를 오버라이드 하거나
|
|
/// /// Awake 등에서 이벤트 핸들러를 추가/제거 할 수 있습니다.)
|
|
/// /// 이 예제에서는 ToggleSubMenuDisplay를 오버라이드하여 하위 메뉴 토글 시 로그를 남깁니다.
|
|
/// /// </summary>
|
|
/// protected override void ToggleSubMenuDisplay(MenuItemData itemData, GameObject menuItemInstance)
|
|
/// {
|
|
/// ULog.Debug($"Custom ToggleSubMenuDisplay: {itemData.DisplayNameKey} 클릭됨.");
|
|
/// base.ToggleSubMenuDisplay(itemData, menuItemInstance); // 부모의 원래 로직 실행
|
|
/// }
|
|
///
|
|
/// // 필요에 따라 다른 virtual 메서드들 (CreateMenuItems, ClearMenuItems 등)도
|
|
/// // 오버라이드하여 동작을 변경하거나 확장할 수 있습니다.
|
|
/// }
|
|
/// </code>
|
|
/// </example>
|
|
public class TopMenuView : MonoBehaviour
|
|
{
|
|
// --- 프리팹 경로 및 UI 요소 이름 ---
|
|
// 메뉴 아이템, 하위 메뉴 아이템, 구분선 프리팹의 Resources 폴더 내 경로입니다.
|
|
// 상속 클래스에서 이 경로들을 변경하여 다른 프리팹을 사용할 수 있습니다.
|
|
protected string menuItemPrefabPath = "Prefabs/UI/Menu/MenuItem";
|
|
protected string subMenuItemPrefabPath = "Prefabs/UI/Menu/SubMenuItem";
|
|
protected string menuSeparatorPrefabPath = "Prefabs/UI/Menu/MenuSeparator";
|
|
|
|
// 메뉴 UI 요소들을 찾기 위한 이름들입니다.
|
|
// 프리팹의 계층 구조나 이름이 다를 경우, 상속 클래스에서 이 값들을 변경해야 합니다.
|
|
protected string menuContainerName = "TopMenu"; // 최상위 메뉴 아이템들이 배치될 부모 GameObject의 이름
|
|
protected string subMenuContainerName = "SubMenuContainer"; // 하위 메뉴 아이템들이 배치될 부모 GameObject의 이름 (각 메뉴 아이템 내부에 존재)
|
|
protected string subMenuArrowName = "SubMenuArrow"; // 하위 메뉴가 있음을 나타내는 화살표 UI GameObject의 이름 (각 메뉴 아이템 내부에 존재)
|
|
|
|
/// <summary>
|
|
/// 기본 메뉴 아이템 프리팹의 Resources 폴더 내 경로입니다.
|
|
/// 상속 클래스에서 오버라이드하여 다른 경로를 지정할 수 있습니다.
|
|
/// </summary>
|
|
protected virtual string MenuItemPrefabPath => menuItemPrefabPath;
|
|
/// <summary>
|
|
/// 하위 메뉴 아이템 프리팹의 Resources 폴더 내 경로입니다.
|
|
/// 상속 클래스에서 오버라이드하여 다른 경로를 지정할 수 있습니다.
|
|
/// </summary>
|
|
protected virtual string SubMenuItemPrefabPath => subMenuItemPrefabPath;
|
|
/// <summary>
|
|
/// 메뉴 구분선 프리팹의 Resources 폴더 내 경로입니다.
|
|
/// 상속 클래스에서 오버라이드하여 다른 경로를 지정할 수 있습니다.
|
|
/// </summary>
|
|
protected virtual string MenuSeparatorPrefabPath => menuSeparatorPrefabPath;
|
|
/// <summary>
|
|
/// 최상위 메뉴 아이템들이 배치될 컨테이너 GameObject의 이름입니다.
|
|
/// </summary>
|
|
protected virtual string MenuContainerName => menuContainerName;
|
|
/// <summary>
|
|
/// 하위 메뉴 아이템들이 배치될 컨테이너 GameObject의 이름입니다. (각 메뉴 아이템 프리팹 내부에 있어야 함)
|
|
/// </summary>
|
|
protected virtual string SubMenuContainerName => subMenuContainerName;
|
|
/// <summary>
|
|
/// 하위 메뉴 존재를 나타내는 화살표 UI GameObject의 이름입니다. (각 메뉴 아이템 프리팹 내부에 있어야 함)
|
|
/// </summary>
|
|
protected virtual string SubMenuArrowName => subMenuArrowName;
|
|
|
|
// --- 로드된 프리팹 및 UI 참조 ---
|
|
protected GameObject menuItemPrefab; // 로드된 기본 메뉴 아이템 프리팹
|
|
protected GameObject subMenuItemPrefab; // 로드된 하위 메뉴 아이템 프리팹
|
|
protected GameObject menuSeparatorPrefab; // 로드된 메뉴 구분선 프리팹
|
|
protected Transform menuContainer; // 메뉴 아이템들이 실제로 배치될 부모 Transform
|
|
|
|
// --- UI 레이아웃 관련 설정 값 ---
|
|
// 이 값들은 프리팹에서 자동으로 읽어오려고 시도하지만, 실패 시 또는 커스텀 로직에서 사용될 수 있습니다.
|
|
// 상속 클래스에서 이 값들을 직접 변경하거나, Awake()에서 프리팹으로부터 다시 읽어오도록 수정할 수 있습니다.
|
|
protected Vector2 menuItemInitPosition = new Vector2(0, 0); // 1depth 메뉴 아이템의 초기 위치 (부모 컨테이너 기준)
|
|
protected Vector2 menuItemSize = new Vector2(100, 30); // 기본 메뉴 아이템의 크기
|
|
protected Vector2 subMenuItemSize = new Vector2(100, 30); // 하위 메뉴 아이템의 크기
|
|
protected Vector2 menuItemSeparatorSize = new Vector2(100, 1); // 메뉴 구분선의 크기
|
|
protected Vector4 menuItemSeparatorMargin = new Vector4(0, 4, 0, 4); // 구분선의 좌, 상, 우, 하 마진 (주로 상하 마진 사용)
|
|
protected Vector2 menuItemSpace = new Vector2(10, 2); // 메뉴 아이템 간의 간격 (수평, 수직)
|
|
protected Vector2 menuDepthSpace = new Vector2(5, 10); // 1차 메뉴 아이템과 그 하위 메뉴 컨테이너 간의 간격 (수평, 수직)
|
|
protected Vector2 subMenuDepthSpace = new Vector2(5, 10); // 하위 메뉴 아이템과 그 하위 메뉴 컨테이너 간의 간격 (수평, 수직)
|
|
protected Vector4 subContainerPadding = new Vector4(4, 10, 4, 10); // 하위 메뉴 컨테이너 내부의 좌, 상, 우, 하 패딩
|
|
|
|
/// <summary>
|
|
/// 메뉴 아이템들이 배치되는 부모 Transform 입니다.
|
|
/// </summary>
|
|
public Transform MenuContainer { get => menuContainer; }
|
|
|
|
/// <summary>
|
|
/// 메뉴 아이템이 클릭되었을 때 발생하는 이벤트입니다.
|
|
/// 클릭된 메뉴의 MenuItemData를 전달합니다.
|
|
/// </summary>
|
|
public event Action<MenuItemData> OnMenuItemClicked;
|
|
|
|
// --- 내부 상태 변수 ---
|
|
// 생성된 메뉴 아이템 GameObject들을 관리하는 딕셔너리 (키: ItemId, 값: GameObject)
|
|
protected Dictionary<string, GameObject> _menuItemObjects = new Dictionary<string, GameObject>();
|
|
protected LocalizationManager _locManager; // 다국어 처리를 위한 LocalizationManager 인스턴스
|
|
protected GameObject uiBlockerInstance; // 하위 메뉴가 열렸을 때 다른 UI 상호작용을 막기 위한 투명한 UI 요소
|
|
protected bool isAnySubMenuOpen = false; // 하나 이상의 하위 메뉴가 열려있는지 여부
|
|
|
|
/// <summary>
|
|
/// MonoBehaviour의 Awake 메시지입니다.
|
|
/// 메뉴 시스템 초기화에 필요한 리소스 로드 및 참조 설정을 수행합니다.
|
|
/// 상속 클래스에서 이 메서드를 오버라이드하여 추가적인 초기화 로직을 구현할 수 있습니다.
|
|
/// 오버라이드 시 `base.Awake()`를 호출하는 것을 권장합니다.
|
|
/// </summary>
|
|
protected virtual void Awake()
|
|
{
|
|
_locManager = LocalizationManager.Instance;
|
|
if (_locManager == null)
|
|
{
|
|
// LocalizationManager가 필수적인 경우, 여기서 에러를 발생시키거나 게임을 중단시킬 수 있습니다.
|
|
// 여기서는 경고만 기록하고 진행합니다.
|
|
ULog.Error("LocalizationManager 인스턴스를 찾을 수 없습니다. 메뉴 텍스트가 제대로 표시되지 않을 수 있습니다.", new InvalidOperationException("LocalizationManager 인스턴스를 찾을 수 없습니다."));
|
|
}
|
|
|
|
// 메뉴 아이템 프리팹 로드 및 크기 정보 추출
|
|
menuItemPrefab = Resources.Load<GameObject>(MenuItemPrefabPath);
|
|
if (menuItemPrefab != null)
|
|
{
|
|
RectTransform rt = menuItemPrefab.GetComponent<RectTransform>();
|
|
if (rt != null) menuItemSize = rt.sizeDelta;
|
|
else ULog.Error($"메뉴 아이템 프리팹 '{MenuItemPrefabPath}'에 RectTransform 컴포넌트가 없습니다. 기본 크기 값을 사용합니다.");
|
|
}
|
|
else
|
|
{
|
|
ULog.Error($"메뉴 아이템 프리팹을 Resources 폴더에서 로드할 수 없습니다. 경로: {MenuItemPrefabPath}", new System.IO.FileNotFoundException($"Prefab not found at Resources/{MenuItemPrefabPath}"));
|
|
}
|
|
|
|
// 하위 메뉴 아이템 프리팹 로드 및 크기 정보 추출
|
|
subMenuItemPrefab = Resources.Load<GameObject>(SubMenuItemPrefabPath);
|
|
if (subMenuItemPrefab != null)
|
|
{
|
|
RectTransform rt = subMenuItemPrefab.GetComponent<RectTransform>();
|
|
if (rt != null) subMenuItemSize = rt.sizeDelta;
|
|
else ULog.Error($"서브 메뉴 아이템 프리팹 '{SubMenuItemPrefabPath}'에 RectTransform 컴포넌트가 없습니다. 기본 크기 값을 사용합니다.");
|
|
}
|
|
else
|
|
{
|
|
ULog.Error($"서브 메뉴 아이템 프리팹을 Resources 폴더에서 로드할 수 없습니다. 경로: {SubMenuItemPrefabPath}", new System.IO.FileNotFoundException($"Prefab not found at Resources/{SubMenuItemPrefabPath}"));
|
|
}
|
|
|
|
// 메뉴 구분선 프리팹 로드 및 크기 정보 추출
|
|
menuSeparatorPrefab = Resources.Load<GameObject>(MenuSeparatorPrefabPath);
|
|
if (menuSeparatorPrefab != null)
|
|
{
|
|
RectTransform rt = menuSeparatorPrefab.GetComponent<RectTransform>();
|
|
if (rt != null) menuItemSeparatorSize = rt.sizeDelta;
|
|
else ULog.Error($"메뉴 구분선 프리팹 '{MenuSeparatorPrefabPath}'에 RectTransform 컴포넌트가 없습니다. 기본 크기 값을 사용합니다.");
|
|
}
|
|
else
|
|
{
|
|
// 구분선은 선택 사항일 수 있으므로, 경고 수준으로 처리합니다.
|
|
ULog.Warning($"메뉴 구분선 프리팹을 Resources 폴더에서 로드할 수 없습니다. 경로: {MenuSeparatorPrefabPath}. 구분선 기능이 작동하지 않습니다.");
|
|
}
|
|
|
|
// 메뉴 컨테이너 Transform 찾기
|
|
Transform containerTransform = transform.Find(MenuContainerName);
|
|
if (containerTransform != null)
|
|
{
|
|
menuContainer = containerTransform;
|
|
}
|
|
else
|
|
{
|
|
// MenuContainerName으로 지정된 자식 GameObject를 찾지 못한 경우, 현재 GameObject의 Transform을 사용합니다.
|
|
// 이는 TopMenuView GameObject 자체가 메뉴 아이템들의 직접적인 부모가 되는 경우를 의미합니다.
|
|
ULog.Warning($"'{MenuContainerName}'이라는 이름의 자식 GameObject를 찾을 수 없어 현재 Transform을 menuContainer로 사용합니다. 의도된 동작이 아니라면 확인해주세요.");
|
|
menuContainer = transform;
|
|
}
|
|
if (menuContainer == null)
|
|
{
|
|
// menuContainer를 어떤 방식으로도 설정할 수 없는 경우, 에러를 기록합니다.
|
|
// 이는 메뉴 시스템이 정상적으로 작동할 수 없음을 의미합니다.
|
|
ULog.Error("menuContainer를 설정할 수 없습니다. 메뉴 아이템이 표시되지 않습니다.", new InvalidOperationException("menuContainer could not be set."));
|
|
}
|
|
|
|
// UI 블로커 생성
|
|
CreateUIBlocker();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 하위 메뉴가 열렸을 때 다른 UI 요소와의 원치 않는 상호작용을 막기 위한
|
|
/// 투명한 UI 블로커 GameObject를 생성하고 설정합니다.
|
|
/// 블로커는 최상위 Canvas의 자식으로 배치되어 전체 화면을 덮도록 설정됩니다.
|
|
/// </summary>
|
|
protected virtual void CreateUIBlocker()
|
|
{
|
|
uiBlockerInstance = new GameObject("TopMenuUIBlocker");
|
|
// Canvas를 찾아 그 자식으로 설정합니다. 씬에 여러 Canvas가 있다면, 적절한 Canvas를 찾는 로직이 필요할 수 있습니다.
|
|
// 여기서는 FindFirstObjectByType을 사용하여 첫 번째 활성 Canvas를 찾습니다.
|
|
Canvas canvas = FindFirstObjectByType<Canvas>();
|
|
Transform blockerParent = canvas != null ? canvas.transform : transform.parent; // Canvas가 없으면 TopMenuView의 부모를 사용
|
|
|
|
if (blockerParent == null) // 부모를 찾지 못한 극단적인 경우, TopMenuView 자신을 부모로 설정 (권장되지 않음)
|
|
{
|
|
blockerParent = transform;
|
|
ULog.Warning("TopMenuUIBlocker의 부모를 TopMenuView 자신으로 설정합니다. Canvas 하위에 배치하는 것을 권장합니다.");
|
|
}
|
|
uiBlockerInstance.transform.SetParent(blockerParent, false);
|
|
|
|
|
|
// 블로커의 RectTransform 설정 (화면 전체를 덮도록)
|
|
RectTransform blockerRect = uiBlockerInstance.AddComponent<RectTransform>();
|
|
blockerRect.anchorMin = Vector2.zero; // (0,0)
|
|
blockerRect.anchorMax = Vector2.one; // (1,1)
|
|
blockerRect.offsetMin = Vector2.zero; // left, bottom
|
|
blockerRect.offsetMax = Vector2.zero; // right, top
|
|
|
|
// 블로커의 이미지 설정 (클릭 이벤트를 받기 위함, 색상은 거의 투명하게)
|
|
Image blockerImage = uiBlockerInstance.AddComponent<Image>();
|
|
blockerImage.color = new Color(0, 0, 0, 0.001f); // 매우 낮은 알파값으로 거의 보이지 않게 설정
|
|
blockerImage.raycastTarget = true; // Raycast를 받아야 클릭 이벤트 감지 가능
|
|
|
|
// 블로커에 버튼 컴포넌트 추가 및 클릭 이벤트 리스너 설정
|
|
Button blockerButton = uiBlockerInstance.AddComponent<Button>();
|
|
blockerButton.transition = Selectable.Transition.None; // 시각적 변화 없음
|
|
blockerButton.onClick.AddListener(CloseAllOpenSubMenus); // 클릭 시 모든 하위 메뉴 닫기
|
|
|
|
// 블로커의 초기 상태는 비활성화
|
|
uiBlockerInstance.SetActive(false);
|
|
|
|
// 블로커의 렌더링 순서(Sibling Index) 조정: 메뉴 바로 뒤(UI상 바로 뒤)에 오도록 설정
|
|
// menuContainer가 blockerParent의 직계 자식인 경우를 기준으로 함
|
|
if (menuContainer != null && menuContainer.parent == blockerParent)
|
|
{
|
|
// menuContainer 바로 뒤에 오도록 (SiblingIndex가 작을수록 먼저 그려짐 - 뒤에 위치)
|
|
// 블로커가 메뉴보다 먼저 그려져야 메뉴를 가리지 않으면서 메뉴 외 영역 클릭을 막을 수 있음.
|
|
// 하지만 UI 이벤트는 앞쪽에 있는 것부터 받으므로, 블로커가 메뉴보다 뒤에 있어야 함.
|
|
// 따라서, 블로커는 메뉴보다 낮은 SiblingIndex - 1를 가져야 함.
|
|
// 여기서는 메뉴 컨테이너 바로 뒤에 오도록 설정 (블로커가 렌더링된 후 메뉴가 렌더링되어 메뉴를 덮지 않도록)
|
|
// 혼동을 피하기 위해, 블로커는 메뉴 시스템 전체보다 뒤에 위치해야 클릭을 가로챌 수 있습니다.
|
|
// CreateUIBlocker 호출 시점에는 menuContainer의 sibling index가 확정되지 않았을 수 있으므로,
|
|
// UpdateBlockerVisibility에서 최종적으로 조정하는 것이 더 안전할 수 있습니다.
|
|
// 여기서는 일단 menuContainer의 뒤에 두도록 시도합니다.
|
|
uiBlockerInstance.transform.SetSiblingIndex(menuContainer.GetSiblingIndex() - 1);
|
|
}
|
|
else if (transform.parent == blockerParent)
|
|
{
|
|
// TopMenuView 자체가 blockerParent의 자식인 경우, TopMenuView 뒤에 블로커를 둡니다.
|
|
uiBlockerInstance.transform.SetSiblingIndex(transform.GetSiblingIndex() - 1);
|
|
}
|
|
else
|
|
{
|
|
// 그 외의 경우, 블로커를 부모의 마지막 자식으로 보내 UI 요소들 중 가장 앞에 오도록 합니다.
|
|
// 이렇게 하면 다른 UI 요소들의 클릭을 막을 수 있습니다.
|
|
uiBlockerInstance.transform.SetAsLastSibling();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 제공된 MenuItemData 리스트를 기반으로 실제 UI GameObject들을 생성하고 배치합니다.
|
|
/// 이 메서드는 재귀적으로 호출되어 하위 메뉴들도 생성합니다.
|
|
/// </summary>
|
|
/// <param name="items">생성할 메뉴 아이템들의 데이터 리스트입니다.</param>
|
|
/// <param name="parentContainer">생성된 메뉴 아이템들이 자식으로 추가될 부모 Transform입니다.</param>
|
|
/// <param name="depth">현재 메뉴의 깊이입니다. 최상위 메뉴는 0입니다.</param>
|
|
/// <returns>생성된 메뉴 아이템들이 차지하는 전체 영역의 크기 (너비, 높이)를 반환합니다.</returns>
|
|
public virtual Vector2 CreateMenuItems(List<MenuItemData> items, Transform parentContainer, int depth = 0)
|
|
{
|
|
if (items == null || parentContainer == null)
|
|
{
|
|
ULog.Error("메뉴 아이템 생성에 필요한 'items' 또는 'parentContainer'가 null입니다.", new ArgumentNullException(items == null ? "items" : "parentContainer"));
|
|
return Vector2.zero; // 오류 발생 시 크기 0 반환
|
|
}
|
|
|
|
// 현재 레벨(깊이)에서 메뉴 아이템들이 차지할 총 너비와 높이를 계산하기 위한 변수
|
|
float currentLevelCalculatedWidth = 0;
|
|
float currentLevelCalculatedHeight = 0;
|
|
|
|
// 아이템이 없고, 하위 메뉴를 그리는 경우가 아니라면 (depth > 0), 크기는 0
|
|
if (items.Count == 0 && depth > 0)
|
|
{
|
|
return Vector2.zero;
|
|
}
|
|
|
|
for (int i = 0; i < items.Count; i++)
|
|
{
|
|
MenuItemData itemData = items[i];
|
|
GameObject instanceToLayout = null; // 레이아웃을 적용할 GameObject (메뉴 아이템 또는 구분선)
|
|
GameObject prefabToUse = null; // 인스턴스화할 프리팹
|
|
|
|
if (itemData.IsSeparator) // 구분선 아이템인 경우
|
|
{
|
|
if (menuSeparatorPrefab == null)
|
|
{
|
|
ULog.Error("menuSeparatorPrefab이 할당되지 않았습니다. 구분선을 생성할 수 없습니다.", new InvalidOperationException("menuSeparatorPrefab is not loaded."));
|
|
continue; // 다음 아이템으로 넘어감
|
|
}
|
|
GameObject separatorInstance = Instantiate(menuSeparatorPrefab, parentContainer);
|
|
separatorInstance.name = $"Separator_{itemData.ItemId}_Depth{depth}";
|
|
_menuItemObjects[itemData.ItemId] = separatorInstance; // 관리 목록에 추가
|
|
instanceToLayout = separatorInstance;
|
|
}
|
|
else // 일반 메뉴 아이템인 경우
|
|
{
|
|
// 메뉴 깊이에 따라 사용할 프리팹 결정
|
|
if (depth == 0) // 1차 깊이 메뉴 (최상위 메뉴)
|
|
{
|
|
prefabToUse = menuItemPrefab;
|
|
if (prefabToUse == null)
|
|
{
|
|
ULog.Error("menuItemPrefab이 할당되지 않았습니다. 1차 메뉴 아이템을 생성할 수 없습니다.", new InvalidOperationException("menuItemPrefab is not loaded."));
|
|
continue;
|
|
}
|
|
}
|
|
else // 2차 깊이 이상 하위 메뉴
|
|
{
|
|
prefabToUse = subMenuItemPrefab;
|
|
if (prefabToUse == null)
|
|
{
|
|
ULog.Error("subMenuItemPrefab이 할당되지 않았습니다. 하위 메뉴 아이템을 생성할 수 없습니다.", new InvalidOperationException("subMenuItemPrefab is not loaded."));
|
|
continue;
|
|
}
|
|
}
|
|
|
|
GameObject menuItemInstance = Instantiate(prefabToUse, parentContainer);
|
|
menuItemInstance.name = $"MenuItem_{itemData.ItemId}_Depth{depth}";
|
|
_menuItemObjects[itemData.ItemId] = menuItemInstance; // 관리 목록에 추가
|
|
instanceToLayout = menuItemInstance;
|
|
|
|
// 메뉴 아이템 텍스트 설정
|
|
TextMeshProUGUI buttonText = menuItemInstance.GetComponentInChildren<TextMeshProUGUI>(true); // 비활성화된 자식도 검색
|
|
// 메뉴 아이템 버튼 기능 설정
|
|
Button button = menuItemInstance.GetComponent<Button>();
|
|
|
|
if (buttonText != null && !string.IsNullOrEmpty(itemData.DisplayNameKey))
|
|
{
|
|
if (_locManager != null)
|
|
{
|
|
buttonText.text = _locManager.GetString(itemData.DisplayNameKey);
|
|
}
|
|
else
|
|
{
|
|
// LocalizationManager가 없는 경우, 키 값을 그대로 표시 (개발 중 확인 용도)
|
|
ULog.Warning($"LocalizationManager 인스턴스가 없어 메뉴 아이템 텍스트를 키 값으로 설정합니다: {itemData.DisplayNameKey}");
|
|
buttonText.text = itemData.DisplayNameKey;
|
|
}
|
|
}
|
|
|
|
if (button != null)
|
|
{
|
|
button.onClick.RemoveAllListeners(); // 기존 리스너 제거 (프리팹에 설정된 것이 있을 수 있으므로)
|
|
button.onClick.AddListener(() =>
|
|
{
|
|
// 메뉴 아이템 클릭 시 등록된 이벤트 핸들러 호출
|
|
OnMenuItemClicked?.Invoke(itemData);
|
|
|
|
if (itemData.SubMenuItems != null && itemData.SubMenuItems.Count > 0)
|
|
{
|
|
// 하위 메뉴가 있으면 해당 하위 메뉴의 표시 상태를 토글
|
|
ToggleSubMenuDisplay(itemData, menuItemInstance);
|
|
}
|
|
else
|
|
{
|
|
// 하위 메뉴가 없는 아이템 클릭 시 모든 열린 하위 메뉴 닫기 (선택적 동작)
|
|
CloseAllOpenSubMenus();
|
|
}
|
|
});
|
|
// MenuItemData의 IsEnabled 상태에 따라 버튼의 상호작용 가능 여부 설정
|
|
button.interactable = itemData.IsEnabled;
|
|
}
|
|
|
|
// 하위 메뉴가 있는 경우, 하위 메뉴 관련 UI 처리
|
|
if (itemData.SubMenuItems != null && itemData.SubMenuItems.Count > 0)
|
|
{
|
|
// 하위 메뉴 화살표 아이콘 표시
|
|
Transform subMenuArrowTransform = menuItemInstance.transform.Find(SubMenuArrowName);
|
|
if (subMenuArrowTransform != null)
|
|
{
|
|
subMenuArrowTransform.gameObject.SetActive(true);
|
|
}
|
|
|
|
// 하위 메뉴 컨테이너 찾기 및 설정
|
|
Transform subMenuContainerTransform = menuItemInstance.transform.Find(SubMenuContainerName);
|
|
if (subMenuContainerTransform != null)
|
|
{
|
|
RectTransform subMenuRect = subMenuContainerTransform.GetComponent<RectTransform>();
|
|
if (subMenuRect == null)
|
|
{
|
|
ULog.Warning($"{SubMenuContainerName} for '{menuItemInstance.name}' is missing RectTransform. Adding one.");
|
|
subMenuRect = subMenuContainerTransform.gameObject.AddComponent<RectTransform>();
|
|
}
|
|
|
|
// 하위 메뉴 컨테이너의 RectTransform 기본 설정 (앵커, 피벗)
|
|
subMenuRect.anchorMin = new Vector2(0, 1); // 좌상단 기준
|
|
subMenuRect.anchorMax = new Vector2(0, 1); // 좌상단 기준
|
|
subMenuRect.pivot = new Vector2(0, 1); // 좌상단 기준
|
|
|
|
// 하위 메뉴 위치 조정
|
|
if (depth == 0) // 1차 메뉴의 하위 메뉴는 부모 아이템의 오른쪽 아래에 펼쳐짐
|
|
{
|
|
// 부모 메뉴 아이템의 너비만큼 오른쪽으로, 설정된 깊이 간격만큼 아래로 이동
|
|
subMenuRect.anchoredPosition = new Vector2(menuItemSize.x - menuDepthSpace.x, -menuDepthSpace.y);
|
|
}
|
|
else // 2차 이상 메뉴의 하위 메뉴는 부모 아이템의 오른쪽 아래에 펼쳐짐
|
|
{
|
|
// 부모 하위 메뉴 아이템의 너비만큼 오른쪽으로, 설정된 깊이 간격만큼 아래로 이동
|
|
subMenuRect.anchoredPosition = new Vector2(subMenuItemSize.x - subMenuDepthSpace.x, -subMenuDepthSpace.y);
|
|
}
|
|
|
|
// 재귀 호출을 통해 하위 메뉴 아이템들 생성 및 배치
|
|
Vector2 subContentSize = CreateMenuItems(itemData.SubMenuItems, subMenuContainerTransform, depth + 1);
|
|
|
|
// 하위 메뉴 컨테이너의 크기를 내용물(하위 메뉴 아이템들)과 패딩에 맞게 조절
|
|
float containerWidth = subContentSize.x + subContainerPadding.x + subContainerPadding.z; // 좌우 패딩 추가
|
|
float containerHeight = subContentSize.y + subContainerPadding.y + subContainerPadding.w; // 상하 패딩 추가
|
|
subMenuRect.sizeDelta = new Vector2(containerWidth, containerHeight);
|
|
|
|
// 하위 메뉴는 초기에 숨겨진 상태로 설정
|
|
subMenuContainerTransform.gameObject.SetActive(false);
|
|
}
|
|
else
|
|
{
|
|
ULog.Warning($"'{menuItemInstance.name}' 내부에 '{SubMenuContainerName}'를 찾을 수 없습니다. 하위 메뉴가 정상적으로 표시되지 않을 수 있습니다.");
|
|
}
|
|
}
|
|
else // 하위 메뉴가 없는 경우
|
|
{
|
|
// 하위 메뉴 화살표 아이콘 숨김 (프리팹에 기본적으로 활성화되어 있을 수 있으므로)
|
|
Transform existingArrow = menuItemInstance.transform.Find(SubMenuArrowName);
|
|
if (existingArrow != null)
|
|
{
|
|
existingArrow.gameObject.SetActive(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 생성된 메뉴 아이템(또는 구분선)의 레이아웃(위치, 크기) 설정
|
|
if (instanceToLayout != null)
|
|
{
|
|
LayoutMenuItem(instanceToLayout, itemData, depth, i, items);
|
|
}
|
|
|
|
// 현재 레벨의 전체 너비와 높이 누적 계산
|
|
if (depth == 0) // 최상위 메뉴 (수평으로 아이템 나열)
|
|
{
|
|
if (i > 0) currentLevelCalculatedWidth += menuItemSpace.x; // 아이템 간 수평 간격 추가
|
|
// 아이템 너비 추가 (구분선은 구분선 크기 + 좌우 마진, 일반 아이템은 menuItemSize.x)
|
|
currentLevelCalculatedWidth += itemData.IsSeparator ? (menuItemSeparatorSize.x + menuItemSeparatorMargin.x + menuItemSeparatorMargin.z) : menuItemSize.x;
|
|
// 현재 레벨의 최대 높이 갱신 (구분선은 구분선 높이 + 상하 마진, 일반 아이템은 menuItemSize.y)
|
|
currentLevelCalculatedHeight = Mathf.Max(currentLevelCalculatedHeight, itemData.IsSeparator ? (menuItemSeparatorSize.y + menuItemSeparatorMargin.y + menuItemSeparatorMargin.w) : menuItemSize.y);
|
|
}
|
|
else // 하위 메뉴 (수직으로 아이템 나열)
|
|
{
|
|
if (i > 0) currentLevelCalculatedHeight += menuItemSpace.y; // 아이템 간 수직 간격 추가
|
|
|
|
if (itemData.IsSeparator) // 구분선인 경우
|
|
{
|
|
// 구분선 높이 + 상하 마진 추가
|
|
currentLevelCalculatedHeight += menuItemSeparatorSize.y + menuItemSeparatorMargin.y + menuItemSeparatorMargin.w;
|
|
}
|
|
else // 일반 하위 메뉴 아이템인 경우
|
|
{
|
|
currentLevelCalculatedHeight += subMenuItemSize.y; // 하위 메뉴 아이템 높이 추가
|
|
}
|
|
// 현재 레벨의 최대 너비 갱신 (모든 하위 메뉴 아이템은 동일한 너비를 가진다고 가정)
|
|
currentLevelCalculatedWidth = Mathf.Max(currentLevelCalculatedWidth, subMenuItemSize.x);
|
|
}
|
|
}
|
|
return new Vector2(currentLevelCalculatedWidth, currentLevelCalculatedHeight);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// UI 요소(메뉴 아이템 또는 구분선)의 위치와 크기를 설정합니다.
|
|
/// 메뉴의 깊이(depth)와 아이템의 인덱스(itemIndex)에 따라 수평 또는 수직으로 배치합니다.
|
|
/// </summary>
|
|
/// <param name="uiElement">레이아웃을 적용할 메뉴 아이템 또는 구분선 GameObject입니다.</param>
|
|
/// <param name="itemData">해당 UI 요소의 MenuItemData입니다.</param>
|
|
/// <param name="depth">현재 메뉴의 깊이입니다.</param>
|
|
/// <param name="itemIndex">현재 아이템의 부모 컨테이너 내에서의 인덱스입니다.</param>
|
|
/// <param name="siblingItems">현재 아이템과 같은 레벨에 있는 모든 형제 아이템들의 리스트입니다.</param>
|
|
protected virtual void LayoutMenuItem(GameObject uiElement, MenuItemData itemData, int depth, int itemIndex, List<MenuItemData> siblingItems)
|
|
{
|
|
RectTransform rectTransform = uiElement.GetComponent<RectTransform>();
|
|
if (rectTransform == null)
|
|
{
|
|
ULog.Error($"GameObject '{uiElement.name}'에 RectTransform 컴포넌트가 없습니다. 레이아웃을 적용할 수 없습니다.");
|
|
return;
|
|
}
|
|
|
|
// 모든 메뉴 아이템과 구분선의 앵커 및 피벗을 좌상단으로 통일 (계산 편의성)
|
|
rectTransform.anchorMin = new Vector2(0, 1); // 좌상단
|
|
rectTransform.anchorMax = new Vector2(0, 1); // 좌상단
|
|
rectTransform.pivot = new Vector2(0, 1); // 좌상단
|
|
|
|
float currentX = 0; // 계산될 X 위치
|
|
float currentY = 0; // 계산될 Y 위치
|
|
|
|
if (depth == 0) // 1차 깊이 메뉴 (최상위 메뉴, 수평 레이아웃)
|
|
{
|
|
// 초기 위치 설정 (menuContainer의 좌상단 기준)
|
|
currentX = menuItemInitPosition.x;
|
|
currentY = -menuItemInitPosition.y; // Y는 아래로 내려가므로 음수
|
|
|
|
// 현재 아이템 이전의 형제 아이템들의 너비와 간격을 누적하여 X 위치 계산
|
|
for (int k = 0; k < itemIndex; k++)
|
|
{
|
|
MenuItemData sibling = siblingItems[k];
|
|
if (sibling.IsSeparator)
|
|
{
|
|
// 구분선 너비 + 좌우 마진 + 아이템 간 간격
|
|
currentX += menuItemSeparatorSize.x + menuItemSeparatorMargin.x + menuItemSeparatorMargin.z;
|
|
}
|
|
else
|
|
{
|
|
currentX += menuItemSize.x; // 일반 메뉴 아이템 너비
|
|
}
|
|
currentX += menuItemSpace.x; // 아이템 간 수평 간격
|
|
}
|
|
|
|
if (itemData.IsSeparator) // 현재 아이템이 구분선인 경우
|
|
{
|
|
rectTransform.sizeDelta = menuItemSeparatorSize; // 구분선 크기 설정
|
|
// 구분선 위치에 좌측 마진 적용, Y 위치에 상단 마진 적용
|
|
rectTransform.anchoredPosition = new Vector2(currentX + menuItemSeparatorMargin.x, currentY - menuItemSeparatorMargin.y);
|
|
}
|
|
else // 현재 아이템이 일반 메뉴 아이템인 경우
|
|
{
|
|
rectTransform.sizeDelta = menuItemSize; // 메뉴 아이템 크기 설정
|
|
rectTransform.anchoredPosition = new Vector2(currentX, currentY); // 계산된 위치 설정
|
|
}
|
|
}
|
|
else // 2차 깊이 이상 하위 메뉴 (수직 레이아웃)
|
|
{
|
|
// 초기 위치 설정 (부모 SubMenuContainer의 좌상단 기준, 패딩 적용)
|
|
currentX = subContainerPadding.x; // 좌측 패딩
|
|
currentY = -subContainerPadding.y; // 상단 패딩 (Y는 아래로 내려가므로 음수)
|
|
|
|
// 현재 아이템 이전의 형제 아이템들의 높이와 간격을 누적하여 Y 위치 계산
|
|
for (int k = 0; k < itemIndex; k++)
|
|
{
|
|
MenuItemData sibling = siblingItems[k];
|
|
if (sibling.IsSeparator)
|
|
{
|
|
// 구분선 상단 마진 + 구분선 높이 + 구분선 하단 마진 + 아이템 간 간격
|
|
currentY -= (menuItemSeparatorMargin.y + menuItemSeparatorSize.y + menuItemSeparatorMargin.w);
|
|
}
|
|
else
|
|
{
|
|
currentY -= subMenuItemSize.y; // 하위 메뉴 아이템 높이
|
|
}
|
|
currentY -= menuItemSpace.y; // 아이템 간 수직 간격
|
|
}
|
|
|
|
if (itemData.IsSeparator) // 현재 아이템이 구분선인 경우
|
|
{
|
|
// 구분선 Y 위치에 추가로 자신의 상단 마진 적용
|
|
currentY -= menuItemSeparatorMargin.y;
|
|
rectTransform.sizeDelta = menuItemSeparatorSize; // 구분선 크기 설정
|
|
// 구분선 X 위치에 좌측 마진 적용
|
|
rectTransform.anchoredPosition = new Vector2(currentX + menuItemSeparatorMargin.x, currentY);
|
|
}
|
|
else // 현재 아이템이 일반 하위 메뉴 아이템인 경우
|
|
{
|
|
rectTransform.sizeDelta = subMenuItemSize; // 하위 메뉴 아이템 크기 설정
|
|
rectTransform.anchoredPosition = new Vector2(currentX, currentY); // 계산된 위치 설정
|
|
}
|
|
}
|
|
// 최종 계산된 anchoredPosition을 다시 한번 설정 (depth 0 일반 아이템의 경우 중복될 수 있으나 일관성을 위해)
|
|
// rectTransform.anchoredPosition = new Vector2(currentX, currentY); // 이 줄은 depth 0 일반 아이템의 경우 이미 위에서 설정됨. 조건부로 하거나 위의 로직을 재검토.
|
|
// 현재 로직에서는 각 분기 내에서 anchoredPosition을 설정하므로 이 줄은 불필요하거나,
|
|
// depth 0 일반 아이템의 X 위치 계산 후 여기서 Y와 함께 최종 설정하는 방식으로 변경 가능.
|
|
// 현재는 각 분기에서 설정하는 것이 명확해 보임.
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// 특정 메뉴 아이템에 연결된 하위 메뉴의 표시 상태를 토글(열기/닫기)합니다.
|
|
/// 하위 메뉴를 열 때, 같은 레벨의 다른 열려있는 하위 메뉴들은 닫습니다.
|
|
/// </summary>
|
|
/// <param name="itemData">토글할 하위 메뉴를 가진 부모 메뉴의 MenuItemData입니다.</param>
|
|
/// <param name="menuItemInstance">부모 메뉴 아이템의 GameObject입니다.</param>
|
|
protected virtual void ToggleSubMenuDisplay(MenuItemData itemData, GameObject menuItemInstance)
|
|
{
|
|
if (itemData.IsSeparator) return; // 구분선은 하위 메뉴를 가질 수 없음
|
|
|
|
// 메뉴 아이템 내부에서 SubMenuContainer 이름의 GameObject를 찾음
|
|
Transform subMenuContainer = menuItemInstance.transform.Find(SubMenuContainerName);
|
|
if (subMenuContainer != null)
|
|
{
|
|
bool isActive = subMenuContainer.gameObject.activeSelf; // 현재 하위 메뉴의 활성화 상태
|
|
bool becomingActive = !isActive; // 토글 후 활성화될 상태
|
|
|
|
if (becomingActive) // 하위 메뉴가 열리려고 하는 경우
|
|
{
|
|
// 현재 클릭된 메뉴 아이템의 부모 컨테이너에서, 이 하위 메뉴를 제외한 다른 모든 하위 메뉴를 닫음
|
|
CloseOtherSubMenusInParent(menuItemInstance.transform.parent, subMenuContainer);
|
|
|
|
// 하위 메뉴를 포함하는 menuItemInstance를 그 부모 컨테이너 내에서 가장 앞으로 가져와서
|
|
// 다른 형제 메뉴 아이템에 가려지지 않도록 합니다. (렌더링 순서 조정)
|
|
menuItemInstance.transform.SetAsLastSibling();
|
|
}
|
|
|
|
// 하위 메뉴 컨테이너의 활성화 상태를 토글
|
|
subMenuContainer.gameObject.SetActive(becomingActive);
|
|
|
|
// isAnySubMenuOpen 상태 및 UI 블로커 업데이트
|
|
if (becomingActive)
|
|
{
|
|
isAnySubMenuOpen = true; // 하나라도 열리면 true
|
|
}
|
|
else
|
|
{
|
|
// 하위 메뉴가 닫혔으므로, 다른 열린 하위 메뉴가 있는지 다시 확인
|
|
CheckIfAnySubMenuRemainsOpen();
|
|
}
|
|
UpdateBlockerVisibility(); // UI 블로커 표시 여부 업데이트
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 특정 부모 컨테이너 내에서, 지정된 하위 메뉴(subMenuToExclude)를 제외한 모든 다른 열려있는 하위 메뉴들을 닫습니다.
|
|
/// 이는 한 번에 하나의 하위 메뉴만 열려 있도록 보장하는 데 사용됩니다.
|
|
/// </summary>
|
|
/// <param name="currentMenuItemsParent">하위 메뉴들을 검사할 부모 Transform입니다. (예: TopMenu 또는 다른 SubMenuContainer)</param>
|
|
/// <param name="subMenuToExclude">닫지 않고 유지할 특정 하위 메뉴의 Transform입니다. (보통 새로 열리려는 하위 메뉴)</param>
|
|
protected virtual void CloseOtherSubMenusInParent(Transform currentMenuItemsParent, Transform subMenuToExclude)
|
|
{
|
|
if (currentMenuItemsParent == null)
|
|
{
|
|
ULog.Warning("CloseOtherSubMenusInParent 호출 시 currentMenuItemsParent가 null입니다. 다른 하위 메뉴를 닫을 수 없습니다.");
|
|
return;
|
|
}
|
|
|
|
// 부모 컨테이너의 모든 자식 메뉴 아이템들을 순회
|
|
for (int i = 0; i < currentMenuItemsParent.childCount; i++)
|
|
{
|
|
Transform siblingMenuItemTransform = currentMenuItemsParent.GetChild(i);
|
|
|
|
// 자기 자신이거나, 제외할 하위 메뉴의 부모 메뉴 아이템인 경우는 건너뜀
|
|
if (siblingMenuItemTransform == null || siblingMenuItemTransform == subMenuToExclude?.parent) continue;
|
|
|
|
// 형제 메뉴 아이템 내부에서 SubMenuContainer를 찾음
|
|
Transform potentialSubMenu = siblingMenuItemTransform.Find(SubMenuContainerName);
|
|
|
|
// 찾은 SubMenuContainer가 존재하고, 제외할 하위 메뉴가 아니며, 현재 활성화 상태라면 비활성화시킴
|
|
if (potentialSubMenu != null && potentialSubMenu != subMenuToExclude && potentialSubMenu.gameObject.activeSelf)
|
|
{
|
|
potentialSubMenu.gameObject.SetActive(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 현재 열려있는 모든 하위 메뉴들을 닫습니다.
|
|
/// 주로 메뉴 외부 영역 클릭(UI 블로커 클릭) 시 또는 최상위 메뉴 아이템 클릭 시 호출됩니다.
|
|
/// </summary>
|
|
public virtual void CloseAllOpenSubMenus()
|
|
{
|
|
bool anyActuallyClosed = false; // 실제로 닫힌 하위 메뉴가 있었는지 추적
|
|
|
|
// _menuItemObjects에 저장된 모든 메뉴 아이템 GameObject들을 순회
|
|
foreach (GameObject menuItemGO in _menuItemObjects.Values)
|
|
{
|
|
if (menuItemGO == null) continue; // null 체크
|
|
|
|
// 각 메뉴 아이템에서 SubMenuContainer를 찾아 비활성화
|
|
// 구분선 아이템은 SubMenuContainer가 없으므로 Find 결과가 null이 되어 자동으로 처리됨.
|
|
Transform subMenuContainer = menuItemGO.transform.Find(SubMenuContainerName);
|
|
if (subMenuContainer != null && subMenuContainer.gameObject.activeSelf)
|
|
{
|
|
subMenuContainer.gameObject.SetActive(false);
|
|
anyActuallyClosed = true; // 하나라도 닫았으면 true
|
|
}
|
|
}
|
|
|
|
// 실제로 메뉴가 닫혔거나, 이전에 어떤 메뉴든 열려 있었다면 상태 업데이트
|
|
if (anyActuallyClosed || isAnySubMenuOpen)
|
|
{
|
|
isAnySubMenuOpen = false; // 모든 하위 메뉴가 닫혔으므로 false로 설정
|
|
UpdateBlockerVisibility(); // UI 블로커 숨김 처리
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 현재 열려있는 하위 메뉴가 있는지 확인하고, `isAnySubMenuOpen` 상태를 업데이트합니다.
|
|
/// </summary>
|
|
protected virtual void CheckIfAnySubMenuRemainsOpen()
|
|
{
|
|
isAnySubMenuOpen = false; // 일단 false로 가정
|
|
foreach (GameObject menuItemGO in _menuItemObjects.Values)
|
|
{
|
|
if (menuItemGO == null) continue;
|
|
|
|
Transform subMenuContainer = menuItemGO.transform.Find(SubMenuContainerName);
|
|
if (subMenuContainer != null && subMenuContainer.gameObject.activeSelf)
|
|
{
|
|
isAnySubMenuOpen = true; // 하나라도 열려있으면 true로 설정하고 반복 종료
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// `isAnySubMenuOpen` 상태에 따라 UI 블로커의 활성화/비활성화 상태를 업데이트합니다.
|
|
/// 블로커가 활성화될 때는 메뉴 시스템 바로 뒤(UI 상으로는 바로 앞)에 오도록 렌더링 순서를 조정합니다.
|
|
/// </summary>
|
|
protected virtual void UpdateBlockerVisibility()
|
|
{
|
|
if (uiBlockerInstance != null)
|
|
{
|
|
uiBlockerInstance.SetActive(isAnySubMenuOpen); // isAnySubMenuOpen 값에 따라 블로커 활성화/비활성화
|
|
|
|
if (isAnySubMenuOpen)
|
|
{
|
|
// 블로커가 활성화될 때, 렌더링 순서를 조정하여 다른 메뉴 요소들보다 뒤에 오도록 합니다.
|
|
// CreateUIBlocker에서 이미 부모와 기본적인 Sibling Index가 설정되었으므로,
|
|
// 여기서는 SetAsLastSibling을 호출하여 해당 부모 내에서 메뉴 바로 뒤에 오도록 설정합니다.
|
|
// 이렇게 하면 블로커가 다른 모든 UI 요소들의 클릭을 가로챌 수 있습니다.
|
|
Transform blockerParent = uiBlockerInstance.transform.parent;
|
|
if (menuContainer != null && menuContainer.parent == blockerParent)
|
|
{
|
|
// menuContainer 바로 뒤에 오도록 (SiblingIndex가 작을수록 먼저 그려짐 - 뒤에 위치)
|
|
// 블로커가 메뉴보다 먼저 그려져야 메뉴를 가리지 않으면서 메뉴 외 영역 클릭을 막을 수 있음.
|
|
// 하지만 UI 이벤트는 앞쪽에 있는 것부터 받으므로, 블로커가 메뉴보다 뒤에 있어야 함.
|
|
// 따라서, 블로커는 메뉴보다 낮은 SiblingIndex - 1를 가져야 함.
|
|
// 여기서는 메뉴 컨테이너 바로 뒤에 오도록 설정 (블로커가 렌더링된 후 메뉴가 렌더링되어 메뉴를 덮지 않도록)
|
|
// 혼동을 피하기 위해, 블로커는 메뉴 시스템 전체보다 뒤에 위치해야 클릭을 가로챌 수 있습니다.
|
|
// CreateUIBlocker 호출 시점에는 menuContainer의 sibling index가 확정되지 않았을 수 있으므로,
|
|
// UpdateBlockerVisibility에서 최종적으로 조정하는 것이 더 안전할 수 있습니다.
|
|
// 여기서는 일단 menuContainer의 뒤에 두도록 시도합니다.
|
|
uiBlockerInstance.transform.SetSiblingIndex(menuContainer.GetSiblingIndex() - 1);
|
|
}
|
|
else if (transform.parent == blockerParent)
|
|
{
|
|
// TopMenuView 자체가 blockerParent의 자식인 경우, TopMenuView 뒤에 블로커를 둡니다.
|
|
uiBlockerInstance.transform.SetSiblingIndex(transform.GetSiblingIndex() - 1);
|
|
}
|
|
else
|
|
{
|
|
// 그 외의 경우, 블로커를 부모의 마지막 자식으로 보내 UI 요소들 중 가장 앞에 오도록 합니다.
|
|
// 이렇게 하면 다른 UI 요소들의 클릭을 막을 수 있습니다.
|
|
uiBlockerInstance.transform.SetAsLastSibling();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// 화면에 표시된 모든 메뉴 아이템들을 제거하고 관련 내부 데이터도 초기화합니다.
|
|
/// </summary>
|
|
public virtual void ClearMenuItems()
|
|
{
|
|
CloseAllOpenSubMenus(); // 먼저 열려있는 모든 하위 메뉴를 닫습니다.
|
|
|
|
// _menuItemObjects에 저장된 모든 GameObject들을 파괴하고 딕셔너리를 비웁니다.
|
|
foreach (var pair in _menuItemObjects)
|
|
{
|
|
if (pair.Value != null) Destroy(pair.Value);
|
|
}
|
|
_menuItemObjects.Clear();
|
|
|
|
// menuContainer의 모든 자식 GameObject들을 직접 파괴합니다.
|
|
// 이는 _menuItemObjects에 등록되지 않은 자식 요소(예: 동적으로 추가된 다른 요소)도 제거합니다.
|
|
if (menuContainer != null)
|
|
{
|
|
foreach (Transform child in menuContainer)
|
|
{
|
|
// 자식 GameObject를 직접 파괴합니다.
|
|
// Instantiate로 생성된 객체들이므로 Destroy로 제거하는 것이 맞습니다.
|
|
Destroy(child.gameObject);
|
|
}
|
|
}
|
|
// menuContainer 자체를 파괴하는 것이 아니라 그 내용물만 비우는 것입니다.
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// 제공된 MenuItemData 리스트를 기반으로 화면에 표시된 모든 메뉴 아이템들의 텍스트를 업데이트합니다.
|
|
/// 주로 언어 변경 시 호출됩니다.
|
|
/// </summary>
|
|
/// <param name="items">최상위 메뉴 아이템들의 데이터 리스트입니다.</param>
|
|
public virtual void UpdateAllMenuTexts(List<MenuItemData> items)
|
|
{
|
|
if (_locManager == null)
|
|
{
|
|
ULog.Warning("LocalizationManager가 없어 메뉴 텍스트를 업데이트할 수 없습니다.");
|
|
return;
|
|
}
|
|
// 재귀 함수를 호출하여 모든 메뉴 아이템(하위 메뉴 포함)의 텍스트를 업데이트합니다.
|
|
UpdateMenuTextsRecursive(items);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 재귀적으로 메뉴 아이템들의 텍스트를 업데이트하는 내부 도우미 함수입니다.
|
|
/// </summary>
|
|
/// <param name="items">텍스트를 업데이트할 메뉴 아이템들의 데이터 리스트입니다.</param>
|
|
protected virtual void UpdateMenuTextsRecursive(List<MenuItemData> items)
|
|
{
|
|
if (items == null) return;
|
|
|
|
foreach (var itemData in items)
|
|
{
|
|
if (itemData.IsSeparator) continue; // 구분선은 텍스트가 없음
|
|
|
|
// _menuItemObjects에서 해당 아이템의 GameObject를 찾음
|
|
if (_menuItemObjects.TryGetValue(itemData.ItemId, out GameObject menuItemInstance))
|
|
{
|
|
// GameObject에서 TextMeshProUGUI 컴포넌트를 찾아 텍스트 업데이트
|
|
TextMeshProUGUI buttonText = menuItemInstance.GetComponentInChildren<TextMeshProUGUI>(true);
|
|
if (buttonText != null && !string.IsNullOrEmpty(itemData.DisplayNameKey))
|
|
{
|
|
buttonText.text = _locManager.GetString(itemData.DisplayNameKey);
|
|
}
|
|
}
|
|
|
|
// 하위 메뉴가 있으면 재귀적으로 호출하여 하위 메뉴 아이템들의 텍스트도 업데이트
|
|
if (itemData.SubMenuItems != null && itemData.SubMenuItems.Count > 0)
|
|
{
|
|
UpdateMenuTextsRecursive(itemData.SubMenuItems);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 메뉴 아이템의 고유 ID를 사용하여 해당 메뉴 아이템의 GameObject를 가져옵니다.
|
|
/// </summary>
|
|
/// <param name="itemId">찾고자 하는 메뉴 아이템의 고유 ID입니다.</param>
|
|
/// <param name="menuItemGO">찾은 경우, 해당 GameObject가 할당됩니다. 찾지 못한 경우 null입니다.</param>
|
|
/// <returns>메뉴 아이템을 찾았으면 true, 그렇지 않으면 false를 반환합니다.</returns>
|
|
public bool TryGetMenuItemGameObject(string itemId, out GameObject menuItemGO)
|
|
{
|
|
return _menuItemObjects.TryGetValue(itemId, out menuItemGO);
|
|
}
|
|
}
|
|
} |