using System.Collections; using UnityEngine; using UnityEngine.Events; #if UNITY_EDITOR using UnityEditor; #endif /// /// 월드에 떠 있는 아이템 오브젝트를 직접 주울 때 사용합니다. /// VR에서는 Player 태그만 쓰기보다 PlayerInventoryCollector 또는 LayerMask를 함께 쓰는 것을 권장합니다. /// 기능: 중복 획득 저장, 획득 연출, 사운드, 컨트롤러 햅틱, 최대 소지 수량 차단. /// [DisallowMultipleComponent] [RequireComponent(typeof(Collider))] public class ItemPickup : MonoBehaviour { [Header("Reference")] [SerializeField] private InventoryManager inventoryManager; [Header("Item")] [SerializeField] private InventoryItemType itemType = InventoryItemType.Fish; [SerializeField] private int amount = 1; [Header("Pickup Detection")] [Tooltip("true면 충돌한 대상 또는 부모에 PlayerInventoryCollector가 있어야 획득됩니다. VR 손/몸 Collider에 붙이면 안정적입니다.")] [SerializeField] private bool requirePlayerInventoryCollector = true; [SerializeField] private bool acceptPlayerInventoryCollector = true; [Tooltip("LayerMask 검사 사용 여부입니다. Player/Hand 같은 전용 Layer를 만들었다면 사용하는 것을 권장합니다.")] [SerializeField] private bool useLayerMask = false; [SerializeField] private LayerMask pickupLayerMask = ~0; [Tooltip("기존 Player 태그 방식과의 호환용입니다. VR에서는 Collector 또는 LayerMask가 더 안정적입니다.")] [SerializeField] private bool usePlayerTagFallback = true; [SerializeField] private string playerTag = "Player"; [Header("Persistent Pickup")] [Tooltip("true면 한 번 획득한 아이템은 저장 후 다시 나타나지 않게 할 수 있습니다.")] [SerializeField] private bool persistPickupState = false; [SerializeField] private string persistentPickupKey; [SerializeField] private bool disableIfAlreadyPickedUp = true; [Header("Pickup Result")] [SerializeField] private bool destroyAfterPickup = true; [SerializeField] private bool disableAfterPickup = false; [SerializeField] private bool autoFindManager = true; [Header("Pickup Animation")] [SerializeField] private bool playPickupAnimation = true; [SerializeField] private float animationDuration = 0.25f; [SerializeField] private AnimationCurve scaleCurve = AnimationCurve.EaseInOut(0f, 1f, 1f, 0f); [SerializeField] private bool moveTowardCollector = true; [SerializeField] private Transform animationTargetOverride; [Header("Feedback")] [SerializeField] private AudioSource audioSource; [SerializeField] private AudioClip pickupClipOverride; [SerializeField] private bool playCollectorHaptic = true; [Range(0f, 1f)] [SerializeField] private float hapticAmplitude = 0.35f; [SerializeField] private float hapticDuration = 0.08f; [Header("Events")] public UnityEvent onPickedUp; public UnityEvent onPickupBlocked; public UnityEvent onPickupAlreadyConsumed; [Header("Debug")] [SerializeField] private bool showDebugLog = true; private bool pickedUp; private Collider cachedCollider; private Renderer[] renderers; private Vector3 originalScale; private void Awake() { cachedCollider = GetComponent(); renderers = GetComponentsInChildren(); originalScale = transform.localScale; if (audioSource == null) audioSource = GetComponent(); if (cachedCollider != null && !cachedCollider.isTrigger) Debug.LogWarning("[ItemPickup] Collider의 Is Trigger가 꺼져 있습니다. 자동 획득 방식이면 Is Trigger를 켜세요.", this); ResolveManager(); } private void Start() { ResolvePersistentKey(); if (persistPickupState && ResolveManager() != null && inventoryManager.IsPersistentKeyConsumed(persistentPickupKey)) { onPickupAlreadyConsumed?.Invoke(); if (disableIfAlreadyPickedUp) gameObject.SetActive(false); } } private void OnTriggerEnter(Collider other) { if (pickedUp) return; PlayerInventoryCollector collector = other.GetComponentInParent(); if (!CanBePickedUpBy(other, collector)) return; PickUp(collector); } public void PickUp() { PickUp(null); } public void PickUp(PlayerInventoryCollector collector) { if (pickedUp) return; pickedUp = true; InventoryManager manager = ResolveManager(); if (collector != null && collector.InventoryManagerOverride != null) manager = collector.InventoryManagerOverride; if (manager == null) { Debug.LogWarning("[ItemPickup] InventoryManager가 연결되지 않았습니다.", this); pickedUp = false; onPickupBlocked?.Invoke(); return; } int addAmount = Mathf.Max(1, amount); int actuallyAdded = manager.AddItemAndGetAddedAmount(itemType, addAmount); if (actuallyAdded <= 0) { pickedUp = false; onPickupBlocked?.Invoke(); return; } if (persistPickupState) manager.MarkPersistentKeyConsumed(persistentPickupKey); if (showDebugLog) Debug.Log($"[ItemPickup] {itemType} +{actuallyAdded}", this); PlayFeedback(manager, collector); onPickedUp?.Invoke(); if (cachedCollider != null) cachedCollider.enabled = false; if (playPickupAnimation) StartCoroutine(PickupAnimationRoutine(collector)); else FinishPickup(); } private bool CanBePickedUpBy(Collider other, PlayerInventoryCollector collector) { if (collector != null && !collector.CanCollectItems) return false; bool collectorAccepted = acceptPlayerInventoryCollector && collector != null; bool layerAccepted = useLayerMask && other != null && (pickupLayerMask.value & (1 << other.gameObject.layer)) != 0; bool tagAccepted = usePlayerTagFallback && !string.IsNullOrWhiteSpace(playerTag) && (SafeCompareTag(other, playerTag) || SafeCompareTag(other != null ? other.transform.root : null, playerTag)); // true이면 VR 손/몸 Collider에 PlayerInventoryCollector가 붙어 있어야만 획득됩니다. // false이면 Collector, LayerMask, Tag 중 하나만 맞아도 획득됩니다. if (requirePlayerInventoryCollector) return collectorAccepted; return collectorAccepted || layerAccepted || tagAccepted; } private bool SafeCompareTag(Component component, string tagName) { return component != null && SafeCompareTag(component.gameObject, tagName); } private bool SafeCompareTag(GameObject target, string tagName) { if (target == null || string.IsNullOrWhiteSpace(tagName)) return false; try { return target.CompareTag(tagName); } catch (UnityException) { if (showDebugLog) Debug.LogWarning($"[ItemPickup] 태그 '{tagName}'가 프로젝트에 없습니다. Project Settings > Tags and Layers를 확인하세요.", this); return false; } } private IEnumerator PickupAnimationRoutine(PlayerInventoryCollector collector) { float duration = Mathf.Max(0.01f, animationDuration); float elapsed = 0f; Vector3 startPosition = transform.position; Vector3 targetPosition = startPosition; Transform target = animationTargetOverride; if (target == null && moveTowardCollector && collector != null) target = collector.transform; if (target != null) targetPosition = target.position; while (elapsed < duration) { elapsed += Time.deltaTime; float t = Mathf.Clamp01(elapsed / duration); if (target != null) transform.position = Vector3.Lerp(startPosition, targetPosition, t); float scale = scaleCurve != null ? scaleCurve.Evaluate(t) : Mathf.Lerp(1f, 0f, t); transform.localScale = originalScale * Mathf.Max(0f, scale); yield return null; } FinishPickup(); } private void FinishPickup() { if (destroyAfterPickup) Destroy(gameObject); else if (disableAfterPickup) gameObject.SetActive(false); else { for (int i = 0; i < renderers.Length; i++) { if (renderers[i] != null) renderers[i].enabled = false; } } } private void PlayFeedback(InventoryManager manager, PlayerInventoryCollector collector) { AudioClip clip = pickupClipOverride != null ? pickupClipOverride : manager.GetAcquisitionClip(itemType); if (clip != null) { // 아이템 오브젝트가 곧 Destroy/Disable될 경우, 오브젝트에 붙은 AudioSource는 소리가 끊길 수 있습니다. if (destroyAfterPickup || disableAfterPickup) AudioSource.PlayClipAtPoint(clip, transform.position); else if (audioSource != null) audioSource.PlayOneShot(clip); else AudioSource.PlayClipAtPoint(clip, transform.position); } if (playCollectorHaptic && collector != null) collector.PlayHaptic(hapticAmplitude, hapticDuration); } private InventoryManager ResolveManager() { if (inventoryManager != null) return inventoryManager; if (InventoryManager.Instance != null) inventoryManager = InventoryManager.Instance; else if (autoFindManager) inventoryManager = FindFirstObjectByType(); return inventoryManager; } private void ResolvePersistentKey() { if (!persistPickupState) return; if (string.IsNullOrWhiteSpace(persistentPickupKey)) persistentPickupKey = $"Pickup_{gameObject.scene.name}_{GetHierarchyPath(transform)}_{itemType}"; } private string GetHierarchyPath(Transform target) { if (target == null) return "Unknown"; string path = target.name; Transform parent = target.parent; while (parent != null) { path = parent.name + "/" + path; parent = parent.parent; } return path; } public void ResetPickup() { pickedUp = false; if (cachedCollider != null) cachedCollider.enabled = true; transform.localScale = originalScale; for (int i = 0; i < renderers.Length; i++) { if (renderers[i] != null) renderers[i].enabled = true; } } #if UNITY_EDITOR private void OnValidate() { amount = Mathf.Max(1, amount); animationDuration = Mathf.Max(0.01f, animationDuration); hapticDuration = Mathf.Max(0f, hapticDuration); if (string.IsNullOrWhiteSpace(playerTag)) playerTag = "Player"; if (persistPickupState && string.IsNullOrWhiteSpace(persistentPickupKey)) { string editorId = GlobalObjectId.GetGlobalObjectIdSlow(this).ToString(); persistentPickupKey = string.IsNullOrWhiteSpace(editorId) ? $"Pickup_{gameObject.scene.name}_{GetHierarchyPath(transform)}_{itemType}" : $"Pickup_{editorId}_{itemType}"; } } #endif }