using System;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
#if UNITY_EDITOR
using UnityEditor;
#endif
public enum InventoryRootUIMode
{
TestMode,
PlayMode,
BuildMode,
HiddenMode,
LayoutOnly
}
public enum InventoryUIAnchorPreset
{
Custom,
MiddleCenter,
TopLeft,
TopCenter,
TopRight,
MiddleLeft,
MiddleRight,
BottomLeft,
BottomCenter,
BottomRight,
StretchAll
}
[Serializable]
public class InventoryControlledRectTarget
{
[Header("Target")]
public string name;
public RectTransform rectTransform;
public GameObject activeTarget;
[Header("Rect Layout")]
public bool applyRect = true;
public InventoryUIAnchorPreset anchorPreset = InventoryUIAnchorPreset.MiddleCenter;
public Vector2 customAnchorMin = new Vector2(0.5f, 0.5f);
public Vector2 customAnchorMax = new Vector2(0.5f, 0.5f);
public Vector2 customPivot = new Vector2(0.5f, 0.5f);
public Vector2 size = new Vector2(100f, 100f);
public Vector2 anchoredPosition = Vector2.zero;
[Header("Transform")]
public bool resetScaleToOne = true;
public bool applyLocalScale = false;
public Vector3 localScale = Vector3.one;
public bool applyLocalRotation = false;
public Vector3 localEulerAngles = Vector3.zero;
public bool applySiblingIndex = false;
public int siblingIndex = 0;
[Header("Active By Mode")]
public bool controlActiveState = false;
public bool activeInTestMode = true;
public bool activeInPlayMode = true;
public bool activeInBuildMode = true;
public bool activeInHiddenMode = false;
[Header("CanvasGroup")]
public bool applyCanvasGroup = false;
public bool addCanvasGroupIfMissing = true;
[Range(0f, 1f)] public float canvasAlpha = 1f;
public bool canvasInteractable = false;
public bool canvasBlocksRaycasts = false;
[Header("Raycast")]
public bool applySelfImageRaycast = false;
public bool selfImageRaycastTarget = false;
public bool applyChildImageRaycasts = false;
public bool childImageRaycastTarget = false;
public bool preserveButtonImageRaycasts = true;
public bool applyChildTextRaycasts = false;
public bool childTextRaycastTarget = false;
}
[Serializable]
public class InventoryControlledTextTarget
{
[Header("Target")]
public string name;
public TMP_Text text;
[Header("Rect Layout")]
public bool applyRect = false;
public InventoryUIAnchorPreset anchorPreset = InventoryUIAnchorPreset.MiddleCenter;
public Vector2 customAnchorMin = new Vector2(0.5f, 0.5f);
public Vector2 customAnchorMax = new Vector2(0.5f, 0.5f);
public Vector2 customPivot = new Vector2(0.5f, 0.5f);
public Vector2 size = new Vector2(100f, 30f);
public Vector2 anchoredPosition = Vector2.zero;
[Header("Text Settings")]
public bool applyFontSize = false;
public float fontSize = 20f;
public bool applyAlignment = false;
public TextAlignmentOptions alignment = TextAlignmentOptions.Center;
public bool applyColor = false;
public Color color = Color.white;
public bool applyRaycastTarget = true;
public bool raycastTarget = false;
public bool resetScaleToOne = true;
[Header("Outline / Shadow")]
public bool applyOutline = false;
public bool addOutlineIfMissing = true;
public Color outlineColor = new Color(0f, 0.75f, 1f, 0.8f);
public Vector2 outlineEffectDistance = new Vector2(1f, -1f);
public bool outlineUseGraphicAlpha = true;
public bool applyShadow = false;
public bool addShadowIfMissing = true;
public Color shadowColor = new Color(0f, 0f, 0f, 0.65f);
public Vector2 shadowEffectDistance = new Vector2(2f, -2f);
public bool shadowUseGraphicAlpha = true;
}
[Serializable]
public class InventoryControlledImageTarget
{
[Header("Target")]
public string name;
public Image image;
[Header("Rect Layout")]
public bool applyRect = false;
public InventoryUIAnchorPreset anchorPreset = InventoryUIAnchorPreset.MiddleCenter;
public Vector2 customAnchorMin = new Vector2(0.5f, 0.5f);
public Vector2 customAnchorMax = new Vector2(0.5f, 0.5f);
public Vector2 customPivot = new Vector2(0.5f, 0.5f);
public Vector2 size = new Vector2(64f, 64f);
public Vector2 anchoredPosition = Vector2.zero;
[Header("Image Settings")]
public bool applyColor = false;
public Color color = Color.white;
public bool applySprite = false;
public Sprite sprite;
public bool applyRaycastTarget = true;
public bool raycastTarget = false;
public bool resetScaleToOne = true;
[Header("Outline / Shadow")]
public bool applyOutline = false;
public bool addOutlineIfMissing = true;
public Color outlineColor = new Color(0f, 0.75f, 1f, 0.8f);
public Vector2 outlineEffectDistance = new Vector2(1f, -1f);
public bool outlineUseGraphicAlpha = true;
public bool applyShadow = false;
public bool addShadowIfMissing = true;
public Color shadowColor = new Color(0f, 0f, 0f, 0.55f);
public Vector2 shadowEffectDistance = new Vector2(2f, -2f);
public bool shadowUseGraphicAlpha = true;
}
[Serializable]
public class InventoryControlledButtonTarget
{
[Header("Target")]
public string name;
public Button button;
[Header("Rect Layout")]
public bool applyRect = false;
public InventoryUIAnchorPreset anchorPreset = InventoryUIAnchorPreset.MiddleCenter;
public Vector2 customAnchorMin = new Vector2(0.5f, 0.5f);
public Vector2 customAnchorMax = new Vector2(0.5f, 0.5f);
public Vector2 customPivot = new Vector2(0.5f, 0.5f);
public Vector2 size = new Vector2(100f, 35f);
public Vector2 anchoredPosition = Vector2.zero;
[Header("Button Settings")]
public bool applyInteractable = false;
public bool interactable = true;
public bool applyImageRaycastTarget = true;
public bool imageRaycastTarget = true;
public bool disableChildTextRaycasts = true;
public bool resetScaleToOne = true;
[Header("Button Colors")]
public bool applyButtonColors = false;
public Color normalColor = new Color(0.07f, 0.18f, 0.30f, 0.90f);
public Color highlightedColor = new Color(0.12f, 0.42f, 0.70f, 1f);
public Color pressedColor = new Color(0.04f, 0.12f, 0.20f, 1f);
public Color selectedColor = new Color(0.15f, 0.50f, 0.85f, 1f);
public Color disabledColor = new Color(0.20f, 0.20f, 0.20f, 0.45f);
public float colorMultiplier = 1f;
public float fadeDuration = 0.1f;
[Header("Outline / Shadow")]
public bool applyOutline = false;
public bool addOutlineIfMissing = true;
public Color outlineColor = new Color(0f, 0.75f, 1f, 0.8f);
public Vector2 outlineEffectDistance = new Vector2(1f, -1f);
public bool outlineUseGraphicAlpha = true;
public bool applyShadow = false;
public bool addShadowIfMissing = true;
public Color shadowColor = new Color(0f, 0f, 0f, 0.55f);
public Vector2 shadowEffectDistance = new Vector2(2f, -2f);
public bool shadowUseGraphicAlpha = true;
}
[Serializable]
public class InventoryLayoutPadding
{
public int left;
public int right;
public int top;
public int bottom;
public InventoryLayoutPadding() { }
public InventoryLayoutPadding(int left, int right, int top, int bottom)
{
this.left = left;
this.right = right;
this.top = top;
this.bottom = bottom;
}
public RectOffset ToRectOffset()
{
return new RectOffset(left, right, top, bottom);
}
}
[Serializable]
public class InventoryControlledGridLayoutTarget
{
[Header("Target")]
public string name;
public GridLayoutGroup grid;
[Header("Grid Settings")]
public bool applyGrid = true;
public Vector2 cellSize = new Vector2(100f, 100f);
public Vector2 spacing = Vector2.zero;
public TextAnchor childAlignment = TextAnchor.MiddleCenter;
public GridLayoutGroup.Constraint constraint = GridLayoutGroup.Constraint.FixedColumnCount;
public int constraintCount = 5;
public InventoryLayoutPadding padding;
}
///
/// InventoryRoot 아래 UI를 한 곳에서 정리하는 컨트롤러입니다.
/// - 기본 인벤토리 패널들의 위치/크기/배치를 제어합니다.
/// - Inspector에 직접 연결한 Rect/Text/Image/Button/Grid 대상도 제어합니다.
/// - 없는 CanvasGroup, Image, GridLayoutGroup, AudioSource 등을 자동으로 붙일 수 있습니다.
/// - Text 내용 자체는 바꾸지 않습니다. 내용 변경은 InventoryUI가 담당하게 두는 것을 권장합니다.
///
[ExecuteAlways]
[DisallowMultipleComponent]
public class InventoryUILayoutController : MonoBehaviour
{
[Header("Root / Canvas")]
[SerializeField] private bool configureRootCanvas = true;
[SerializeField] private Vector2 rootCanvasSize = new Vector2(800f, 500f);
[SerializeField] private bool applyRootWorldScale = false;
[SerializeField] private Vector3 rootWorldScale = new Vector3(0.0015f, 0.0015f, 0.0015f);
[SerializeField] private bool addGraphicRaycasterIfMissing = true;
[SerializeField] private bool addTrackedDeviceGraphicRaycasterIfAvailable = true;
[Header("Known UI References")]
[SerializeField] private RectTransform inventoryPanel;
[SerializeField] private RectTransform titleText;
[SerializeField] private RectTransform closeButton;
[SerializeField] private RectTransform categoryButtons;
[SerializeField] private RectTransform memoryProgressArea;
[SerializeField] private RectTransform slotContainer;
[SerializeField] private RectTransform recentLogArea;
[SerializeField] private RectTransform debugPanel;
[SerializeField] private RectTransform quickUseButtons;
[Header("Popup / Floating Panels")]
[SerializeField] private RectTransform tooltipPanel;
[SerializeField] private RectTransform acquisitionPopupPanel;
[SerializeField] private RectTransform messagePanel;
[SerializeField] private RectTransform confirmationPanel;
[SerializeField] private RectTransform detailPanel;
[Header("Inventory Scripts")]
[SerializeField] private InventoryUI inventoryUI;
[SerializeField] private InventorySlotUI[] slots;
[Header("Auto Find Names")]
[SerializeField] private string inventoryPanelName = "InventoryPanel";
[SerializeField] private string titleTextName = "TitleText";
[SerializeField] private string closeButtonName = "CloseButton";
[SerializeField] private string categoryButtonsName = "CategoryButtons";
[SerializeField] private string memoryProgressAreaName = "MemoryProgressArea";
[SerializeField] private string slotContainerName = "SlotContainer";
[SerializeField] private string recentLogAreaName = "RecentLogArea";
[SerializeField] private string debugPanelName = "DebugPanel";
[SerializeField] private string quickUseButtonsName = "QuickUseButtons";
[SerializeField] private string tooltipPanelName = "TooltipPanel";
[SerializeField] private string acquisitionPopupPanelName = "AcquisitionPopupPanel";
[SerializeField] private string messagePanelName = "MessagePanel";
[SerializeField] private string confirmationPanelName = "ConfirmationPanel";
[SerializeField] private string detailPanelName = "DetailPanel";
[Header("Apply Settings")]
[SerializeField] private bool autoFindReferences = true;
[SerializeField] private bool autoAddMissingComponents = true;
[SerializeField] private bool applyKnownLayout = true;
[SerializeField] private bool applyCustomControlledTargets = true;
[SerializeField] private bool resetChildScaleToOne = true;
[SerializeField] private bool applyLayoutOnAwake = true;
[SerializeField] private bool applyModeOnAwake = true;
[Tooltip("InventoryUI.Start()의 visibleOnStart 처리 이후에 모드를 한 번 더 적용해 패널 ON/OFF 충돌을 줄입니다.")]
[SerializeField] private bool applyModeAfterOneFrame = true;
[SerializeField] private bool applyInEditor = true;
[SerializeField] private InventoryRootUIMode initialMode = InventoryRootUIMode.TestMode;
[Header("Known Panel Layout")]
[SerializeField] private Vector2 inventoryPanelSize = new Vector2(760f, 460f);
[SerializeField] private Vector2 inventoryPanelPos = Vector2.zero;
[SerializeField] private Vector2 titleTextSize = new Vector2(360f, 50f);
[SerializeField] private Vector2 titleTextPos = new Vector2(0f, -18f);
[SerializeField] private Vector2 closeButtonSize = new Vector2(70f, 38f);
[SerializeField] private Vector2 closeButtonPos = new Vector2(-18f, -18f);
[SerializeField] private Vector2 categoryButtonsSize = new Vector2(620f, 45f);
[SerializeField] private Vector2 categoryButtonsPos = new Vector2(0f, -75f);
[SerializeField] private Vector2 memoryProgressAreaSize = new Vector2(620f, 55f);
[SerializeField] private Vector2 memoryProgressAreaPos = new Vector2(0f, -125f);
[SerializeField] private Vector2 slotContainerSize = new Vector2(500f, 145f);
[SerializeField] private Vector2 slotContainerPos = new Vector2(-80f, 20f);
[SerializeField] private Vector2 recentLogSize = new Vector2(340f, 110f);
[SerializeField] private Vector2 recentLogPos = new Vector2(25f, 25f);
[SerializeField] private Vector2 debugPanelSize = new Vector2(300f, 110f);
[SerializeField] private Vector2 debugPanelPos = new Vector2(-25f, 25f);
[SerializeField] private Vector2 quickUseButtonsSize = new Vector2(130f, 160f);
[SerializeField] private Vector2 quickUseButtonsPos = new Vector2(-20f, -35f);
[Header("Known Popup / Detail Layout")]
[SerializeField] private Vector2 tooltipPanelSize = new Vector2(520f, 120f);
[SerializeField] private Vector2 tooltipPanelPos = new Vector2(0f, -15f);
[SerializeField] private Vector2 acquisitionPopupSize = new Vector2(360f, 70f);
[SerializeField] private Vector2 acquisitionPopupPos = new Vector2(0f, 40f);
[SerializeField] private Vector2 messagePanelSize = new Vector2(460f, 60f);
[SerializeField] private Vector2 messagePanelPos = new Vector2(0f, -85f);
[SerializeField] private Vector2 confirmationPanelSize = new Vector2(420f, 220f);
[SerializeField] private Vector2 confirmationPanelPos = Vector2.zero;
[SerializeField] private Vector2 detailPanelSize = new Vector2(260f, 340f);
[SerializeField] private Vector2 detailPanelPos = new Vector2(-25f, 0f);
[Header("Known LayoutGroup Defaults")]
[SerializeField] private Vector2 slotCellSize = new Vector2(85f, 120f);
[SerializeField] private Vector2 slotSpacing = new Vector2(8f, 0f);
[SerializeField] private int slotColumnCount = 5;
[SerializeField] private Vector2 debugButtonCellSize = new Vector2(85f, 28f);
[SerializeField] private Vector2 debugButtonSpacing = new Vector2(5f, 5f);
[SerializeField] private int debugButtonColumnCount = 3;
[SerializeField] private Vector2 quickUseButtonCellSize = new Vector2(115f, 26f);
[SerializeField] private Vector2 quickUseButtonSpacing = new Vector2(0f, 5f);
[Header("Known Slot Child Layout")]
[SerializeField] private bool arrangeSlotChildren = true;
[SerializeField] private Vector2 slotIconSize = new Vector2(64f, 64f);
[SerializeField] private Vector2 slotIconPos = new Vector2(0f, 18f);
[SerializeField] private Vector2 slotCountTextSize = new Vector2(100f, 28f);
[SerializeField] private Vector2 slotCountTextPos = new Vector2(0f, 8f);
[SerializeField] private Vector2 slotNewBadgeSize = new Vector2(52f, 24f);
[SerializeField] private Vector2 slotNewBadgePos = new Vector2(-4f, -4f);
[Header("Mode Active States")]
[SerializeField] private bool testShowInventoryPanel = true;
[SerializeField] private bool testShowDebugPanel = true;
[SerializeField] private bool testShowRecentLogArea = true;
[SerializeField] private bool testShowCategoryButtons = true;
[SerializeField] private bool testShowMemoryProgressArea = true;
[SerializeField] private bool testShowQuickUseButtons = false;
[SerializeField] private bool playShowInventoryPanel = false;
[SerializeField] private bool playShowDebugPanel = false;
[SerializeField] private bool playShowRecentLogArea = true;
[SerializeField] private bool playShowCategoryButtons = true;
[SerializeField] private bool playShowMemoryProgressArea = true;
[SerializeField] private bool playShowQuickUseButtons = false;
[SerializeField] private bool buildShowInventoryPanel = false;
[SerializeField] private bool buildShowDebugPanel = false;
[SerializeField] private bool buildShowRecentLogArea = true;
[SerializeField] private bool buildShowCategoryButtons = true;
[SerializeField] private bool buildShowMemoryProgressArea = true;
[SerializeField] private bool buildShowQuickUseButtons = false;
[Header("Custom Inspector-Controlled Targets")]
[Tooltip("패널, 영역, 빈 오브젝트 등 RectTransform 기준으로 제어할 대상입니다.")]
[SerializeField] private List controlledRects = new List();
[Tooltip("패널 자식 TMP_Text의 위치, 크기, 폰트 크기, 정렬, Raycast를 제어합니다. Text 내용은 바꾸지 않습니다.")]
[SerializeField] private List controlledTexts = new List();
[Tooltip("패널 자식 Image의 위치, 크기, 색상, Raycast를 제어합니다. Sprite는 Apply Sprite를 켰을 때만 바꿉니다.")]
[SerializeField] private List controlledImages = new List();
[Tooltip("패널 자식 Button의 위치, 크기, Interactable, Raycast를 제어합니다.")]
[SerializeField] private List controlledButtons = new List();
[Tooltip("SlotContainer, DebugPanel 같은 GridLayoutGroup의 Cell Size, Spacing, Count를 제어합니다.")]
[SerializeField] private List controlledGridLayouts = new List();
[Header("Basic Visual Theme")]
[SerializeField] private bool applyBasicVisualTheme = true;
[SerializeField] private bool styleKnownPanels = true;
[SerializeField] private bool styleKnownButtons = true;
[SerializeField] private bool styleKnownTexts = true;
[SerializeField] private bool styleKnownSlots = true;
[SerializeField] private bool addOutlineToKnownGraphics = true;
[SerializeField] private bool addShadowToKnownTexts = true;
[Header("Theme Colors - Ocean VR")]
[SerializeField] private Color mainPanelColor = new Color(0.03f, 0.08f, 0.15f, 0.88f);
[SerializeField] private Color subPanelColor = new Color(0.04f, 0.14f, 0.24f, 0.82f);
[SerializeField] private Color popupPanelColor = new Color(0.02f, 0.10f, 0.18f, 0.92f);
[SerializeField] private Color slotColor = new Color(0.05f, 0.16f, 0.26f, 0.90f);
[SerializeField] private Color slotImportantGlowColor = new Color(1.0f, 0.72f, 0.20f, 0.38f);
[SerializeField] private Color filteredOverlayColor = new Color(0f, 0f, 0f, 0.55f);
[SerializeField] private Color newBadgeColor = new Color(1.0f, 0.82f, 0.12f, 0.95f);
[SerializeField] private Color mainTextColor = new Color(0.95f, 0.92f, 0.82f, 1f);
[SerializeField] private Color accentTextColor = new Color(1.0f, 0.78f, 0.35f, 1f);
[SerializeField] private Color mutedTextColor = new Color(0.70f, 0.86f, 0.95f, 1f);
[SerializeField] private Color outlineColor = new Color(0.18f, 0.76f, 1f, 0.65f);
[SerializeField] private Color goldOutlineColor = new Color(1.0f, 0.72f, 0.20f, 0.80f);
[SerializeField] private Color shadowColor = new Color(0f, 0f, 0f, 0.62f);
[Header("Theme Button Colors")]
[SerializeField] private Color buttonNormalColor = new Color(0.06f, 0.20f, 0.33f, 0.95f);
[SerializeField] private Color buttonHighlightedColor = new Color(0.12f, 0.44f, 0.74f, 1f);
[SerializeField] private Color buttonPressedColor = new Color(0.03f, 0.12f, 0.22f, 1f);
[SerializeField] private Color buttonSelectedColor = new Color(0.16f, 0.50f, 0.82f, 1f);
[SerializeField] private Color buttonDisabledColor = new Color(0.20f, 0.22f, 0.24f, 0.45f);
[SerializeField] private Color debugButtonColor = new Color(0.07f, 0.22f, 0.32f, 0.95f);
[SerializeField] private Color dangerButtonColor = new Color(0.45f, 0.10f, 0.12f, 0.95f);
[Header("Popup Groups")]
[SerializeField] private GameObject[] extraPopupPanelsToHide;
[Header("Raycast / Component Fix")]
[SerializeField] private bool fixRaycastTargets = true;
[SerializeField] private bool fixCanvasGroups = true;
[SerializeField] private bool disableTextRaycasts = true;
[SerializeField] private bool disableNonInteractiveImageRaycasts = true;
[SerializeField] private bool setSliderInteractableFalse = true;
[SerializeField] private bool setSlotHoverSafeValues = true;
[SerializeField] private bool slotUseHoverScale = true;
[SerializeField] private float slotHoverScale = 1.03f;
[SerializeField] private bool slotBringToFrontOnHover = false;
private bool isApplying;
private void Reset()
{
AutoFindReferences();
}
private void Awake()
{
if (!Application.isPlaying)
return;
if (autoFindReferences)
AutoFindReferences();
if (autoAddMissingComponents)
AutoSetupMissingComponents();
if (applyLayoutOnAwake)
ApplyFullInventoryRootLayout();
if (applyModeOnAwake)
ApplyInitialMode();
}
private System.Collections.IEnumerator Start()
{
if (!Application.isPlaying || !applyModeAfterOneFrame)
yield break;
// InventoryUI.Start()의 visibleOnStart 적용이 끝난 뒤 모드를 다시 적용합니다.
yield return null;
ApplyInitialMode();
}
#if UNITY_EDITOR
private void OnValidate()
{
if (!applyInEditor)
return;
EditorApplication.delayCall -= DelayedApplyInEditor;
EditorApplication.delayCall += DelayedApplyInEditor;
}
private void DelayedApplyInEditor()
{
if (this == null || isApplying)
return;
ApplyFullInventoryRootLayout();
}
#endif
[ContextMenu("Inventory UI/Apply Full Inventory Root Layout")]
public void ApplyFullInventoryRootLayout()
{
if (isApplying)
return;
isApplying = true;
if (autoFindReferences)
AutoFindReferences();
if (autoAddMissingComponents)
AutoSetupMissingComponents();
if (configureRootCanvas)
ConfigureRootCanvas();
if (applyKnownLayout)
ApplyKnownLayout();
if (arrangeSlotChildren)
ArrangeSlotChildren();
if (applyCustomControlledTargets)
ApplyCustomTargets();
if (applyBasicVisualTheme)
ApplyBasicVisualTheme();
if (fixRaycastTargets)
FixAllRaycastTargets();
isApplying = false;
}
[ContextMenu("Inventory UI/Apply Custom Inspector Targets Only")]
public void ApplyCustomTargets()
{
ApplyControlledRectTargets();
ApplyControlledTextTargets();
ApplyControlledImageTargets();
ApplyControlledButtonTargets();
ApplyControlledGridLayoutTargets();
}
[ContextMenu("Inventory UI/Auto Find References")]
public void AutoFindReferences()
{
inventoryPanel = FindRectIfNull(inventoryPanel, inventoryPanelName);
titleText = FindRectIfNull(titleText, titleTextName);
closeButton = FindRectIfNull(closeButton, closeButtonName);
categoryButtons = FindRectIfNull(categoryButtons, categoryButtonsName);
memoryProgressArea = FindRectIfNull(memoryProgressArea, memoryProgressAreaName);
slotContainer = FindRectIfNull(slotContainer, slotContainerName);
recentLogArea = FindRectIfNull(recentLogArea, recentLogAreaName);
debugPanel = FindRectIfNull(debugPanel, debugPanelName);
quickUseButtons = FindRectIfNull(quickUseButtons, quickUseButtonsName);
tooltipPanel = FindRectIfNull(tooltipPanel, tooltipPanelName);
acquisitionPopupPanel = FindRectIfNull(acquisitionPopupPanel, acquisitionPopupPanelName);
messagePanel = FindRectIfNull(messagePanel, messagePanelName);
confirmationPanel = FindRectIfNull(confirmationPanel, confirmationPanelName);
detailPanel = FindRectIfNull(detailPanel, detailPanelName);
if (inventoryUI == null)
inventoryUI = GetComponent();
if (slots == null || slots.Length == 0)
slots = GetComponentsInChildren(true);
}
[ContextMenu("Inventory UI/Auto Setup Missing Components")]
public void AutoSetupMissingComponents()
{
Canvas canvas = GetOrAdd