644 lines
21 KiB
C#
644 lines
21 KiB
C#
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using TMPro;
|
|
using UnityEngine;
|
|
using UnityEngine.EventSystems;
|
|
using UnityEngine.UI;
|
|
|
|
/// <summary>
|
|
/// 전체 인벤토리 UI를 갱신하는 스크립트입니다.
|
|
/// 기능: 패널 열기/닫기, 슬롯 갱신, 획득 팝업, 부족 안내 메시지, 확인창, 상세보기, 획득 로그, 기억의 조각 진행도, 간단한 손목/타겟 추적.
|
|
/// InventoryUI가 붙은 오브젝트는 항상 켜두고, 실제 보이는 inventoryPanel만 켜고 끄는 구조를 권장합니다.
|
|
/// </summary>
|
|
[DisallowMultipleComponent]
|
|
public class InventoryUI : MonoBehaviour
|
|
{
|
|
[Header("References")]
|
|
[SerializeField] private InventoryManager inventoryManager;
|
|
[Tooltip("실제로 보이거나 숨겨질 자식 패널입니다. InventoryUI가 붙은 자기 자신을 넣지 않는 것을 권장합니다.")]
|
|
[SerializeField] private GameObject inventoryPanel;
|
|
[SerializeField] private InventorySlotUI[] slots;
|
|
|
|
[Header("Panel Settings")]
|
|
[SerializeField] private bool autoFindManager = true;
|
|
[SerializeField] private bool autoFindPanelChild = true;
|
|
[SerializeField] private string autoPanelChildName = "InventoryPanel";
|
|
[SerializeField] private bool visibleOnStart = false;
|
|
[SerializeField] private bool refreshOnEnable = true;
|
|
[SerializeField] private bool showNewBadgeOnIncrease = true;
|
|
[SerializeField] private bool preventDisablingSelf = true;
|
|
|
|
[Header("Filter")]
|
|
[SerializeField] private InventoryItemCategory currentFilter = InventoryItemCategory.All;
|
|
[SerializeField] private bool hideSlotsOutsideFilter = false;
|
|
|
|
[Header("Acquisition Popup")]
|
|
[SerializeField] private GameObject acquisitionPopupPanel;
|
|
[SerializeField] private CanvasGroup acquisitionPopupCanvasGroup;
|
|
[SerializeField] private Image acquisitionIconImage;
|
|
[SerializeField] private TMP_Text acquisitionText;
|
|
[SerializeField] private float acquisitionPopupTime = 1.4f;
|
|
[SerializeField] private string acquisitionFormat = "{0} x{1} 획득!";
|
|
|
|
[Header("Message Popup")]
|
|
[SerializeField] private GameObject messagePanel;
|
|
[SerializeField] private CanvasGroup messageCanvasGroup;
|
|
[SerializeField] private TMP_Text messageText;
|
|
[SerializeField] private float messageShowTime = 1.5f;
|
|
|
|
[Header("Confirmation UI")]
|
|
[SerializeField] private GameObject confirmationPanel;
|
|
[SerializeField] private TMP_Text confirmationTitleText;
|
|
[SerializeField] private TMP_Text confirmationBodyText;
|
|
[SerializeField] private Button confirmationYesButton;
|
|
[SerializeField] private Button confirmationNoButton;
|
|
|
|
[Header("Detail UI")]
|
|
[SerializeField] private GameObject detailPanel;
|
|
[SerializeField] private Image detailIconImage;
|
|
[SerializeField] private TMP_Text detailTitleText;
|
|
[SerializeField] private TMP_Text detailDescriptionText;
|
|
[SerializeField] private TMP_Text detailCountText;
|
|
[SerializeField] private TMP_Text detailGoalText;
|
|
[SerializeField] private Button detailUseButton;
|
|
|
|
[Header("Recent Log UI")]
|
|
[SerializeField] private TMP_Text[] recentLogTexts;
|
|
|
|
[Header("Memory Piece UI")]
|
|
[SerializeField] private TMP_Text memoryProgressText;
|
|
[SerializeField] private Slider memoryProgressSlider;
|
|
[SerializeField] private string memoryProgressFormat = "기억의 조각 {0} / {1}";
|
|
|
|
[Header("Audio")]
|
|
[SerializeField] private AudioSource uiAudioSource;
|
|
[SerializeField] private AudioClip defaultAcquisitionClip;
|
|
[SerializeField] private AudioClip defaultUseClip;
|
|
[SerializeField] private AudioClip defaultErrorClip;
|
|
|
|
[Header("Optional Follow Target / Wrist UI")]
|
|
[SerializeField] private bool followTarget = false;
|
|
[SerializeField] private Transform targetToFollow;
|
|
[SerializeField] private Vector3 localPositionOffset = new Vector3(0f, 0.08f, 0.12f);
|
|
[SerializeField] private Vector3 localEulerOffset = Vector3.zero;
|
|
[SerializeField] private bool faceMainCamera = false;
|
|
|
|
[Header("Editor Test Toggle")]
|
|
[SerializeField] private bool enableKeyboardToggleForTesting = false;
|
|
[SerializeField] private KeyCode keyboardToggleKey = KeyCode.I;
|
|
|
|
private bool subscribed;
|
|
private Coroutine acquisitionRoutine;
|
|
private Coroutine messageRoutine;
|
|
private InventoryItemType pendingUseItemType;
|
|
private int pendingUseAmount = 1;
|
|
private bool hasPendingUse;
|
|
private InventoryItemType currentDetailItemType;
|
|
private bool hasDetailItem;
|
|
|
|
private void Awake()
|
|
{
|
|
if (inventoryManager == null && InventoryManager.Instance != null)
|
|
inventoryManager = InventoryManager.Instance;
|
|
|
|
if (inventoryManager == null && autoFindManager)
|
|
inventoryManager = FindFirstObjectByType<InventoryManager>();
|
|
|
|
if (inventoryPanel == null && autoFindPanelChild)
|
|
{
|
|
Transform panelTransform = transform.Find(autoPanelChildName);
|
|
if (panelTransform != null)
|
|
inventoryPanel = panelTransform.gameObject;
|
|
}
|
|
|
|
if (uiAudioSource == null)
|
|
uiAudioSource = GetComponent<AudioSource>();
|
|
|
|
if (acquisitionPopupPanel != null)
|
|
acquisitionPopupPanel.SetActive(false);
|
|
|
|
if (messagePanel != null)
|
|
messagePanel.SetActive(false);
|
|
|
|
if (confirmationPanel != null)
|
|
confirmationPanel.SetActive(false);
|
|
|
|
if (detailPanel != null)
|
|
detailPanel.SetActive(false);
|
|
|
|
if (confirmationYesButton != null)
|
|
{
|
|
confirmationYesButton.onClick.RemoveListener(ConfirmPendingUse);
|
|
confirmationYesButton.onClick.AddListener(ConfirmPendingUse);
|
|
}
|
|
|
|
if (confirmationNoButton != null)
|
|
{
|
|
confirmationNoButton.onClick.RemoveListener(CancelPendingUse);
|
|
confirmationNoButton.onClick.AddListener(CancelPendingUse);
|
|
}
|
|
|
|
if (detailUseButton != null)
|
|
{
|
|
detailUseButton.onClick.RemoveListener(UseCurrentDetailItem);
|
|
detailUseButton.onClick.AddListener(UseCurrentDetailItem);
|
|
}
|
|
|
|
ApplyItemDataToSlots();
|
|
}
|
|
|
|
private void Start()
|
|
{
|
|
SetVisible(visibleOnStart);
|
|
RefreshAllSlots();
|
|
RefreshRecentLogs();
|
|
RefreshMemoryProgress();
|
|
}
|
|
|
|
private void OnEnable()
|
|
{
|
|
Subscribe();
|
|
|
|
if (refreshOnEnable)
|
|
RefreshAllSlots();
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
Unsubscribe();
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
if (enableKeyboardToggleForTesting && Input.GetKeyDown(keyboardToggleKey))
|
|
ToggleVisible();
|
|
}
|
|
|
|
private void LateUpdate()
|
|
{
|
|
if (!followTarget || targetToFollow == null)
|
|
return;
|
|
|
|
transform.position = targetToFollow.TransformPoint(localPositionOffset);
|
|
transform.rotation = targetToFollow.rotation * Quaternion.Euler(localEulerOffset);
|
|
|
|
if (faceMainCamera && Camera.main != null)
|
|
{
|
|
Vector3 direction = transform.position - Camera.main.transform.position;
|
|
if (direction.sqrMagnitude > 0.0001f)
|
|
transform.rotation = Quaternion.LookRotation(direction.normalized, Vector3.up);
|
|
}
|
|
}
|
|
|
|
public void SetVisible(bool visible)
|
|
{
|
|
if (inventoryPanel == null)
|
|
return;
|
|
|
|
if (preventDisablingSelf && inventoryPanel == gameObject)
|
|
{
|
|
Debug.LogWarning("[InventoryUI] InventoryPanel에 자기 자신이 들어가 있습니다. InventoryRoot는 항상 켜두고 자식 InventoryPanel만 연결하세요.", this);
|
|
return;
|
|
}
|
|
|
|
inventoryPanel.SetActive(visible);
|
|
|
|
if (visible)
|
|
RefreshAllSlots();
|
|
}
|
|
|
|
public void ToggleVisible()
|
|
{
|
|
if (inventoryPanel != null)
|
|
SetVisible(!inventoryPanel.activeSelf);
|
|
}
|
|
|
|
public void ShowInventory() => SetVisible(true);
|
|
public void HideInventory() => SetVisible(false);
|
|
|
|
public void SetManager(InventoryManager manager)
|
|
{
|
|
if (inventoryManager == manager)
|
|
return;
|
|
|
|
Unsubscribe();
|
|
inventoryManager = manager;
|
|
Subscribe();
|
|
ApplyItemDataToSlots();
|
|
RefreshAllSlots();
|
|
}
|
|
|
|
public void SetFilterAll() => SetFilter(InventoryItemCategory.All);
|
|
public void SetFilterConsumable() => SetFilter(InventoryItemCategory.Consumable);
|
|
public void SetFilterQuest() => SetFilter(InventoryItemCategory.Quest);
|
|
public void SetFilterKeyItem() => SetFilter(InventoryItemCategory.KeyItem);
|
|
public void SetFilterMaterial() => SetFilter(InventoryItemCategory.Material);
|
|
|
|
public void SetFilter(InventoryItemCategory category)
|
|
{
|
|
currentFilter = category;
|
|
ApplyFilterToSlots();
|
|
}
|
|
|
|
public void RefreshAllSlots()
|
|
{
|
|
if (inventoryManager == null || slots == null)
|
|
return;
|
|
|
|
for (int i = 0; i < slots.Length; i++)
|
|
{
|
|
InventorySlotUI slot = slots[i];
|
|
if (slot == null)
|
|
continue;
|
|
|
|
InventoryItemDefinition definition = inventoryManager.GetDefinition(slot.ItemType);
|
|
slot.SetInventoryUI(this);
|
|
slot.SetDefinition(definition);
|
|
|
|
int count = inventoryManager.GetItemCount(slot.ItemType);
|
|
int maxCount = inventoryManager.GetMaxCount(slot.ItemType);
|
|
slot.SetCount(count, false, maxCount);
|
|
}
|
|
|
|
ApplyFilterToSlots();
|
|
}
|
|
|
|
private void RefreshSlot(InventoryItemType itemType, int count)
|
|
{
|
|
if (slots == null)
|
|
return;
|
|
|
|
int maxCount = inventoryManager != null ? inventoryManager.GetMaxCount(itemType) : 0;
|
|
|
|
for (int i = 0; i < slots.Length; i++)
|
|
{
|
|
InventorySlotUI slot = slots[i];
|
|
if (slot == null || slot.ItemType != itemType)
|
|
continue;
|
|
|
|
slot.SetCount(count, showNewBadgeOnIncrease, maxCount);
|
|
}
|
|
|
|
if (hasDetailItem && currentDetailItemType == itemType)
|
|
ShowItemDetail(itemType);
|
|
}
|
|
|
|
private void ApplyItemDataToSlots()
|
|
{
|
|
if (inventoryManager == null || slots == null)
|
|
return;
|
|
|
|
for (int i = 0; i < slots.Length; i++)
|
|
{
|
|
InventorySlotUI slot = slots[i];
|
|
if (slot == null)
|
|
continue;
|
|
|
|
slot.SetInventoryUI(this);
|
|
slot.SetDefinition(inventoryManager.GetDefinition(slot.ItemType));
|
|
}
|
|
}
|
|
|
|
private void ApplyFilterToSlots()
|
|
{
|
|
if (slots == null)
|
|
return;
|
|
|
|
for (int i = 0; i < slots.Length; i++)
|
|
{
|
|
InventorySlotUI slot = slots[i];
|
|
if (slot == null)
|
|
continue;
|
|
|
|
bool visible = currentFilter == InventoryItemCategory.All || slot.Category == currentFilter;
|
|
if (hideSlotsOutsideFilter)
|
|
slot.gameObject.SetActive(visible);
|
|
else
|
|
slot.SetFilteredOut(!visible);
|
|
}
|
|
}
|
|
|
|
private void Subscribe()
|
|
{
|
|
if (subscribed || inventoryManager == null)
|
|
return;
|
|
|
|
inventoryManager.ItemCountChanged += RefreshSlot;
|
|
inventoryManager.ItemAdded += HandleItemAdded;
|
|
inventoryManager.ItemUsed += HandleItemUsed;
|
|
inventoryManager.MessageRequested += ShowMessage;
|
|
inventoryManager.LogAdded += HandleLogAdded;
|
|
inventoryManager.MemoryPieceProgressChanged += HandleMemoryPieceProgressChanged;
|
|
subscribed = true;
|
|
}
|
|
|
|
private void Unsubscribe()
|
|
{
|
|
if (!subscribed || inventoryManager == null)
|
|
return;
|
|
|
|
inventoryManager.ItemCountChanged -= RefreshSlot;
|
|
inventoryManager.ItemAdded -= HandleItemAdded;
|
|
inventoryManager.ItemUsed -= HandleItemUsed;
|
|
inventoryManager.MessageRequested -= ShowMessage;
|
|
inventoryManager.LogAdded -= HandleLogAdded;
|
|
inventoryManager.MemoryPieceProgressChanged -= HandleMemoryPieceProgressChanged;
|
|
subscribed = false;
|
|
}
|
|
|
|
private void HandleItemAdded(InventoryItemType itemType, int addedAmount, int totalCount)
|
|
{
|
|
ShowAcquisitionPopup(itemType, addedAmount);
|
|
}
|
|
|
|
private void HandleItemUsed(InventoryItemType itemType, int count)
|
|
{
|
|
AudioClip clip = inventoryManager != null ? inventoryManager.GetUseClip(itemType) : null;
|
|
PlayUIClip(clip != null ? clip : defaultUseClip);
|
|
}
|
|
|
|
private void HandleLogAdded(InventoryLogEntry entry)
|
|
{
|
|
RefreshRecentLogs();
|
|
}
|
|
|
|
private void HandleMemoryPieceProgressChanged(int current, int target)
|
|
{
|
|
RefreshMemoryProgress(current, target);
|
|
}
|
|
|
|
public void ShowAcquisitionPopup(InventoryItemType itemType, int amount)
|
|
{
|
|
if (inventoryManager == null)
|
|
return;
|
|
|
|
string displayName = inventoryManager.GetDisplayName(itemType);
|
|
Sprite icon = inventoryManager.GetIcon(itemType);
|
|
|
|
if (acquisitionIconImage != null)
|
|
{
|
|
acquisitionIconImage.sprite = icon;
|
|
acquisitionIconImage.enabled = icon != null;
|
|
}
|
|
|
|
if (acquisitionText != null)
|
|
acquisitionText.text = string.Format(acquisitionFormat, displayName, Mathf.Max(1, amount));
|
|
|
|
AudioClip clip = inventoryManager.GetAcquisitionClip(itemType);
|
|
PlayUIClip(clip != null ? clip : defaultAcquisitionClip);
|
|
|
|
if (acquisitionRoutine != null)
|
|
StopCoroutine(acquisitionRoutine);
|
|
|
|
acquisitionRoutine = StartCoroutine(ShowPanelRoutine(acquisitionPopupPanel, acquisitionPopupCanvasGroup, acquisitionPopupTime));
|
|
}
|
|
|
|
public void ShowMessage(string message)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(message))
|
|
return;
|
|
|
|
if (messageText != null)
|
|
messageText.text = message;
|
|
|
|
if (messageRoutine != null)
|
|
StopCoroutine(messageRoutine);
|
|
|
|
messageRoutine = StartCoroutine(ShowPanelRoutine(messagePanel, messageCanvasGroup, messageShowTime));
|
|
}
|
|
|
|
private IEnumerator ShowPanelRoutine(GameObject panel, CanvasGroup canvasGroup, float showTime)
|
|
{
|
|
if (panel == null)
|
|
yield break;
|
|
|
|
panel.SetActive(true);
|
|
|
|
if (canvasGroup != null)
|
|
canvasGroup.alpha = 1f;
|
|
|
|
yield return new WaitForSecondsRealtime(Mathf.Max(0.05f, showTime));
|
|
|
|
if (canvasGroup != null)
|
|
canvasGroup.alpha = 0f;
|
|
|
|
panel.SetActive(false);
|
|
}
|
|
|
|
public void RequestUseItem(InventoryItemType itemType)
|
|
{
|
|
RequestUseItem(itemType, 1);
|
|
}
|
|
|
|
public void RequestUseItem(InventoryItemType itemType, int amount)
|
|
{
|
|
if (inventoryManager == null)
|
|
return;
|
|
|
|
if (!inventoryManager.IsUsable(itemType))
|
|
{
|
|
ShowMessage($"{inventoryManager.GetDisplayName(itemType)}은(는) 지금 사용할 수 없습니다.");
|
|
PlayUIClip(defaultErrorClip);
|
|
return;
|
|
}
|
|
|
|
if (!inventoryManager.HasItem(itemType, amount))
|
|
{
|
|
ShowMessage(inventoryManager.GetInsufficientMessage(itemType, amount));
|
|
PlayUIClip(defaultErrorClip);
|
|
return;
|
|
}
|
|
|
|
if (inventoryManager.RequiresUseConfirmation(itemType))
|
|
{
|
|
pendingUseItemType = itemType;
|
|
pendingUseAmount = Mathf.Max(1, amount);
|
|
ShowConfirmation(itemType, amount);
|
|
}
|
|
else
|
|
{
|
|
bool result = inventoryManager.UseItem(itemType, amount);
|
|
if (!result)
|
|
PlayUIClip(defaultErrorClip);
|
|
}
|
|
}
|
|
|
|
private void ShowConfirmation(InventoryItemType itemType, int amount)
|
|
{
|
|
pendingUseItemType = itemType;
|
|
pendingUseAmount = Mathf.Max(1, amount);
|
|
hasPendingUse = true;
|
|
|
|
if (confirmationPanel == null)
|
|
{
|
|
ConfirmPendingUse();
|
|
return;
|
|
}
|
|
|
|
string displayName = inventoryManager != null ? inventoryManager.GetDisplayName(itemType) : itemType.ToString();
|
|
|
|
if (confirmationTitleText != null)
|
|
confirmationTitleText.text = "아이템 사용";
|
|
|
|
if (confirmationBodyText != null)
|
|
confirmationBodyText.text = $"{displayName}을(를) 사용하시겠습니까?";
|
|
|
|
confirmationPanel.SetActive(true);
|
|
}
|
|
|
|
public void ConfirmPendingUse()
|
|
{
|
|
if (confirmationPanel != null)
|
|
confirmationPanel.SetActive(false);
|
|
|
|
if (!hasPendingUse)
|
|
return;
|
|
|
|
InventoryItemType itemType = pendingUseItemType;
|
|
int amount = Mathf.Max(1, pendingUseAmount);
|
|
hasPendingUse = false;
|
|
|
|
if (inventoryManager != null)
|
|
{
|
|
bool result = inventoryManager.UseItem(itemType, amount);
|
|
if (!result)
|
|
PlayUIClip(defaultErrorClip);
|
|
}
|
|
}
|
|
|
|
public void CancelPendingUse()
|
|
{
|
|
hasPendingUse = false;
|
|
|
|
if (confirmationPanel != null)
|
|
confirmationPanel.SetActive(false);
|
|
}
|
|
|
|
public void ShowItemDetail(InventoryItemType itemType)
|
|
{
|
|
hasDetailItem = true;
|
|
currentDetailItemType = itemType;
|
|
|
|
if (inventoryManager == null || detailPanel == null)
|
|
return;
|
|
|
|
InventoryItemDefinition definition = inventoryManager.GetDefinition(itemType);
|
|
int count = inventoryManager.GetItemCount(itemType);
|
|
int maxCount = inventoryManager.GetMaxCount(itemType);
|
|
Sprite icon = inventoryManager.GetIcon(itemType);
|
|
|
|
if (detailIconImage != null)
|
|
{
|
|
detailIconImage.sprite = icon;
|
|
detailIconImage.enabled = icon != null;
|
|
}
|
|
|
|
if (detailTitleText != null)
|
|
detailTitleText.text = inventoryManager.GetDisplayName(itemType);
|
|
|
|
if (detailDescriptionText != null)
|
|
detailDescriptionText.text = inventoryManager.GetDescription(itemType);
|
|
|
|
if (detailCountText != null)
|
|
detailCountText.text = maxCount > 0 ? $"보유: x{count} / {maxCount}" : $"보유: x{count}";
|
|
|
|
if (detailGoalText != null)
|
|
detailGoalText.text = inventoryManager.GetGoalHint(itemType);
|
|
|
|
if (detailUseButton != null)
|
|
{
|
|
bool usable = definition != null && definition.usable && count > 0;
|
|
detailUseButton.gameObject.SetActive(definition != null && definition.usable);
|
|
detailUseButton.interactable = usable;
|
|
}
|
|
|
|
detailPanel.SetActive(true);
|
|
}
|
|
|
|
public void HideItemDetail()
|
|
{
|
|
hasDetailItem = false;
|
|
if (detailPanel != null)
|
|
detailPanel.SetActive(false);
|
|
}
|
|
|
|
public void UseCurrentDetailItem()
|
|
{
|
|
if (!hasDetailItem)
|
|
return;
|
|
|
|
RequestUseItem(currentDetailItemType, 1);
|
|
}
|
|
|
|
private void RefreshRecentLogs()
|
|
{
|
|
if (recentLogTexts == null || recentLogTexts.Length == 0 || inventoryManager == null)
|
|
return;
|
|
|
|
IReadOnlyList<InventoryLogEntry> logs = inventoryManager.GetRecentLogs();
|
|
|
|
for (int i = 0; i < recentLogTexts.Length; i++)
|
|
{
|
|
if (recentLogTexts[i] == null)
|
|
continue;
|
|
|
|
recentLogTexts[i].text = i < logs.Count && logs[i] != null ? logs[i].ToString() : string.Empty;
|
|
}
|
|
}
|
|
|
|
private void RefreshMemoryProgress()
|
|
{
|
|
if (inventoryManager == null)
|
|
return;
|
|
|
|
RefreshMemoryProgress(inventoryManager.GetItemCount(inventoryManager.MemoryPieceItemType), inventoryManager.MemoryPieceTargetCount);
|
|
}
|
|
|
|
private void RefreshMemoryProgress(int current, int target)
|
|
{
|
|
target = Mathf.Max(1, target);
|
|
current = Mathf.Clamp(current, 0, target);
|
|
|
|
if (memoryProgressText != null)
|
|
memoryProgressText.text = string.Format(memoryProgressFormat, current, target);
|
|
|
|
if (memoryProgressSlider != null)
|
|
{
|
|
memoryProgressSlider.minValue = 0f;
|
|
memoryProgressSlider.maxValue = target;
|
|
memoryProgressSlider.value = current;
|
|
}
|
|
}
|
|
|
|
private void PlayUIClip(AudioClip clip)
|
|
{
|
|
if (clip == null)
|
|
return;
|
|
|
|
if (uiAudioSource != null)
|
|
uiAudioSource.PlayOneShot(clip);
|
|
else
|
|
AudioSource.PlayClipAtPoint(clip, transform.position);
|
|
}
|
|
|
|
public void CheckVRUISetup()
|
|
{
|
|
Canvas canvas = GetComponentInParent<Canvas>();
|
|
if (canvas == null)
|
|
{
|
|
Debug.LogWarning("[InventoryUI] Canvas가 없습니다.", this);
|
|
return;
|
|
}
|
|
|
|
if (canvas.renderMode != RenderMode.WorldSpace)
|
|
Debug.LogWarning("[InventoryUI] VR UI에서는 Canvas Render Mode를 World Space로 권장합니다.", canvas);
|
|
|
|
if (EventSystem.current == null)
|
|
Debug.LogWarning("[InventoryUI] EventSystem이 없습니다. VR 포인터 UI가 동작하지 않을 수 있습니다.", this);
|
|
|
|
GraphicRaycaster graphicRaycaster = canvas.GetComponent<GraphicRaycaster>();
|
|
if (graphicRaycaster == null)
|
|
Debug.LogWarning("[InventoryUI] Canvas에 GraphicRaycaster 또는 Tracked Device Graphic Raycaster가 필요합니다.", canvas);
|
|
}
|
|
}
|