349 lines
12 KiB
C#
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
|
|
}
|