#nullable enable using System; using System.Collections.Generic; using System.Linq; using TMPro; using UnityEngine; using UnityEngine.UI; using UVC.Extension; using UVC.Locale; // 다국어 처리 네임스페이스 namespace UVC.UI.Menu { /// /// 상단 메뉴 UI의 표시 및 상호작용을 관리하는 클래스입니다. /// MenuItemData를 기반으로 실제 Unity UI 요소들을 생성하고 배치하며, 사용자 입력을 처리합니다. /// 이 클래스는 상속을 통해 특정 프로젝트의 요구사항에 맞게 커스터마이징될 수 있도록 설계되었습니다. /// /// /// /// /// TopMenuView를 상속받아 특정 기능을 커스터마이징하는 예제 클래스입니다. /// 예를 들어, 다른 프리팹 경로를 사용하거나 메뉴 아이템 레이아웃 방식을 변경할 수 있습니다. /// /// public class CustomTopMenuView : TopMenuView /// { /// /// /// /// MonoBehaviour의 Awake 메시지입니다. /// /// 부모 클래스의 Awake를 호출한 후, 추가적인 커스텀 초기화 로직을 수행합니다. /// /// /// protected override void Awake() /// { /// // 부모 클래스(TopMenuView)의 Awake 로직을 먼저 실행합니다. /// // 이렇게 하면 프리팹 로드, 기본 설정 등이 먼저 처리됩니다. /// base.Awake(); /// /// // 여기에 CustomTopMenuView만의 추가적인 초기화 코드를 작성할 수 있습니다. /// ULog.Debug("CustomTopMenuView Awake 실행됨. 커스텀 프리팹 경로가 사용됩니다."); /// } /// /// /// /// /// 메뉴 아이템이 클릭되었을 때 호출되는 이벤트 핸들러를 커스터마이징 할 수 있습니다. /// /// (참고: OnMenuItemClicked 이벤트는 TopMenuController에서 구독하여 주로 처리합니다. /// /// View에서 직접 처리해야 하는 로직이 있다면 이와 유사한 방식으로 메서드를 오버라이드 하거나 /// /// Awake 등에서 이벤트 핸들러를 추가/제거 할 수 있습니다.) /// /// 이 예제에서는 ToggleSubMenuDisplay를 오버라이드하여 하위 메뉴 토글 시 로그를 남깁니다. /// /// /// protected override void ToggleSubMenuDisplay(MenuItemData itemData, GameObject menuItemInstance) /// { /// ULog.Debug($"Custom ToggleSubMenuDisplay: {itemData.DisplayName} 클릭됨."); /// base.ToggleSubMenuDisplay(itemData, menuItemInstance); // 부모의 원래 로직 실행 /// } /// /// // 필요에 따라 다른 virtual 메서드들 (CreateMenuItems, ClearMenuItems 등)도 /// // 오버라이드하여 동작을 변경하거나 확장할 수 있습니다. /// } /// /// public class TopMenuView : MonoBehaviour { [Header("UI References")] [Tooltip("메인 메뉴 아이템을 위한 프리팹입니다.")] [SerializeField] public GameObject menuItemPrefab; [Tooltip("하위 메뉴 아이템을 위한 프리팹입니다.")] [SerializeField] public GameObject subMenuItemPrefab; [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"; [Tooltip("단축키를 표시하는 TextMeshProUGUI의 이름입니다. (SubMenuItemPrefab 내부에 존재)")] [SerializeField] public string shortcutTextName = "ShorcutText"; [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; /// /// 메뉴 아이템이 클릭되었을 때 발생하는 이벤트입니다. /// 클릭된 메뉴의 MenuItemData를 전달합니다. /// public event Action OnMenuItemClicked; // --- 내부 상태 변수 --- // 생성된 메뉴 아이템 GameObject들을 관리하는 딕셔너리 (키: ItemId, 값: GameObject) protected Dictionary _menuItemObjects = new Dictionary(); protected Dictionary _subMenuContainerObjects = new Dictionary(); protected Dictionary _menuItemDataMap = new Dictionary(); protected LocalizationManager _locManager; // 다국어 처리를 위한 LocalizationManager 인스턴스 protected GameObject uiBlockerInstance; // 하위 메뉴가 열렸을 때 다른 UI 상호작용을 막기 위한 투명한 UI 요소 protected bool isAnySubMenuOpen = false; // 하나 이상의 하위 메뉴가 열려있는지 여부 /// /// MonoBehaviour의 Awake 메시지입니다. /// 메뉴 시스템 초기화에 필요한 리소스 로드 및 참조 설정을 수행합니다. /// 상속 클래스에서 이 메서드를 오버라이드하여 추가적인 초기화 로직을 구현할 수 있습니다. /// 오버라이드 시 `base.Awake()`를 호출하는 것을 권장합니다. /// protected virtual void Awake() { _locManager = LocalizationManager.Instance; if (_locManager == null) { // LocalizationManager가 필수적인 경우, 여기서 에러를 발생시키거나 게임을 중단시킬 수 있습니다. // 여기서는 경고만 기록하고 진행합니다. Debug.LogWarning("LocalizationManager 인스턴스를 찾을 수 없습니다. 메뉴 텍스트가 제대로 표시되지 않을 수 있습니다."); } // Inspector에서 할당된 참조 확인 if (menuItemPrefab == null) { Debug.LogWarning("menuItemPrefab이 Inspector에서 할당되지 않았습니다.", this); } if (subMenuItemPrefab == null) { Debug.LogWarning("subMenuItemPrefab이 Inspector에서 할당되지 않았습니다.", this); } if (menuSeparatorPrefab == null) { // 구분선은 선택 사항일 수 있으므로, 경고 수준으로 처리합니다. Debug.LogWarning("menuSeparatorPrefab이 Inspector에서 할당되지 않았습니다. 구분선 기능이 작동하지 않습니다.", this); } // 메뉴 컨테이너 확인 if (menuContainer == null) { Debug.LogWarning("menuContainer가 Inspector에서 할당되지 않았습니다. Inspector에서 참조를 설정해주세요.", this); } else { // 메뉴 컨테이너에 LayoutGroup이 있는지 확인하고, 없다면 경고를 표시합니다. // 자동 배치를 위해 HorizontalLayoutGroup 또는 VerticalLayoutGroup이 필요합니다. if (menuContainer.GetComponent() == null) { Debug.LogWarning($"menuContainer '{menuContainer.name}'에 LayoutGroup 컴포넌트가 없습니다. 메뉴 아이템이 자동으로 배치되지 않을 수 있습니다. Inspector에서 HorizontalLayoutGroup 또는 VerticalLayoutGroup을 추가해주세요."); } } // UI 블로커 생성 CreateUIBlocker(); } /// /// 하위 메뉴가 열렸을 때 다른 UI 요소와의 원치 않는 상호작용을 막기 위한 /// 투명한 UI 블로커 GameObject를 생성하고 설정합니다. /// 블로커는 최상위 Canvas의 자식으로 배치되어 전체 화면을 덮도록 설정됩니다. /// protected virtual void CreateUIBlocker() { uiBlockerInstance = new GameObject("TopMenuUIBlocker"); // Canvas를 찾아 그 자식으로 설정합니다. 씬에 여러 Canvas가 있다면, 적절한 Canvas를 찾는 로직이 필요할 수 있습니다. Canvas canvas = GetComponentInParent(); Transform blockerParent = canvas != null ? canvas.transform : transform.parent; // Canvas가 없으면 TopMenuView의 부모를 사용 if (blockerParent == null) // 부모를 찾지 못한 극단적인 경우, TopMenuView 자신을 부모로 설정 (권장되지 않음) { blockerParent = transform; Debug.LogWarning("TopMenuUIBlocker의 부모를 TopMenuView 자신으로 설정합니다. Canvas 하위에 배치하는 것을 권장합니다."); } uiBlockerInstance.transform.SetParent(blockerParent, false); // 블로커의 RectTransform 설정 (화면 전체를 덮도록) RectTransform blockerRect = uiBlockerInstance.AddComponent(); 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(); blockerImage.color = new Color(0, 0, 0, 0.001f); // 매우 낮은 알파값으로 거의 보이지 않게 설정 blockerImage.raycastTarget = true; // Raycast를 받아야 클릭 이벤트 감지 가능 // 블로커에 버튼 컴포넌트 추가 및 클릭 이벤트 리스너 설정 Button blockerButton = uiBlockerInstance.AddComponent