Files
WhaleAdventure_VR/Assets/02_Scripts/Cave/ClamBiteDetector.cs
2026-06-23 17:17:00 +09:00

527 lines
14 KiB
C#

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.XR.Interaction.Toolkit;
using UnityEngine.XR.Interaction.Toolkit.Interactables;
public class ClamBiteDetector : MonoBehaviour
{
[Header("References")]
[SerializeField] private ClamOpenClose clam;
[SerializeField] private RaftHealth health;
[SerializeField] private MemoryFragmentReset memoryFragment;
[Header("Bite Damage")]
[SerializeField] private int biteDamage = 20;
[Header("Bite Zone")]
[SerializeField] private Collider biteZoneCollider;
[Tooltip("조개 미션이 시작되었을 때만 물림 판정을 합니다.")]
[SerializeField] private bool missionActiveOnStart = false;
[Tooltip("조개가 닫히는 동안 이미 한 번 물렸으면 추가 판정을 막습니다.")]
[SerializeField] private bool biteOncePerClose = true;
[Tooltip("미션 중 BiteZone Collider를 계속 켜두어 조각이 빠져나갔는지 추적합니다.")]
[SerializeField] private bool keepBiteZoneEnabledDuringMission = true;
[Header("Fragment Rule")]
[Tooltip("기억의 조각을 잡은 뒤 BiteZone을 빠져나가지 못한 상태에서 조개가 닫히면 물림 처리합니다.")]
[SerializeField] private bool biteIfGrabbedFragmentDidNotExitBiteZone = true;
[Tooltip("기억의 조각을 잡고 있을 때만 조각 물림 판정을 합니다.")]
[SerializeField] private bool biteFragmentOnlyWhenGrabbed = true;
[Header("Hand Rule")]
[Tooltip("손 콜라이더가 BiteZone 안에 있으면 조개가 닫힐 때 물림 처리합니다.")]
[SerializeField] private bool biteHandInsideZone = true;
[Header("Target Detection")]
[SerializeField] private string handTag = "PlayerHand";
[SerializeField] private string fragmentTag = "MemoryFragment";
[Header("Clear Event")]
[Tooltip("기억의 조각을 잡은 상태로 BiteZone 밖으로 빼냈을 때 한 번만 성공 처리합니다.")]
[SerializeField] private bool clearOnce = true;
[Tooltip("성공하면 조개 물림 미션을 끕니다.")]
[SerializeField] private bool stopMissionOnClear = true;
public UnityEvent onMemoryFragmentEscaped;
[Header("Debug")]
[SerializeField] private bool showDebugLog = true;
private bool missionActive;
private bool biteWindowOpen;
private bool hasBittenThisClose;
private bool fragmentGrabbed;
private bool fragmentInsideBiteZone;
private bool fragmentGrabStartedInsideBiteZone;
private bool clearTriggered;
private XRGrabInteractable memoryGrabInteractable;
private Collider[] memoryFragmentColliders;
private readonly HashSet<Collider> collidersInside = new();
private void Awake()
{
if (biteZoneCollider == null)
biteZoneCollider = GetComponent<Collider>();
if (biteZoneCollider != null)
{
biteZoneCollider.isTrigger = true;
biteZoneCollider.enabled = false;
}
if (clam == null)
clam = GetComponentInParent<ClamOpenClose>();
if (health == null)
health = FindFirstObjectByType<RaftHealth>();
if (memoryFragment == null)
memoryFragment = FindFirstObjectByType<MemoryFragmentReset>();
if (memoryFragment != null)
{
memoryGrabInteractable = memoryFragment.GetComponent<XRGrabInteractable>();
memoryFragmentColliders = memoryFragment.GetComponentsInChildren<Collider>(true);
}
missionActive = missionActiveOnStart;
}
private void Start()
{
if (missionActive && keepBiteZoneEnabledDuringMission)
{
SetBiteZoneEnabled(true);
RefreshInitialOverlapState();
}
}
private void Update()
{
if (!missionActive || !fragmentGrabbed || !fragmentGrabStartedInsideBiteZone || clearTriggered)
return;
UpdateFragmentOverlapState();
}
private void OnEnable()
{
if (clam != null)
{
clam.onCloseStarted.AddListener(OnClamCloseStarted);
clam.onClosed.AddListener(OnClamClosed);
clam.onOpened.AddListener(OnClamOpened);
}
if (memoryGrabInteractable != null)
{
memoryGrabInteractable.selectEntered.AddListener(OnFragmentGrabbed);
memoryGrabInteractable.selectExited.AddListener(OnFragmentReleased);
}
}
private void OnDisable()
{
if (clam != null)
{
clam.onCloseStarted.RemoveListener(OnClamCloseStarted);
clam.onClosed.RemoveListener(OnClamClosed);
clam.onOpened.RemoveListener(OnClamOpened);
}
if (memoryGrabInteractable != null)
{
memoryGrabInteractable.selectEntered.RemoveListener(OnFragmentGrabbed);
memoryGrabInteractable.selectExited.RemoveListener(OnFragmentReleased);
}
}
private void OnTriggerEnter(Collider other)
{
collidersInside.Add(other);
if (IsMemoryFragmentCollider(other))
{
fragmentInsideBiteZone = true;
}
if (biteWindowOpen)
{
TryBiteByCollider(other);
}
}
private void OnTriggerStay(Collider other)
{
collidersInside.Add(other);
if (IsMemoryFragmentCollider(other))
{
fragmentInsideBiteZone = true;
}
if (biteWindowOpen)
{
TryBiteByCollider(other);
}
}
private void OnTriggerExit(Collider other)
{
collidersInside.Remove(other);
if (IsMemoryFragmentCollider(other))
{
UpdateFragmentOverlapState();
}
}
public void StartBiteMission()
{
missionActive = true;
biteWindowOpen = false;
hasBittenThisClose = false;
clearTriggered = false;
collidersInside.Clear();
SetBiteZoneEnabled(true);
RefreshInitialOverlapState();
if (showDebugLog)
Debug.Log("[ClamBiteDetector] 조개 미션 시작. BiteZone 추적 ON.", this);
}
public void StopBiteMission()
{
missionActive = false;
biteWindowOpen = false;
hasBittenThisClose = false;
fragmentGrabbed = false;
fragmentInsideBiteZone = false;
fragmentGrabStartedInsideBiteZone = false;
collidersInside.Clear();
SetBiteZoneEnabled(false);
if (showDebugLog)
Debug.Log("[ClamBiteDetector] 조개 미션 정지. BiteZone 추적 OFF.", this);
}
private void OnClamOpened()
{
hasBittenThisClose = false;
biteWindowOpen = false;
if (!missionActive)
return;
SetBiteZoneEnabled(true);
RefreshInitialOverlapState();
}
private void OnClamCloseStarted()
{
if (!missionActive)
return;
hasBittenThisClose = false;
biteWindowOpen = true;
SetBiteZoneEnabled(true);
RefreshInitialOverlapState();
if (showDebugLog)
Debug.Log("[ClamBiteDetector] 조개 닫힘 시작. 물림 판정 체크.", this);
TryBiteGrabbedFragmentIfStillInside();
TryBiteCurrentHandsInside();
}
private void OnClamClosed()
{
biteWindowOpen = false;
hasBittenThisClose = false;
collidersInside.Clear();
if (!missionActive)
return;
if (keepBiteZoneEnabledDuringMission)
{
SetBiteZoneEnabled(true);
RefreshInitialOverlapState();
}
else
{
SetBiteZoneEnabled(false);
}
if (showDebugLog)
Debug.Log("[ClamBiteDetector] 조개 닫힘 완료. 물림 판정 종료.", this);
}
private void OnFragmentGrabbed(SelectEnterEventArgs args)
{
fragmentGrabbed = true;
fragmentInsideBiteZone = IsFragmentOverlappingBiteZone();
fragmentGrabStartedInsideBiteZone = fragmentInsideBiteZone;
RefreshInitialOverlapState();
if (showDebugLog)
Debug.Log("[ClamBiteDetector] 기억의 조각을 잡았습니다.", this);
}
private void OnFragmentReleased(SelectExitEventArgs args)
{
fragmentGrabbed = false;
fragmentGrabStartedInsideBiteZone = false;
if (showDebugLog)
Debug.Log("[ClamBiteDetector] 기억의 조각을 놓았습니다.", this);
}
private void TryBiteGrabbedFragmentIfStillInside()
{
if (!biteIfGrabbedFragmentDidNotExitBiteZone)
return;
if (memoryFragment == null)
return;
if (biteOncePerClose && hasBittenThisClose)
return;
bool isSelected = IsFragmentGrabbed();
if (biteFragmentOnlyWhenGrabbed && !isSelected)
return;
bool shouldBite = isSelected && IsFragmentOverlappingBiteZone();
if (!shouldBite)
return;
BiteNow("기억의 조각을 잡은 뒤 BiteZone 밖으로 빼내지 못함");
}
private void TryBiteCurrentHandsInside()
{
if (!biteHandInsideZone)
return;
foreach (Collider col in collidersInside)
{
if (col == null)
continue;
if (IsHandCollider(col))
{
BiteNow("손이 BiteZone 안에 있음");
return;
}
}
}
private void TryBiteByCollider(Collider other)
{
if (!missionActive)
return;
if (!biteWindowOpen)
return;
if (biteOncePerClose && hasBittenThisClose)
return;
if (biteHandInsideZone && IsHandCollider(other))
{
BiteNow("손이 BiteZone 안에 있음");
return;
}
if (IsMemoryFragmentCollider(other))
{
if (memoryGrabInteractable != null && memoryGrabInteractable.isSelected)
{
BiteNow("기억의 조각이 BiteZone 안에 있음");
}
}
}
private void BiteNow(string reason)
{
if (biteOncePerClose && hasBittenThisClose)
return;
hasBittenThisClose = true;
biteWindowOpen = false;
if (health != null)
health.TakeDamage(biteDamage);
if (memoryFragment != null)
memoryFragment.ResetFragment();
fragmentGrabbed = false;
fragmentInsideBiteZone = false;
fragmentGrabStartedInsideBiteZone = false;
if (showDebugLog)
{
Debug.Log($"[ClamBiteDetector] 조개에게 물림. 이유: {reason}. 데미지 {biteDamage}, 기억의 조각 리셋", this);
}
}
private void TriggerMemoryFragmentEscaped()
{
if (clearOnce && clearTriggered)
return;
clearTriggered = true;
if (showDebugLog)
Debug.Log("[ClamBiteDetector] 기억의 조각 꺼내기 성공. 클리어 이벤트 실행.", this);
onMemoryFragmentEscaped?.Invoke();
if (stopMissionOnClear)
{
StopBiteMission();
}
}
private void RefreshInitialOverlapState()
{
if (biteZoneCollider == null || !biteZoneCollider.enabled)
return;
collidersInside.Clear();
fragmentInsideBiteZone = false;
Bounds bounds = biteZoneCollider.bounds;
Collider[] hits = Physics.OverlapBox(
bounds.center,
bounds.extents,
biteZoneCollider.transform.rotation,
~0,
QueryTriggerInteraction.Collide
);
foreach (Collider hit in hits)
{
if (hit == null)
continue;
collidersInside.Add(hit);
if (IsMemoryFragmentCollider(hit))
fragmentInsideBiteZone = true;
}
}
private void UpdateFragmentOverlapState()
{
bool wasInside = fragmentInsideBiteZone;
fragmentInsideBiteZone = IsFragmentOverlappingBiteZone();
if (!wasInside || fragmentInsideBiteZone)
return;
if (!fragmentGrabbed || !missionActive || !fragmentGrabStartedInsideBiteZone)
return;
if (showDebugLog)
Debug.Log("[ClamBiteDetector] 기억의 조각이 BiteZone 밖으로 빠져나갔습니다.", this);
TriggerMemoryFragmentEscaped();
}
private bool IsFragmentOverlappingBiteZone()
{
if (biteZoneCollider == null || memoryFragment == null)
return false;
if (memoryFragmentColliders == null || memoryFragmentColliders.Length == 0)
memoryFragmentColliders = memoryFragment.GetComponentsInChildren<Collider>(true);
foreach (Collider fragmentCollider in memoryFragmentColliders)
{
if (fragmentCollider == null || fragmentCollider == biteZoneCollider)
continue;
if (Physics.ComputePenetration(
biteZoneCollider,
biteZoneCollider.transform.position,
biteZoneCollider.transform.rotation,
fragmentCollider,
fragmentCollider.transform.position,
fragmentCollider.transform.rotation,
out _,
out _))
{
return true;
}
if (biteZoneCollider.bounds.Intersects(fragmentCollider.bounds))
{
return true;
}
}
return false;
}
private bool IsFragmentGrabbed()
{
if (memoryGrabInteractable != null)
return memoryGrabInteractable.isSelected;
return fragmentGrabbed;
}
private void SetBiteZoneEnabled(bool enabled)
{
if (biteZoneCollider == null)
return;
biteZoneCollider.enabled = enabled;
}
private bool IsMemoryFragmentCollider(Collider other)
{
if (other == null)
return false;
if (other.CompareTag(fragmentTag))
return true;
MemoryFragmentReset fragment = other.GetComponentInParent<MemoryFragmentReset>();
return fragment != null;
}
private bool IsHandCollider(Collider other)
{
if (other == null)
return false;
if (other.CompareTag(handTag))
return true;
XRHandMarker marker = other.GetComponentInParent<XRHandMarker>();
return marker != null;
}
}