2025-08-05 20:18:01 +09:00
#nullable enable
2025-06-18 20:09:16 +09:00
using System ;
2025-06-11 19:24:08 +09:00
using System.Collections.Generic ;
2025-06-18 20:09:16 +09:00
using System.Linq ;
2025-06-11 19:24:08 +09:00
using TMPro ;
using UnityEngine ;
using UnityEngine.UI ;
2025-06-18 20:09:16 +09:00
using UVC.Extension ;
2025-06-12 19:25:33 +09:00
using UVC.Locale ; // 다국어 처리 네임스페이스
2025-06-11 19:24:08 +09:00
namespace UVC.UI.Menu
{
2025-06-12 19:25:33 +09:00
/// <summary>
/// 상단 메뉴 UI의 표시 및 상호작용을 관리하는 클래스입니다.
/// MenuItemData를 기반으로 실제 Unity UI 요소들을 생성하고 배치하며, 사용자 입력을 처리합니다.
/// 이 클래스는 상속을 통해 특정 프로젝트의 요구사항에 맞게 커스터마이징될 수 있도록 설계되었습니다.
/// </summary>
/// <example>
/// <code>
/// <summary>
/// TopMenuView를 상속받아 특정 기능을 커스터마이징하는 예제 클래스입니다.
/// 예를 들어, 다른 프리팹 경로를 사용하거나 메뉴 아이템 레이아웃 방식을 변경할 수 있습니다.
/// </summary>
/// public class CustomTopMenuView : TopMenuView
/// {
/// /// <summary>
/// /// MonoBehaviour의 Awake 메시지입니다.
/// /// 부모 클래스의 Awake를 호출한 후, 추가적인 커스텀 초기화 로직을 수행합니다.
/// /// </summary>
/// protected override void Awake()
/// {
/// // 부모 클래스(TopMenuView)의 Awake 로직을 먼저 실행합니다.
/// // 이렇게 하면 프리팹 로드, 기본 설정 등이 먼저 처리됩니다.
/// base.Awake();
///
/// // 여기에 CustomTopMenuView만의 추가적인 초기화 코드를 작성할 수 있습니다.
/// ULog.Debug("CustomTopMenuView Awake 실행됨. 커스텀 프리팹 경로가 사용됩니다.");
/// }
///
/// /// <summary>
/// /// 메뉴 아이템이 클릭되었을 때 호출되는 이벤트 핸들러를 커스터마이징 할 수 있습니다.
/// /// (참고: OnMenuItemClicked 이벤트는 TopMenuController에서 구독하여 주로 처리합니다.
/// /// View에서 직접 처리해야 하는 로직이 있다면 이와 유사한 방식으로 메서드를 오버라이드 하거나
/// /// Awake 등에서 이벤트 핸들러를 추가/제거 할 수 있습니다.)
/// /// 이 예제에서는 ToggleSubMenuDisplay를 오버라이드하여 하위 메뉴 토글 시 로그를 남깁니다.
/// /// </summary>
/// protected override void ToggleSubMenuDisplay(MenuItemData itemData, GameObject menuItemInstance)
/// {
2025-06-18 20:09:16 +09:00
/// ULog.Debug($"Custom ToggleSubMenuDisplay: {itemData.DisplayName} 클릭됨.");
2025-06-12 19:25:33 +09:00
/// base.ToggleSubMenuDisplay(itemData, menuItemInstance); // 부모의 원래 로직 실행
/// }
///
/// // 필요에 따라 다른 virtual 메서드들 (CreateMenuItems, ClearMenuItems 등)도
/// // 오버라이드하여 동작을 변경하거나 확장할 수 있습니다.
/// }
/// </code>
/// </example>
2025-06-11 19:24:08 +09:00
public class TopMenuView : MonoBehaviour
{
2025-06-18 20:09:16 +09:00
[Header("UI References")]
[Tooltip("메인 메뉴 아이템을 위한 프리팹입니다.")]
[SerializeField] public GameObject menuItemPrefab ;
2025-06-12 19:25:33 +09:00
2025-06-18 20:09:16 +09:00
[Tooltip("하위 메뉴 아이템을 위한 프리팹입니다.")]
[SerializeField] public GameObject subMenuItemPrefab ;
2025-06-12 19:25:33 +09:00
2025-06-18 20:09:16 +09:00
[Tooltip("메뉴 구분선을 위한 프리팹입니다. (선택 사항)")]
[SerializeField] public GameObject menuSeparatorPrefab ;
[Tooltip("최상위 메뉴 아이템들이 배치될 부모 Transform입니다.")]
[SerializeField] public Transform menuContainer ;
[Tooltip("하위 메뉴 아이템들이 배치될 부모 Transform입니다.")]
[SerializeField] public Transform subMenuContainer ;
[Header("UI Element Names")]
[Tooltip("하위 메뉴가 있음을 나타내는 화살표 UI GameObject의 이름입니다. (메뉴 아이템 프리팹 내부에 존재)")]
[SerializeField] public string subMenuArrowName = "SubMenuArrow" ;
[Header("Layout Settings")]
[Tooltip("1차 메뉴 아이템과 그 하위 메뉴 컨테이너 간의 간격 (수평, 수직)")]
[SerializeField] public Vector2 menuDepthSpace = new Vector2 ( 0 , - 5 ) ;
[Tooltip("하위 메뉴 아이템과 그 하위 메뉴 컨테이너 간의 간격 (수평, 수직)")]
[SerializeField] public Vector2 subMenuDepthSpace = new Vector2 ( - 5 , 0 ) ;
[Tooltip("1 depth 메뉴를 넓이를 글자 크기로 줄일지 여부")]
[SerializeField] public bool isShrinkMenuItemWidth = true ;
[Tooltip("isShrinkMenuItemWidth가 활성화됐을 때, 텍스트 좌우에 추가될 여백입니다.")]
[SerializeField] public float menuItemWidthPadding = 20f ;
2025-06-11 19:24:08 +09:00
2025-06-12 19:25:33 +09:00
/// <summary>
/// 메뉴 아이템이 클릭되었을 때 발생하는 이벤트입니다.
/// 클릭된 메뉴의 MenuItemData를 전달합니다.
/// </summary>
2025-06-11 19:24:08 +09:00
public event Action < MenuItemData > OnMenuItemClicked ;
2025-06-12 19:25:33 +09:00
// --- 내부 상태 변수 ---
// 생성된 메뉴 아이템 GameObject들을 관리하는 딕셔너리 (키: ItemId, 값: GameObject)
2025-06-11 19:24:08 +09:00
protected Dictionary < string , GameObject > _menuItemObjects = new Dictionary < string , GameObject > ( ) ;
2025-06-18 20:09:16 +09:00
protected Dictionary < string , GameObject > _subMenuContainerObjects = new Dictionary < string , GameObject > ( ) ;
protected Dictionary < string , MenuItemData > _menuItemDataMap = new Dictionary < string , MenuItemData > ( ) ;
2025-06-12 19:25:33 +09:00
protected LocalizationManager _locManager ; // 다국어 처리를 위한 LocalizationManager 인스턴스
protected GameObject uiBlockerInstance ; // 하위 메뉴가 열렸을 때 다른 UI 상호작용을 막기 위한 투명한 UI 요소
protected bool isAnySubMenuOpen = false ; // 하나 이상의 하위 메뉴가 열려있는지 여부
/// <summary>
/// MonoBehaviour의 Awake 메시지입니다.
/// 메뉴 시스템 초기화에 필요한 리소스 로드 및 참조 설정을 수행합니다.
/// 상속 클래스에서 이 메서드를 오버라이드하여 추가적인 초기화 로직을 구현할 수 있습니다.
/// 오버라이드 시 `base.Awake()`를 호출하는 것을 권장합니다.
/// </summary>
2025-06-11 19:24:08 +09:00
protected virtual void Awake ( )
{
_locManager = LocalizationManager . Instance ;
if ( _locManager = = null )
{
2025-06-12 19:25:33 +09:00
// LocalizationManager가 필수적인 경우, 여기서 에러를 발생시키거나 게임을 중단시킬 수 있습니다.
// 여기서는 경고만 기록하고 진행합니다.
2025-08-09 01:39:24 +09:00
Debug . LogWarning ( "LocalizationManager 인스턴스를 찾을 수 없습니다. 메뉴 텍스트가 제대로 표시되지 않을 수 있습니다." ) ;
2025-06-11 19:24:08 +09:00
}
2025-06-18 20:09:16 +09:00
// Inspector에서 할당된 참조 확인
if ( menuItemPrefab = = null )
2025-06-11 19:24:08 +09:00
{
2025-08-09 01:39:24 +09:00
Debug . LogWarning ( "menuItemPrefab이 Inspector에서 할당되지 않았습니다." , this ) ;
2025-06-11 19:24:08 +09:00
}
2025-06-18 20:09:16 +09:00
if ( subMenuItemPrefab = = null )
2025-06-12 19:25:33 +09:00
{
2025-08-09 01:39:24 +09:00
Debug . LogWarning ( "subMenuItemPrefab이 Inspector에서 할당되지 않았습니다." , this ) ;
2025-06-12 19:25:33 +09:00
}
2025-06-18 20:09:16 +09:00
if ( menuSeparatorPrefab = = null )
2025-06-12 19:25:33 +09:00
{
// 구분선은 선택 사항일 수 있으므로, 경고 수준으로 처리합니다.
2025-06-18 20:09:16 +09:00
Debug . LogWarning ( "menuSeparatorPrefab이 Inspector에서 할당되지 않았습니다. 구분선 기능이 작동하지 않습니다." , this ) ;
2025-06-11 19:24:08 +09:00
}
2025-06-18 20:09:16 +09:00
// 메뉴 컨테이너 확인
if ( menuContainer = = null )
2025-06-11 19:24:08 +09:00
{
2025-08-09 01:39:24 +09:00
Debug . LogWarning ( "menuContainer가 Inspector에서 할당되지 않았습니다. Inspector에서 참조를 설정해주세요." , this ) ;
2025-06-11 19:24:08 +09:00
}
else
{
2025-06-18 20:09:16 +09:00
// 메뉴 컨테이너에 LayoutGroup이 있는지 확인하고, 없다면 경고를 표시합니다.
// 자동 배치를 위해 HorizontalLayoutGroup 또는 VerticalLayoutGroup이 필요합니다.
if ( menuContainer . GetComponent < LayoutGroup > ( ) = = null )
{
Debug . LogWarning ( $"menuContainer '{menuContainer.name}'에 LayoutGroup 컴포넌트가 없습니다. 메뉴 아이템이 자동으로 배치되지 않을 수 있습니다. Inspector에서 HorizontalLayoutGroup 또는 VerticalLayoutGroup을 추가해주세요." ) ;
}
2025-06-11 19:24:08 +09:00
}
2025-06-12 19:25:33 +09:00
// UI 블로커 생성
2025-06-11 19:24:08 +09:00
CreateUIBlocker ( ) ;
}
2025-06-12 19:25:33 +09:00
/// <summary>
/// 하위 메뉴가 열렸을 때 다른 UI 요소와의 원치 않는 상호작용을 막기 위한
/// 투명한 UI 블로커 GameObject를 생성하고 설정합니다.
/// 블로커는 최상위 Canvas의 자식으로 배치되어 전체 화면을 덮도록 설정됩니다.
/// </summary>
2025-06-11 19:24:08 +09:00
protected virtual void CreateUIBlocker ( )
{
uiBlockerInstance = new GameObject ( "TopMenuUIBlocker" ) ;
2025-06-12 19:25:33 +09:00
// Canvas를 찾아 그 자식으로 설정합니다. 씬에 여러 Canvas가 있다면, 적절한 Canvas를 찾는 로직이 필요할 수 있습니다.
2025-06-23 20:06:15 +09:00
Canvas canvas = GetComponentInParent < Canvas > ( ) ;
2025-06-12 19:25:33 +09:00
Transform blockerParent = canvas ! = null ? canvas . transform : transform . parent ; // Canvas가 없으면 TopMenuView의 부모를 사용
2025-06-11 19:24:08 +09:00
2025-06-12 19:25:33 +09:00
if ( blockerParent = = null ) // 부모를 찾지 못한 극단적인 경우, TopMenuView 자신을 부모로 설정 (권장되지 않음)
2025-06-11 19:24:08 +09:00
{
blockerParent = transform ;
2025-06-18 20:09:16 +09:00
Debug . LogWarning ( "TopMenuUIBlocker의 부모를 TopMenuView 자신으로 설정합니다. Canvas 하위에 배치하는 것을 권장합니다." ) ;
2025-06-11 19:24:08 +09:00
}
2025-06-12 19:25:33 +09:00
uiBlockerInstance . transform . SetParent ( blockerParent , false ) ;
2025-06-11 19:24:08 +09:00
2025-06-12 19:25:33 +09:00
// 블로커의 RectTransform 설정 (화면 전체를 덮도록)
2025-06-11 19:24:08 +09:00
RectTransform blockerRect = uiBlockerInstance . AddComponent < RectTransform > ( ) ;
2025-06-12 19:25:33 +09:00
blockerRect . anchorMin = Vector2 . zero ; // (0,0)
blockerRect . anchorMax = Vector2 . one ; // (1,1)
blockerRect . offsetMin = Vector2 . zero ; // left, bottom
blockerRect . offsetMax = Vector2 . zero ; // right, top
2025-06-11 19:24:08 +09:00
2025-06-12 19:25:33 +09:00
// 블로커의 이미지 설정 (클릭 이벤트를 받기 위함, 색상은 거의 투명하게)
2025-06-11 19:24:08 +09:00
Image blockerImage = uiBlockerInstance . AddComponent < Image > ( ) ;
2025-06-12 19:25:33 +09:00
blockerImage . color = new Color ( 0 , 0 , 0 , 0.001f ) ; // 매우 낮은 알파값으로 거의 보이지 않게 설정
blockerImage . raycastTarget = true ; // Raycast를 받아야 클릭 이벤트 감지 가능
2025-06-11 19:24:08 +09:00
2025-06-12 19:25:33 +09:00
// 블로커에 버튼 컴포넌트 추가 및 클릭 이벤트 리스너 설정
2025-06-11 19:24:08 +09:00
Button blockerButton = uiBlockerInstance . AddComponent < Button > ( ) ;
2025-06-12 19:25:33 +09:00
blockerButton . transition = Selectable . Transition . None ; // 시각적 변화 없음
blockerButton . onClick . AddListener ( CloseAllOpenSubMenus ) ; // 클릭 시 모든 하위 메뉴 닫기
2025-06-11 19:24:08 +09:00
2025-06-12 19:25:33 +09:00
// 블로커의 초기 상태는 비활성화
2025-06-11 19:24:08 +09:00
uiBlockerInstance . SetActive ( false ) ;
2025-06-12 19:25:33 +09:00
// 블로커의 렌더링 순서(Sibling Index) 조정: 메뉴 바로 뒤(UI상 바로 뒤)에 오도록 설정
// menuContainer가 blockerParent의 직계 자식인 경우를 기준으로 함
if ( menuContainer ! = null & & menuContainer . parent = = blockerParent )
{
// menuContainer 바로 뒤에 오도록 (SiblingIndex가 작을수록 먼저 그려짐 - 뒤에 위치)
// 블로커가 메뉴보다 먼저 그려져야 메뉴를 가리지 않으면서 메뉴 외 영역 클릭을 막을 수 있음.
// 하지만 UI 이벤트는 앞쪽에 있는 것부터 받으므로, 블로커가 메뉴보다 뒤에 있어야 함.
2025-06-18 20:09:16 +09:00
// 따라서, 블로커는 메뉴 시스템 전체보다 뒤에 위치해야 클릭을 가로챌 수 있습니다.
2025-06-12 19:25:33 +09:00
// 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 ( ) ;
}
2025-06-11 19:24:08 +09:00
}
2025-06-12 19:25:33 +09:00
/// <summary>
/// 제공된 MenuItemData 리스트를 기반으로 실제 UI GameObject들을 생성하고 배치합니다.
/// 이 메서드는 재귀적으로 호출되어 하위 메뉴들도 생성합니다.
/// </summary>
/// <param name="items">생성할 메뉴 아이템들의 데이터 리스트입니다.</param>
/// <param name="parentContainer">생성된 메뉴 아이템들이 자식으로 추가될 부모 Transform입니다.</param>
/// <param name="depth">현재 메뉴의 깊이입니다. 최상위 메뉴는 0입니다.</param>
2025-06-18 20:09:16 +09:00
public virtual void CreateMenuItems ( List < MenuItemData > items , Transform parentContainer , int depth = 0 )
2025-06-11 19:24:08 +09:00
{
if ( items = = null | | parentContainer = = null )
{
2025-06-18 20:09:16 +09:00
Debug . LogError ( "메뉴 아이템 생성에 필요한 'items' 또는 'parentContainer'가 null입니다." ) ;
return ;
2025-06-11 19:24:08 +09:00
}
if ( items . Count = = 0 & & depth > 0 )
{
2025-06-18 20:09:16 +09:00
return ;
2025-06-11 19:24:08 +09:00
}
for ( int i = 0 ; i < items . Count ; i + + )
{
MenuItemData itemData = items [ i ] ;
2025-06-18 20:09:16 +09:00
_menuItemDataMap [ itemData . ItemId ] = itemData ;
GameObject prefabToUse = null ;
2025-06-11 19:24:08 +09:00
2025-06-12 19:25:33 +09:00
if ( itemData . IsSeparator ) // 구분선 아이템인 경우
2025-06-11 19:24:08 +09:00
{
if ( menuSeparatorPrefab = = null )
{
2025-06-18 20:09:16 +09:00
Debug . LogError ( "menuSeparatorPrefab이 할당되지 않았습니다. 구분선을 생성할 수 없습니다." ) ;
2025-06-12 19:25:33 +09:00
continue ; // 다음 아이템으로 넘어감
2025-06-11 19:24:08 +09:00
}
GameObject separatorInstance = Instantiate ( menuSeparatorPrefab , parentContainer ) ;
separatorInstance . name = $"Separator_{itemData.ItemId}_Depth{depth}" ;
2025-06-12 19:25:33 +09:00
_menuItemObjects [ itemData . ItemId ] = separatorInstance ; // 관리 목록에 추가
2025-06-11 19:24:08 +09:00
}
2025-06-12 19:25:33 +09:00
else // 일반 메뉴 아이템인 경우
2025-06-11 19:24:08 +09:00
{
2025-06-12 19:25:33 +09:00
// 메뉴 깊이에 따라 사용할 프리팹 결정
if ( depth = = 0 ) // 1차 깊이 메뉴 (최상위 메뉴)
2025-06-11 19:24:08 +09:00
{
prefabToUse = menuItemPrefab ;
if ( prefabToUse = = null )
{
2025-06-18 20:09:16 +09:00
Debug . LogError ( "menuItemPrefab이 할당되지 않았습니다. 1차 메뉴 아이템을 생성할 수 없습니다." ) ;
2025-06-11 19:24:08 +09:00
continue ;
}
}
else // 2차 깊이 이상 하위 메뉴
{
prefabToUse = subMenuItemPrefab ;
if ( prefabToUse = = null )
{
2025-06-18 20:09:16 +09:00
Debug . LogError ( "subMenuItemPrefab이 할당되지 않았습니다. 하위 메뉴 아이템을 생성할 수 없습니다." ) ;
2025-06-11 19:24:08 +09:00
continue ;
}
}
GameObject menuItemInstance = Instantiate ( prefabToUse , parentContainer ) ;
menuItemInstance . name = $"MenuItem_{itemData.ItemId}_Depth{depth}" ;
2025-06-12 19:25:33 +09:00
_menuItemObjects [ itemData . ItemId ] = menuItemInstance ; // 관리 목록에 추가
2025-06-11 19:24:08 +09:00
2025-06-12 19:25:33 +09:00
// 메뉴 아이템 텍스트 설정
TextMeshProUGUI buttonText = menuItemInstance . GetComponentInChildren < TextMeshProUGUI > ( true ) ; // 비활성화된 자식도 검색
// 메뉴 아이템 버튼 기능 설정
2025-06-11 19:24:08 +09:00
Button button = menuItemInstance . GetComponent < Button > ( ) ;
2025-06-18 20:09:16 +09:00
if ( buttonText ! = null & & ! string . IsNullOrEmpty ( itemData . DisplayName ) )
2025-06-11 19:24:08 +09:00
{
2025-08-05 20:18:01 +09:00
buttonText . alpha = itemData . IsEnabled ? 1 : 0.25f ;
2025-06-11 19:24:08 +09:00
if ( _locManager ! = null )
{
2025-06-18 20:09:16 +09:00
buttonText . text = _locManager . GetString ( itemData . DisplayName ) ;
2025-06-11 19:24:08 +09:00
}
else
{
2025-06-12 19:25:33 +09:00
// LocalizationManager가 없는 경우, 키 값을 그대로 표시 (개발 중 확인 용도)
2025-06-18 20:09:16 +09:00
Debug . LogWarning ( $"LocalizationManager 인스턴스가 없어 메뉴 아이템 텍스트를 키 값으로 설정합니다: {itemData.DisplayName}" ) ;
buttonText . text = itemData . DisplayName ;
2025-06-11 19:24:08 +09:00
}
}
2025-06-18 20:09:16 +09:00
// isShrinkMenuItemWidth가 true일 때 1차 메뉴 아이템의 너비를 텍스트에 맞게 조절합니다.
// 텍스트가 설정된 후에 이 로직을 실행해야 정확한 너비를 계산할 수 있습니다.
if ( depth = = 0 & & isShrinkMenuItemWidth & & buttonText ! = null )
{
buttonText . ForceMeshUpdate ( ) ; // 텍스트 변경 후 메쉬 강제 업데이트 (정확한 크기 계산 위함)
Vector2 textSize = buttonText . GetPreferredValues ( buttonText . text ) ; // 텍스트 내용에 따른 선호 크기 계산
RectTransform rect = menuItemInstance . GetComponent < RectTransform > ( ) ;
textSize . x + = menuItemWidthPadding ; // 좌우 여백 추가
if ( rect ! = null ) rect . sizeDelta = new Vector2 ( textSize . x , rect . rect . height ) ;
// LayoutElement가 없으면 추가
LayoutElement layoutElement = menuItemInstance . GetComponent < LayoutElement > ( ) ? ? menuItemInstance . AddComponent < LayoutElement > ( ) ;
layoutElement . preferredWidth = buttonText . preferredWidth ;
}
2025-06-11 19:24:08 +09:00
if ( button ! = null )
{
2025-06-12 19:25:33 +09:00
button . onClick . RemoveAllListeners ( ) ; // 기존 리스너 제거 (프리팹에 설정된 것이 있을 수 있으므로)
2025-06-11 19:24:08 +09:00
button . onClick . AddListener ( ( ) = >
{
2025-06-12 19:25:33 +09:00
// 메뉴 아이템 클릭 시 등록된 이벤트 핸들러 호출
2025-06-11 19:24:08 +09:00
OnMenuItemClicked ? . Invoke ( itemData ) ;
2025-06-12 19:25:33 +09:00
2025-06-11 19:24:08 +09:00
if ( itemData . SubMenuItems ! = null & & itemData . SubMenuItems . Count > 0 )
{
2025-06-12 19:25:33 +09:00
// 하위 메뉴가 있으면 해당 하위 메뉴의 표시 상태를 토글
2025-06-18 20:09:16 +09:00
ToggleSubMenuDisplay ( itemData , menuItemInstance , depth ) ;
2025-06-11 19:24:08 +09:00
}
else
{
2025-06-12 19:25:33 +09:00
// 하위 메뉴가 없는 아이템 클릭 시 모든 열린 하위 메뉴 닫기 (선택적 동작)
2025-06-11 19:24:08 +09:00
CloseAllOpenSubMenus ( ) ;
}
} ) ;
2025-06-12 19:25:33 +09:00
// MenuItemData의 IsEnabled 상태에 따라 버튼의 상호작용 가능 여부 설정
button . interactable = itemData . IsEnabled ;
2025-06-11 19:24:08 +09:00
}
2025-06-12 19:25:33 +09:00
// 하위 메뉴가 있는 경우, 하위 메뉴 관련 UI 처리
2025-06-11 19:24:08 +09:00
if ( itemData . SubMenuItems ! = null & & itemData . SubMenuItems . Count > 0 )
{
2025-06-12 19:25:33 +09:00
// 하위 메뉴 화살표 아이콘 표시
2025-06-18 20:09:16 +09:00
Transform subMenuArrowTransform = menuItemInstance . transform . Find ( subMenuArrowName ) ;
2025-06-11 19:24:08 +09:00
if ( subMenuArrowTransform ! = null )
{
subMenuArrowTransform . gameObject . SetActive ( true ) ;
}
2025-06-18 20:09:16 +09:00
// 하위 메뉴 컨테이너 생성 및 설정
if ( subMenuContainer ! = null )
2025-06-11 19:24:08 +09:00
{
2025-06-18 20:09:16 +09:00
GameObject subMenuContainerInstance = Instantiate ( subMenuContainer . gameObject ) ;
subMenuContainerInstance . name = $"SubMenuContainer_{itemData.ItemId}" ;
_subMenuContainerObjects [ itemData . ItemId ] = subMenuContainerInstance ;
// 부모를 메뉴 아이템으로 설정하여 위치를 쉽게 계산
subMenuContainerInstance . transform . SetParent ( menuItemInstance . transform , false ) ;
RectTransform subMenuRect = subMenuContainerInstance . GetComponent < RectTransform > ( ) ;
2025-06-11 19:24:08 +09:00
if ( subMenuRect = = null )
{
2025-06-18 20:09:16 +09:00
Debug . LogWarning ( $"{subMenuContainer.name} for '{menuItemInstance.name}' is missing RectTransform. Adding one." ) ;
subMenuRect = subMenuContainerInstance . gameObject . AddComponent < RectTransform > ( ) ;
2025-06-11 19:24:08 +09:00
}
2025-06-12 19:25:33 +09:00
// 하위 메뉴 컨테이너의 RectTransform 기본 설정 (앵커, 피벗)
subMenuRect . anchorMin = new Vector2 ( 0 , 1 ) ; // 좌상단 기준
subMenuRect . anchorMax = new Vector2 ( 0 , 1 ) ; // 좌상단 기준
subMenuRect . pivot = new Vector2 ( 0 , 1 ) ; // 좌상단 기준
2025-06-11 19:24:08 +09:00
2025-06-18 20:09:16 +09:00
RectTransform menuItemRect = menuItemInstance . GetComponent < RectTransform > ( ) ;
// 최종 부모 설정
if ( menuContainer ! = null & & menuContainer . parent ! = null )
2025-06-11 19:24:08 +09:00
{
2025-06-18 20:09:16 +09:00
subMenuContainerInstance . transform . SetParent ( menuContainer . parent , true ) ;
2025-06-11 19:24:08 +09:00
}
2025-06-18 20:09:16 +09:00
else
2025-06-11 19:24:08 +09:00
{
2025-06-18 20:09:16 +09:00
Debug . LogWarning ( "menuContainer 또는 menuContainer.parent가 null이므로 하위 메뉴 컨테이너를 최상위로 설정합니다." ) ;
2025-06-11 19:24:08 +09:00
}
2025-06-12 19:25:33 +09:00
// 재귀 호출을 통해 하위 메뉴 아이템들 생성 및 배치
2025-06-18 20:09:16 +09:00
CreateMenuItems ( itemData . SubMenuItems , subMenuContainerInstance . transform , depth + 1 ) ;
2025-06-12 19:25:33 +09:00
// 하위 메뉴는 초기에 숨겨진 상태로 설정
2025-06-18 20:09:16 +09:00
subMenuContainerInstance . gameObject . SetActive ( false ) ;
2025-06-11 19:24:08 +09:00
}
else
{
2025-06-18 20:09:16 +09:00
Debug . LogWarning ( $"'subMenuContainer' 프리팹이 Inspector에서 할당되지 않았습니다. 하위 메뉴가 정상적으로 표시되지 않을 수 있습니다." ) ;
2025-06-11 19:24:08 +09:00
}
}
2025-06-12 19:25:33 +09:00
else // 하위 메뉴가 없는 경우
2025-06-11 19:24:08 +09:00
{
2025-06-12 19:25:33 +09:00
// 하위 메뉴 화살표 아이콘 숨김 (프리팹에 기본적으로 활성화되어 있을 수 있으므로)
2025-06-18 20:09:16 +09:00
Transform existingArrow = menuItemInstance . transform . Find ( subMenuArrowName ) ;
2025-06-11 19:24:08 +09:00
if ( existingArrow ! = null )
{
existingArrow . gameObject . SetActive ( false ) ;
}
}
}
}
}
2025-06-12 19:25:33 +09:00
/// <summary>
2025-06-18 20:09:16 +09:00
/// 특정 메뉴 아이템에 연결된 하위 메뉴의 표시 상태를 토글(열기/닫기)합니다.
/// 하위 메뉴를 열 때, 같은 레벨의 다른 열려있는 하위 메뉴들은 닫습니다.
2025-06-12 19:25:33 +09:00
/// </summary>
2025-06-18 20:09:16 +09:00
/// <param name="itemData">토글할 하위 메뉴를 가진 부모 메뉴의 MenuItemData입니다.</param>
/// <param name="menuItemInstance">부모 메뉴 아이템의 GameObject입니다.</param>
/// <param name="depth">클릭된 메뉴 아이템의 깊이입니다.</param>
protected virtual void ToggleSubMenuDisplay ( MenuItemData itemData , GameObject menuItemInstance , int depth )
2025-06-11 19:24:08 +09:00
{
2025-06-18 20:09:16 +09:00
if ( itemData . IsSeparator ) return ; // 구분선은 하위 메뉴를 가질 수 없음
2025-06-11 19:24:08 +09:00
2025-06-18 20:09:16 +09:00
if ( _subMenuContainerObjects . TryGetValue ( itemData . ItemId , out GameObject subMenuContainerObject ) )
2025-06-11 19:24:08 +09:00
{
2025-06-18 20:09:16 +09:00
bool isActive = subMenuContainerObject . activeSelf ; // 현재 하위 메뉴의 활성화 상태
bool becomingActive = ! isActive ; // 토글 후 활성화될 상태
2025-06-11 19:24:08 +09:00
2025-06-18 20:09:16 +09:00
// 새로 메뉴를 여는 경우, 현재 메뉴와 관련된 다른 메뉴들을 먼저 닫습니다.
if ( becomingActive )
2025-06-11 19:24:08 +09:00
{
2025-06-18 20:09:16 +09:00
// 클릭된 아이템의 부모 컨테이너(같은 레벨의 메뉴 아이템들이 있는 곳)를 기준으로
// 현재 열려는 하위 메뉴(subMenuContainerObject)를 제외한 다른 모든 하위 메뉴를 닫습니다.
CloseOtherSubMenusInParent ( itemData , menuItemInstance . transform . parent , subMenuContainerObject ) ;
// 만약 1차 메뉴(depth 0)를 클릭해서 2차 메뉴를 여는 경우라면,
// 현재 열려있는 모든 하위 메뉴를 닫아줍니다.
// 이렇게 하면 다른 메뉴 가지(branch)에 열려있던 하위 메뉴들이 모두 닫힙니다.
if ( depth = = 0 )
2025-06-11 19:24:08 +09:00
{
2025-06-18 20:09:16 +09:00
CloseAllOpenSubMenus ( ) ;
2025-06-11 19:24:08 +09:00
}
}
2025-06-18 20:09:16 +09:00
else // 하위 메뉴가 닫히려고 하는 경우
2025-06-11 19:24:08 +09:00
{
2025-06-18 20:09:16 +09:00
// 닫으려는 메뉴의 모든 자식 메뉴들도 함께 닫습니다.
CloseSubMenuAndDescendants ( itemData ) ;
2025-06-11 19:24:08 +09:00
}
2025-06-18 20:09:16 +09:00
// 하위 메뉴 컨테이너의 활성화 상태를 토글
subMenuContainerObject . SetActive ( becomingActive ) ;
if ( becomingActive ) subMenuContainerObject . GetComponent < LayoutGroup > ( ) ? . FitToChildren ( ) ; // 하위 메뉴 컨테이너의 크기를 자식 아이템에 맞게 조정
// isAnySubMenuOpen 상태 및 UI 블로커 업데이트
if ( becomingActive )
2025-06-11 19:24:08 +09:00
{
2025-06-18 20:09:16 +09:00
isAnySubMenuOpen = true ; // 하나라도 열리면 true
RectTransform menuItemRect = menuItemInstance . GetComponent < RectTransform > ( ) ;
RectTransform menuItemRectparent = menuItemInstance . transform . parent ? . GetComponent < RectTransform > ( ) ;
RectTransform subMenuRect = subMenuContainerObject . transform ? . GetComponent < RectTransform > ( ) ;
if ( menuItemRect ! = null & & subMenuRect ! = null & & menuItemRectparent ! = null )
2025-06-11 19:24:08 +09:00
{
2025-06-18 20:09:16 +09:00
// 하위 메뉴 위치 조정
if ( depth = = 0 ) // 1차 메뉴의 하위 메뉴는 부모 아이템의 아래쪽에 펼쳐짐
{
subMenuRect . anchoredPosition = new Vector2 ( menuItemRect . anchoredPosition . x + menuDepthSpace . x , - menuItemRect . rect . height + menuDepthSpace . y ) ;
}
else // 2차 이상 메뉴의 하위 메뉴는 부모 아이템의 오른쪽에 펼쳐짐
{
subMenuRect . anchoredPosition = new Vector2 ( menuItemRectparent . anchoredPosition . x + menuItemRectparent . rect . width + subMenuDepthSpace . x , menuItemRectparent . anchoredPosition . y + menuItemRect . anchoredPosition . y + subMenuDepthSpace . y ) ;
}
2025-06-11 19:24:08 +09:00
}
}
2025-06-18 20:09:16 +09:00
else
2025-06-11 19:24:08 +09:00
{
2025-06-18 20:09:16 +09:00
// 하위 메뉴가 닫혔으므로, 다른 열린 하위 메뉴가 있는지 다시 확인
CheckIfAnySubMenuRemainsOpen ( ) ;
2025-06-11 19:24:08 +09:00
}
2025-06-18 20:09:16 +09:00
UpdateBlockerVisibility ( ) ; // UI 블로커 표시 여부 업데이트
2025-06-11 19:24:08 +09:00
}
}
2025-06-12 19:25:33 +09:00
/// <summary>
2025-06-18 20:09:16 +09:00
/// 특정 메뉴 아이템의 모든 하위 메뉴들을 재귀적으로 닫습니다.
2025-06-12 19:25:33 +09:00
/// </summary>
2025-06-18 20:09:16 +09:00
/// <param name="parentItemData">하위 메뉴들을 닫을 부모 메뉴의 MenuItemData입니다.</param>
protected virtual void CloseSubMenuAndDescendants ( MenuItemData parentItemData )
2025-06-11 19:24:08 +09:00
{
2025-06-18 20:09:16 +09:00
if ( parentItemData ? . SubMenuItems = = null ) return ;
2025-06-11 19:24:08 +09:00
2025-06-18 20:09:16 +09:00
foreach ( var subItem in parentItemData . SubMenuItems )
2025-06-11 19:24:08 +09:00
{
2025-06-18 20:09:16 +09:00
if ( subItem . IsSeparator ) continue ;
2025-06-11 19:24:08 +09:00
2025-06-18 20:09:16 +09:00
if ( _subMenuContainerObjects . TryGetValue ( subItem . ItemId , out GameObject subContainer ) )
2025-06-11 19:24:08 +09:00
{
2025-06-18 20:09:16 +09:00
if ( subContainer ! = null & & subContainer . activeSelf )
{
subContainer . SetActive ( false ) ;
// 이 하위 메뉴의 자식들도 재귀적으로 닫습니다.
CloseSubMenuAndDescendants ( subItem ) ;
}
2025-06-11 19:24:08 +09:00
}
}
}
2025-06-12 19:25:33 +09:00
/// <summary>
/// 특정 부모 컨테이너 내에서, 지정된 하위 메뉴(subMenuToExclude)를 제외한 모든 다른 열려있는 하위 메뉴들을 닫습니다.
/// 이는 한 번에 하나의 하위 메뉴만 열려 있도록 보장하는 데 사용됩니다.
/// </summary>
2025-06-18 20:09:16 +09:00
/// <param name="itemData">토글할 하위 메뉴를 가진 메뉴의 MenuItemData입니다.</param>
2025-06-12 19:25:33 +09:00
/// <param name="currentMenuItemsParent">하위 메뉴들을 검사할 부모 Transform입니다. (예: TopMenu 또는 다른 SubMenuContainer)</param>
2025-06-18 20:09:16 +09:00
/// <param name="subMenuToExclude">닫지 않고 유지할 특정 하위 메뉴의 GameObject입니다. (보통 새로 열리려는 하위 메뉴)</param>
protected virtual void CloseOtherSubMenusInParent ( MenuItemData itemData , Transform currentMenuItemsParent , GameObject subMenuToExclude )
2025-06-11 19:24:08 +09:00
{
if ( currentMenuItemsParent = = null )
{
2025-06-18 20:09:16 +09:00
Debug . LogWarning ( "CloseOtherSubMenusInParent 호출 시 currentMenuItemsParent가 null입니다. 다른 하위 메뉴를 닫을 수 없습니다." ) ;
2025-06-11 19:24:08 +09:00
return ;
}
2025-06-18 20:09:16 +09:00
Debug . Log ( $"CloseOtherSubMenusInParent 호출: 현재 부모 - {currentMenuItemsParent.name}, 제외할 하위 메뉴 - {subMenuToExclude?.name}" ) ;
2025-06-12 19:25:33 +09:00
2025-06-18 20:09:16 +09:00
string parentName = currentMenuItemsParent . name + "_" ;
2025-06-11 19:24:08 +09:00
2025-06-18 20:09:16 +09:00
// 현재 부모 컨테이너 내에서 열려있는 하위 메뉴들을 찾습니다.
MenuItemData ? parentItemData = itemData . Parent ;
2025-06-11 19:24:08 +09:00
2025-06-18 20:09:16 +09:00
if ( parentItemData ! = null )
{
// ToList()를 사용하여 반복 중 컬렉션 수정 문제를 방지합니다.
var openSubMenus = _subMenuContainerObjects . Where ( pair = > pair . Value ! = null & &
pair . Value . activeSelf & & pair . Value ! = subMenuToExclude & & parentItemData . HasSubMenuItems ( pair . Key ) ) . ToList ( ) ;
foreach ( var pair in openSubMenus )
2025-06-11 19:24:08 +09:00
{
2025-06-18 20:09:16 +09:00
string itemId = pair . Key ;
GameObject containerToClose = pair . Value ;
// 닫아야 할 형제 하위 메뉴를 찾았습니다.
containerToClose . SetActive ( false ) ;
// 이 하위 메뉴의 자손 메뉴들도 모두 재귀적으로 닫습니다.
if ( _menuItemDataMap . TryGetValue ( itemId , out MenuItemData itemDataToClose ) )
{
CloseSubMenuAndDescendants ( itemDataToClose ) ;
}
2025-06-11 19:24:08 +09:00
}
}
}
2025-06-12 19:25:33 +09:00
/// <summary>
/// 현재 열려있는 모든 하위 메뉴들을 닫습니다.
/// 주로 메뉴 외부 영역 클릭(UI 블로커 클릭) 시 또는 최상위 메뉴 아이템 클릭 시 호출됩니다.
/// </summary>
2025-06-11 19:24:08 +09:00
public virtual void CloseAllOpenSubMenus ( )
{
2025-06-12 19:25:33 +09:00
bool anyActuallyClosed = false ; // 실제로 닫힌 하위 메뉴가 있었는지 추적
2025-06-18 20:09:16 +09:00
foreach ( GameObject subMenuContainer in _subMenuContainerObjects . Values )
2025-06-11 19:24:08 +09:00
{
2025-06-18 20:09:16 +09:00
if ( subMenuContainer ! = null & & subMenuContainer . activeSelf )
2025-06-11 19:24:08 +09:00
{
2025-06-18 20:09:16 +09:00
subMenuContainer . SetActive ( false ) ;
anyActuallyClosed = true ;
2025-06-11 19:24:08 +09:00
}
}
2025-06-12 19:25:33 +09:00
// 실제로 메뉴가 닫혔거나, 이전에 어떤 메뉴든 열려 있었다면 상태 업데이트
2025-06-11 19:24:08 +09:00
if ( anyActuallyClosed | | isAnySubMenuOpen )
{
2025-06-12 19:25:33 +09:00
isAnySubMenuOpen = false ; // 모든 하위 메뉴가 닫혔으므로 false로 설정
UpdateBlockerVisibility ( ) ; // UI 블로커 숨김 처리
2025-06-11 19:24:08 +09:00
}
}
2025-06-12 19:25:33 +09:00
/// <summary>
/// 현재 열려있는 하위 메뉴가 있는지 확인하고, `isAnySubMenuOpen` 상태를 업데이트합니다.
/// </summary>
2025-06-11 19:24:08 +09:00
protected virtual void CheckIfAnySubMenuRemainsOpen ( )
{
2025-06-12 19:25:33 +09:00
isAnySubMenuOpen = false ; // 일단 false로 가정
2025-06-18 20:09:16 +09:00
foreach ( GameObject subMenuGO in _subMenuContainerObjects . Values )
2025-06-11 19:24:08 +09:00
{
2025-06-18 20:09:16 +09:00
if ( subMenuGO ! = null & & subMenuGO . activeSelf )
2025-06-11 19:24:08 +09:00
{
2025-06-12 19:25:33 +09:00
isAnySubMenuOpen = true ; // 하나라도 열려있으면 true로 설정하고 반복 종료
2025-06-11 19:24:08 +09:00
break ;
}
}
}
2025-06-12 19:25:33 +09:00
/// <summary>
/// `isAnySubMenuOpen` 상태에 따라 UI 블로커의 활성화/비활성화 상태를 업데이트합니다.
/// 블로커가 활성화될 때는 메뉴 시스템 바로 뒤(UI 상으로는 바로 앞)에 오도록 렌더링 순서를 조정합니다.
/// </summary>
2025-06-11 19:24:08 +09:00
protected virtual void UpdateBlockerVisibility ( )
{
if ( uiBlockerInstance ! = null )
{
2025-06-12 19:25:33 +09:00
uiBlockerInstance . SetActive ( isAnySubMenuOpen ) ; // isAnySubMenuOpen 값에 따라 블로커 활성화/비활성화
2025-06-11 19:24:08 +09:00
if ( isAnySubMenuOpen )
{
2025-06-12 19:25:33 +09:00
// 블로커가 활성화될 때, 렌더링 순서를 조정하여 다른 메뉴 요소들보다 뒤에 오도록 합니다.
// CreateUIBlocker에서 이미 부모와 기본적인 Sibling Index가 설정되었으므로,
// 여기서는 SetAsLastSibling을 호출하여 해당 부모 내에서 메뉴 바로 뒤에 오도록 설정합니다.
// 이렇게 하면 블로커가 다른 모든 UI 요소들의 클릭을 가로챌 수 있습니다.
Transform blockerParent = uiBlockerInstance . transform . parent ;
if ( menuContainer ! = null & & menuContainer . parent = = blockerParent )
{
uiBlockerInstance . transform . SetSiblingIndex ( menuContainer . GetSiblingIndex ( ) - 1 ) ;
}
else if ( transform . parent = = blockerParent )
2025-06-11 19:24:08 +09:00
{
2025-06-12 19:25:33 +09:00
uiBlockerInstance . transform . SetSiblingIndex ( transform . GetSiblingIndex ( ) - 1 ) ;
}
else
{
uiBlockerInstance . transform . SetAsLastSibling ( ) ;
2025-06-11 19:24:08 +09:00
}
}
}
}
2025-06-12 19:25:33 +09:00
/// <summary>
/// 화면에 표시된 모든 메뉴 아이템들을 제거하고 관련 내부 데이터도 초기화합니다.
/// </summary>
2025-06-11 19:24:08 +09:00
public virtual void ClearMenuItems ( )
{
2025-06-12 19:25:33 +09:00
CloseAllOpenSubMenus ( ) ; // 먼저 열려있는 모든 하위 메뉴를 닫습니다.
// _menuItemObjects에 저장된 모든 GameObject들을 파괴하고 딕셔너리를 비웁니다.
2025-06-11 19:24:08 +09:00
foreach ( var pair in _menuItemObjects )
{
if ( pair . Value ! = null ) Destroy ( pair . Value ) ;
}
_menuItemObjects . Clear ( ) ;
2025-06-18 20:09:16 +09:00
foreach ( var pair in _subMenuContainerObjects )
{
if ( pair . Value ! = null ) Destroy ( pair . Value ) ;
}
_subMenuContainerObjects . Clear ( ) ;
_menuItemDataMap . Clear ( ) ;
2025-06-12 19:25:33 +09:00
// menuContainer의 모든 자식 GameObject들을 직접 파괴합니다.
2025-06-11 19:24:08 +09:00
if ( menuContainer ! = null )
{
foreach ( Transform child in menuContainer )
{
Destroy ( child . gameObject ) ;
}
}
}
2025-06-12 19:25:33 +09:00
/// <summary>
/// 제공된 MenuItemData 리스트를 기반으로 화면에 표시된 모든 메뉴 아이템들의 텍스트를 업데이트합니다.
/// 주로 언어 변경 시 호출됩니다.
/// </summary>
/// <param name="items">최상위 메뉴 아이템들의 데이터 리스트입니다.</param>
2025-06-11 19:24:08 +09:00
public virtual void UpdateAllMenuTexts ( List < MenuItemData > items )
{
if ( _locManager = = null )
{
2025-06-18 20:09:16 +09:00
Debug . LogWarning ( "LocalizationManager가 없어 메뉴 텍스트를 업데이트할 수 없습니다." ) ;
2025-06-11 19:24:08 +09:00
return ;
}
2025-06-18 20:09:16 +09:00
UpdateMenuTextsRecursive ( items , 0 ) ;
2025-06-11 19:24:08 +09:00
}
2025-06-12 19:25:33 +09:00
/// <summary>
2025-06-18 20:09:16 +09:00
/// 재귀적으로 메뉴 아이템들의 텍스트를 업데이트하고, 필요에 따라 너비를 조절하는 내부 도우미 함수입니다.
2025-06-12 19:25:33 +09:00
/// </summary>
/// <param name="items">텍스트를 업데이트할 메뉴 아이템들의 데이터 리스트입니다.</param>
2025-06-18 20:09:16 +09:00
/// <param name="depth">현재 메뉴의 깊이입니다.</param>
protected virtual void UpdateMenuTextsRecursive ( List < MenuItemData > items , int depth = 0 )
2025-06-11 19:24:08 +09:00
{
if ( items = = null ) return ;
foreach ( var itemData in items )
{
2025-06-12 19:25:33 +09:00
if ( itemData . IsSeparator ) continue ; // 구분선은 텍스트가 없음
2025-06-11 19:24:08 +09:00
if ( _menuItemObjects . TryGetValue ( itemData . ItemId , out GameObject menuItemInstance ) )
{
TextMeshProUGUI buttonText = menuItemInstance . GetComponentInChildren < TextMeshProUGUI > ( true ) ;
2025-06-18 20:09:16 +09:00
if ( buttonText ! = null & & ! string . IsNullOrEmpty ( itemData . DisplayName ) )
2025-06-11 19:24:08 +09:00
{
2025-06-18 20:09:16 +09:00
buttonText . text = _locManager . GetString ( itemData . DisplayName ) ;
// isShrinkMenuItemWidth가 true일 때 1차 메뉴 아이템의 너비를 텍스트에 맞게 조절
if ( depth = = 0 & & isShrinkMenuItemWidth )
{
LayoutElement layoutElement = menuItemInstance . GetComponent < LayoutElement > ( ) ? ? menuItemInstance . AddComponent < LayoutElement > ( ) ;
// 텍스트의 preferredWidth를 기반으로 선호 너비 설정 (좌우 여백 포함)
layoutElement . preferredWidth = buttonText . preferredWidth + menuItemWidthPadding ;
}
2025-06-11 19:24:08 +09:00
}
}
if ( itemData . SubMenuItems ! = null & & itemData . SubMenuItems . Count > 0 )
{
2025-06-18 20:09:16 +09:00
UpdateMenuTextsRecursive ( itemData . SubMenuItems , depth + 1 ) ;
2025-06-11 19:24:08 +09:00
}
}
}
2025-06-12 19:25:33 +09:00
/// <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 ) ;
}
2025-06-11 19:24:08 +09:00
}
}