2026.06.24
This commit is contained in:
@@ -1,11 +1,18 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// 월드에 떠 있는 아이템 오브젝트를 직접 주울 때 사용합니다.
|
||||
/// Collider의 Is Trigger를 켜고, Player 태그와 충돌하면 인벤토리에 추가됩니다.
|
||||
/// VR에서는 Player 태그만 쓰기보다 PlayerInventoryCollector 또는 LayerMask를 함께 쓰는 것을 권장합니다.
|
||||
/// 기능: 중복 획득 저장, 획득 연출, 사운드, 컨트롤러 햅틱, 최대 소지 수량 차단.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
[RequireComponent(typeof(Collider))]
|
||||
public class ItemPickup : MonoBehaviour
|
||||
{
|
||||
[Header("Reference")]
|
||||
@@ -15,24 +22,83 @@ public class ItemPickup : MonoBehaviour
|
||||
[SerializeField] private InventoryItemType itemType = InventoryItemType.Fish;
|
||||
[SerializeField] private int amount = 1;
|
||||
|
||||
[Header("Pickup Settings")]
|
||||
[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()
|
||||
{
|
||||
if (inventoryManager == null && autoFindManager)
|
||||
inventoryManager = FindFirstObjectByType<InventoryManager>();
|
||||
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)
|
||||
@@ -40,41 +106,243 @@ private void OnTriggerEnter(Collider other)
|
||||
if (pickedUp)
|
||||
return;
|
||||
|
||||
if (!other.CompareTag(playerTag))
|
||||
PlayerInventoryCollector collector = other.GetComponentInParent<PlayerInventoryCollector>();
|
||||
|
||||
if (!CanBePickedUpBy(other, collector))
|
||||
return;
|
||||
|
||||
PickUp();
|
||||
PickUp(collector);
|
||||
}
|
||||
|
||||
public void PickUp()
|
||||
{
|
||||
PickUp(null);
|
||||
}
|
||||
|
||||
public void PickUp(PlayerInventoryCollector collector)
|
||||
{
|
||||
if (pickedUp)
|
||||
return;
|
||||
|
||||
pickedUp = true;
|
||||
|
||||
if (inventoryManager == null)
|
||||
InventoryManager manager = ResolveManager();
|
||||
if (collector != null && collector.InventoryManagerOverride != null)
|
||||
manager = collector.InventoryManagerOverride;
|
||||
|
||||
if (manager == null)
|
||||
{
|
||||
Debug.LogWarning("[ItemPickup] InventoryManager가 연결되지 않았습니다.");
|
||||
Debug.LogWarning("[ItemPickup] InventoryManager가 연결되지 않았습니다.", this);
|
||||
pickedUp = false;
|
||||
onPickupBlocked?.Invoke();
|
||||
return;
|
||||
}
|
||||
|
||||
int addAmount = Mathf.Max(1, amount);
|
||||
inventoryManager.AddItem(itemType, addAmount);
|
||||
onPickedUp?.Invoke();
|
||||
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} +{addAmount}");
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user