Files
WhaleAdventure_VR/Assets/My project/Inventory/Scripts/ItemPickup.cs
2026-06-24 17:13:40 +09:00

349 lines
12 KiB
C#

using System.Collections;
using UnityEngine;
using UnityEngine.Events;
#if UNITY_EDITOR
using UnityEditor;
#endif
/// <summary>
/// 월드에 떠 있는 아이템 오브젝트를 직접 주울 때 사용합니다.
/// VR에서는 Player 태그만 쓰기보다 PlayerInventoryCollector 또는 LayerMask를 함께 쓰는 것을 권장합니다.
/// 기능: 중복 획득 저장, 획득 연출, 사운드, 컨트롤러 햅틱, 최대 소지 수량 차단.
/// </summary>
[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<Collider>();
renderers = GetComponentsInChildren<Renderer>();
originalScale = transform.localScale;
if (audioSource == null)
audioSource = GetComponent<AudioSource>();
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<PlayerInventoryCollector>();
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<InventoryManager>();
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
}