문달기

This commit is contained in:
2026-06-23 17:17:00 +09:00
parent d094fb19ea
commit ee83d7bc20
31 changed files with 1894 additions and 90 deletions

View File

@@ -1,5 +1,7 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.XR.Interaction.Toolkit;
using UnityEngine.XR.Interaction.Toolkit.Interactables;
public class ClamBiteDetector : MonoBehaviour
@@ -21,22 +23,48 @@ public class ClamBiteDetector : MonoBehaviour
[Tooltip("조개가 닫히는 동안 이미 한 번 물렸으면 추가 판정을 막습니다.")]
[SerializeField] private bool biteOncePerClose = true;
[Header("Target Detection")]
[Tooltip("손 오브젝트에 붙일 태그입니다. 태그를 안 쓰면 XRHandMarker로도 판정합니다.")]
[SerializeField] private string handTag = "PlayerHand";
[Tooltip("미션 중 BiteZone Collider를 계속 켜두어 조각이 빠져나갔는지 추적합니다.")]
[SerializeField] private bool keepBiteZoneEnabledDuringMission = true;
[Tooltip("기억의 조각 태그입니다. 단, 조각은 잡힌 상태일 때만 물림 대상으로 봅니다.")]
[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";
[Tooltip("기억의 조각은 플레이어가 잡고 있을 때만 물림 판정합니다.")]
[SerializeField] private bool biteFragmentOnlyWhenGrabbed = true;
[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()
@@ -59,16 +87,45 @@ private void Awake()
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(EnableBiteWindow);
clam.onClosed.AddListener(DisableBiteWindow);
clam.onOpened.AddListener(ResetBiteState);
clam.onCloseStarted.AddListener(OnClamCloseStarted);
clam.onClosed.AddListener(OnClamClosed);
clam.onOpened.AddListener(OnClamOpened);
}
if (memoryGrabInteractable != null)
{
memoryGrabInteractable.selectEntered.AddListener(OnFragmentGrabbed);
memoryGrabInteractable.selectExited.AddListener(OnFragmentReleased);
}
}
@@ -76,9 +133,15 @@ private void OnDisable()
{
if (clam != null)
{
clam.onCloseStarted.RemoveListener(EnableBiteWindow);
clam.onClosed.RemoveListener(DisableBiteWindow);
clam.onOpened.RemoveListener(ResetBiteState);
clam.onCloseStarted.RemoveListener(OnClamCloseStarted);
clam.onClosed.RemoveListener(OnClamClosed);
clam.onOpened.RemoveListener(OnClamOpened);
}
if (memoryGrabInteractable != null)
{
memoryGrabInteractable.selectEntered.RemoveListener(OnFragmentGrabbed);
memoryGrabInteractable.selectExited.RemoveListener(OnFragmentReleased);
}
}
@@ -86,111 +149,225 @@ private void OnTriggerEnter(Collider other)
{
collidersInside.Add(other);
if (IsBiteWindowOpen())
TryBite(other);
if (IsMemoryFragmentCollider(other))
{
fragmentInsideBiteZone = true;
}
if (biteWindowOpen)
{
TryBiteByCollider(other);
}
}
private void OnTriggerStay(Collider other)
{
collidersInside.Add(other);
if (IsBiteWindowOpen())
TryBite(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] 조개 미션 시작. 물림 판정 활성 준비.", this);
Debug.Log("[ClamBiteDetector] 조개 미션 시작. BiteZone 추적 ON.", this);
}
public void StopBiteMission()
{
missionActive = false;
biteWindowOpen = false;
hasBittenThisClose = false;
fragmentGrabbed = false;
fragmentInsideBiteZone = false;
fragmentGrabStartedInsideBiteZone = false;
collidersInside.Clear();
if (biteZoneCollider != null)
biteZoneCollider.enabled = false;
SetBiteZoneEnabled(false);
if (showDebugLog)
Debug.Log("[ClamBiteDetector] 조개 미션 정지. 물림 판정 비활성.", this);
Debug.Log("[ClamBiteDetector] 조개 미션 정지. BiteZone 추적 OFF.", this);
}
private void EnableBiteWindow()
private void OnClamOpened()
{
hasBittenThisClose = false;
biteWindowOpen = false;
if (!missionActive)
return;
SetBiteZoneEnabled(true);
RefreshInitialOverlapState();
}
private void OnClamCloseStarted()
{
if (!missionActive)
return;
hasBittenThisClose = false;
biteWindowOpen = true;
if (biteZoneCollider != null)
biteZoneCollider.enabled = true;
SetBiteZoneEnabled(true);
RefreshInitialOverlapState();
if (showDebugLog)
Debug.Log("[ClamBiteDetector] 조개 물림 판정 ON", this);
Debug.Log("[ClamBiteDetector] 조개 닫힘 시작. 물림 판정 체크.", this);
foreach (Collider col in collidersInside)
TryBiteGrabbedFragmentIfStillInside();
TryBiteCurrentHandsInside();
}
private void OnClamClosed()
{
biteWindowOpen = false;
hasBittenThisClose = false;
collidersInside.Clear();
if (!missionActive)
return;
if (keepBiteZoneEnabledDuringMission)
{
if (col != null)
TryBite(col);
SetBiteZoneEnabled(true);
RefreshInitialOverlapState();
}
else
{
SetBiteZoneEnabled(false);
}
if (showDebugLog)
Debug.Log("[ClamBiteDetector] 조개 닫힘 완료. 물림 판정 종료.", this);
}
private void DisableBiteWindow()
private void OnFragmentGrabbed(SelectEnterEventArgs args)
{
if (biteZoneCollider != null)
biteZoneCollider.enabled = false;
fragmentGrabbed = true;
fragmentInsideBiteZone = IsFragmentOverlappingBiteZone();
fragmentGrabStartedInsideBiteZone = fragmentInsideBiteZone;
collidersInside.Clear();
RefreshInitialOverlapState();
if (showDebugLog && missionActive)
Debug.Log("[ClamBiteDetector] 조개 물림 판정 OFF", this);
if (showDebugLog)
Debug.Log("[ClamBiteDetector] 기억의 조각을 잡았습니다.", this);
}
private void ResetBiteState()
private void OnFragmentReleased(SelectExitEventArgs args)
{
hasBittenThisClose = false;
fragmentGrabbed = false;
fragmentGrabStartedInsideBiteZone = false;
if (showDebugLog)
Debug.Log("[ClamBiteDetector] 기억의 조각을 놓았습니다.", this);
}
private bool IsBiteWindowOpen()
private void TryBiteGrabbedFragmentIfStillInside()
{
if (!missionActive)
return false;
if (biteZoneCollider == null)
return false;
return biteZoneCollider.enabled;
}
private void TryBite(Collider other)
{
if (other == null)
if (!biteIfGrabbedFragmentDidNotExitBiteZone)
return;
if (!missionActive)
if (memoryFragment == null)
return;
if (biteOncePerClose && hasBittenThisClose)
return;
bool isHand = IsHandCollider(other);
bool isGrabbedFragment = IsGrabbedMemoryFragment(other);
bool isSelected = IsFragmentGrabbed();
if (!isHand && !isGrabbedFragment)
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);
@@ -198,45 +375,153 @@ private void TryBite(Collider other)
if (memoryFragment != null)
memoryFragment.ResetFragment();
fragmentGrabbed = false;
fragmentInsideBiteZone = false;
fragmentGrabStartedInsideBiteZone = false;
if (showDebugLog)
{
Debug.Log($"[ClamBiteDetector] 조개에게 물림. 데미지 {biteDamage}, 기억의 조각 리셋", this);
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;
}
private bool IsGrabbedMemoryFragment(Collider other)
{
MemoryFragmentReset fragment = other.GetComponentInParent<MemoryFragmentReset>();
if (fragment == null)
{
if (!other.CompareTag(fragmentTag))
return false;
fragment = memoryFragment;
}
if (fragment == null)
return false;
XRGrabInteractable grab = fragment.GetComponent<XRGrabInteractable>();
if (grab == null)
return !biteFragmentOnlyWhenGrabbed;
if (biteFragmentOnlyWhenGrabbed)
return grab.isSelected;
return true;
}
}

View File

@@ -0,0 +1,167 @@
using System.Collections;
using UnityEngine;
public class FallingStalactite : MonoBehaviour
{
[Header("References")]
[SerializeField] private Rigidbody rb;
[SerializeField] private Collider damageCollider;
[SerializeField] private DamageObstacle damageObstacle;
[Header("Fall Settings")]
[SerializeField] private float fallDelay = 0.0f;
[Tooltip("떨어질 때 아래 방향으로 추가 속도를 줍니다.")]
[SerializeField] private float initialDownVelocity = 0f;
[Tooltip("떨어질 때 약간 회전시키고 싶으면 값을 넣습니다.")]
[SerializeField] private Vector3 initialAngularVelocity = new Vector3(0f, 0f, 0f);
[Header("Damage")]
[SerializeField] private int damage = 10;
[Tooltip("떨어지기 전에는 데미지를 끄고, 떨어질 때 켭니다.")]
[SerializeField] private bool damageOnlyWhileFalling = true;
[Header("Reset Option")]
[SerializeField] private bool resetAfterFall = true;
[Tooltip("떨어진 뒤 몇 초 후 원래 위치로 돌아갈지 설정합니다.")]
[SerializeField] private float resetDelay = 4.0f;
[Tooltip("리셋할 때 종유석을 다시 숨기지 않고 원위치에 고정합니다.")]
[SerializeField] private bool readyAgainAfterReset = true;
[Header("Debug")]
[SerializeField] private bool showDebugLog = true;
private Vector3 startPosition;
private Quaternion startRotation;
private bool hasFallen;
private Coroutine fallRoutine;
public bool HasFallen => hasFallen;
private void Awake()
{
ResolveReferences();
startPosition = transform.position;
startRotation = transform.rotation;
PrepareStalactite();
}
private void ResolveReferences()
{
if (rb == null)
rb = GetComponent<Rigidbody>();
if (damageCollider == null)
damageCollider = GetComponent<Collider>();
if (damageObstacle == null)
damageObstacle = GetComponent<DamageObstacle>();
}
private void PrepareStalactite()
{
if (rb != null)
{
rb.useGravity = false;
rb.isKinematic = true;
rb.linearVelocity = Vector3.zero;
rb.angularVelocity = Vector3.zero;
}
if (damageObstacle != null)
{
damageObstacle.SetDamage(damage);
damageObstacle.SetCanDamage(!damageOnlyWhileFalling);
}
if (damageCollider != null)
{
damageCollider.enabled = true;
}
hasFallen = false;
}
public void TriggerFall()
{
if (hasFallen)
return;
if (fallRoutine != null)
StopCoroutine(fallRoutine);
fallRoutine = StartCoroutine(FallRoutine());
}
private IEnumerator FallRoutine()
{
hasFallen = true;
if (fallDelay > 0f)
yield return new WaitForSeconds(fallDelay);
if (damageObstacle != null)
{
damageObstacle.SetDamage(damage);
damageObstacle.SetCanDamage(true);
}
if (rb != null)
{
rb.isKinematic = false;
rb.useGravity = true;
if (initialDownVelocity > 0f)
{
rb.linearVelocity = Vector3.down * initialDownVelocity;
}
rb.angularVelocity = initialAngularVelocity;
}
if (showDebugLog)
Debug.Log($"[FallingStalactite] {name} 낙하 시작. 데미지 {damage}", this);
if (resetAfterFall)
{
yield return new WaitForSeconds(resetDelay);
ResetStalactite();
}
fallRoutine = null;
}
public void ResetStalactite()
{
if (rb != null)
{
rb.linearVelocity = Vector3.zero;
rb.angularVelocity = Vector3.zero;
rb.useGravity = false;
rb.isKinematic = true;
}
transform.position = startPosition;
transform.rotation = startRotation;
if (damageObstacle != null)
{
damageObstacle.SetDamage(damage);
damageObstacle.SetCanDamage(!damageOnlyWhileFalling);
}
if (readyAgainAfterReset)
{
hasFallen = false;
}
if (showDebugLog)
Debug.Log($"[FallingStalactite] {name} 원위치 리셋", this);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ba858a5e5caeb144d91cb3020852ad0f

View File

@@ -18,12 +18,41 @@ public class RaftRideEndHandler : MonoBehaviour
[Tooltip("도착 후 활성화할 조개 미션 오브젝트")]
[SerializeField] private GameObject clamMissionObject;
[Tooltip("처음에는 꺼두었다가 나중에 기억의 조각 획득 후 켤 ")]
[Tooltip("기억의 조각 획득 후 켤 다음 문 오브젝트")]
[SerializeField] private GameObject nextDoorObject;
[Header("Clam Bite Mission")]
[Tooltip("도착 후 조개 물림 판정을 시작할지 여부")]
[SerializeField] private bool startClamBiteMissionOnArrive = true;
[Tooltip("직접 연결할 조개 물림 판정 스크립트들입니다. 비워두면 Clam Mission Object 아래에서 자동 탐색합니다.")]
[SerializeField] private ClamBiteDetector[] clamBiteDetectors;
[Header("Clear Options")]
[Tooltip("조개 미션 성공 시 조개 물림 판정을 정지합니다.")]
[SerializeField] private bool stopClamBiteMissionOnClear = true;
[Tooltip("조개 미션 성공 시 조개 오브젝트를 계속 유지할지 여부입니다.")]
[SerializeField] private bool keepClamMissionObjectAfterClear = true;
[Tooltip("조개 미션 성공 시 Next Door Object를 켭니다.")]
[SerializeField] private bool activateNextDoorOnClear = true;
[Header("Debug")]
[SerializeField] private bool showDebugLog = true;
private bool hasArrived;
private bool clamMissionCleared;
public void OnRaftArrived()
{
Debug.Log("[RaftRideEndHandler] 뗏목 도착 처리 시작.");
if (hasArrived)
return;
hasArrived = true;
if (showDebugLog)
Debug.Log("[RaftRideEndHandler] 뗏목 도착 처리 시작.");
if (detachPlayerFromRaft && xrOrigin != null)
{
@@ -32,7 +61,8 @@ public void OnRaftArrived()
if (exitPoint != null && xrOrigin != null)
{
// 강제 이동시키고 싶지 않으면 이 부분은 주석 처리해도 됨.
// 현재는 강제 이동하지 않음.
// 필요하면 아래 두 줄을 활성화.
// xrOrigin.position = exitPoint.position;
// xrOrigin.rotation = exitPoint.rotation;
}
@@ -52,6 +82,126 @@ public void OnRaftArrived()
nextDoorObject.SetActive(false);
}
Debug.Log("[RaftRideEndHandler] 이제 플레이어가 육지로 이동해 조개 미션을 진행할 수 있습니다.");
if (startClamBiteMissionOnArrive)
{
StartClamBiteMission();
}
if (showDebugLog)
Debug.Log("[RaftRideEndHandler] 도착 처리 완료. 육지/조개 미션 진행 가능.");
}
public void OnClamMissionCleared()
{
if (clamMissionCleared)
return;
clamMissionCleared = true;
if (showDebugLog)
Debug.Log("[RaftRideEndHandler] 조개 미션 클리어. 다음 문을 엽니다.");
if (stopClamBiteMissionOnClear)
{
StopClamBiteMission();
}
if (clamMissionObject != null && !keepClamMissionObjectAfterClear)
{
clamMissionObject.SetActive(false);
}
if (activateNextDoorOnClear && nextDoorObject != null)
{
nextDoorObject.SetActive(true);
}
else if (activateNextDoorOnClear && nextDoorObject == null)
{
Debug.LogWarning("[RaftRideEndHandler] Next Door Object가 연결되지 않았습니다.", this);
}
}
public void OpenNextDoor()
{
if (nextDoorObject == null)
{
Debug.LogWarning("[RaftRideEndHandler] Next Door Object가 연결되지 않았습니다.", this);
return;
}
nextDoorObject.SetActive(true);
if (showDebugLog)
Debug.Log("[RaftRideEndHandler] Next Door Object 활성화.");
}
public void CloseNextDoor()
{
if (nextDoorObject == null)
return;
nextDoorObject.SetActive(false);
if (showDebugLog)
Debug.Log("[RaftRideEndHandler] Next Door Object 비활성화.");
}
private void StartClamBiteMission()
{
ResolveClamBiteDetectors();
if (clamBiteDetectors == null || clamBiteDetectors.Length == 0)
{
if (showDebugLog)
Debug.LogWarning("[RaftRideEndHandler] ClamBiteDetector를 찾지 못했습니다.", this);
return;
}
foreach (ClamBiteDetector detector in clamBiteDetectors)
{
if (detector == null)
continue;
detector.StartBiteMission();
}
if (showDebugLog)
Debug.Log("[RaftRideEndHandler] 조개 물림 미션 시작.");
}
private void StopClamBiteMission()
{
ResolveClamBiteDetectors();
if (clamBiteDetectors == null || clamBiteDetectors.Length == 0)
return;
foreach (ClamBiteDetector detector in clamBiteDetectors)
{
if (detector == null)
continue;
detector.StopBiteMission();
}
if (showDebugLog)
Debug.Log("[RaftRideEndHandler] 조개 물림 미션 정지.");
}
private void ResolveClamBiteDetectors()
{
if (clamBiteDetectors != null && clamBiteDetectors.Length > 0)
return;
if (clamMissionObject != null)
{
clamBiteDetectors = clamMissionObject.GetComponentsInChildren<ClamBiteDetector>(true);
}
if (clamBiteDetectors == null || clamBiteDetectors.Length == 0)
{
clamBiteDetectors = FindObjectsByType<ClamBiteDetector>(FindObjectsSortMode.None);
}
}
}

View File

@@ -0,0 +1,143 @@
using System.Collections;
using UnityEngine;
public class StalactiteFallTrigger : MonoBehaviour
{
[Header("References")]
[SerializeField] private FallingStalactite[] stalactites;
[Header("Trigger Settings")]
[SerializeField] private bool triggerOnce = true;
[Tooltip("RaftDamageReceiver가 붙은 RaftHitBox가 들어왔을 때만 작동합니다.")]
[SerializeField] private bool requireRaftDamageReceiver = true;
[Tooltip("특정 태그가 들어왔을 때도 작동시키고 싶으면 입력합니다. 비워두면 태그 검사를 하지 않습니다.")]
[SerializeField] private string targetTag = "";
[Tooltip("트리거 후 종유석이 떨어지기까지의 추가 지연 시간입니다.")]
[SerializeField] private float triggerDelay = 0f;
[Tooltip("여러 종유석을 순차적으로 떨어뜨릴 때 간격입니다.")]
[SerializeField] private float intervalBetweenStalactites = 0.15f;
[Header("Debug")]
[SerializeField] private bool showDebugLog = true;
private bool hasTriggered;
private Coroutine triggerRoutine;
private void Reset()
{
Collider col = GetComponent<Collider>();
if (col != null)
col.isTrigger = true;
}
private void Awake()
{
Collider col = GetComponent<Collider>();
if (col != null)
col.isTrigger = true;
if (stalactites == null || stalactites.Length == 0)
{
stalactites = GetComponentsInChildren<FallingStalactite>(true);
}
}
private void OnTriggerEnter(Collider other)
{
TryTrigger(other);
}
private void TryTrigger(Collider other)
{
if (triggerOnce && hasTriggered)
return;
if (!IsValidTarget(other))
return;
hasTriggered = true;
if (triggerRoutine != null)
StopCoroutine(triggerRoutine);
triggerRoutine = StartCoroutine(TriggerRoutine());
if (showDebugLog)
Debug.Log($"[StalactiteFallTrigger] {name} 작동. 감지 대상: {other.name}", this);
}
private bool IsValidTarget(Collider other)
{
if (other == null)
return false;
bool valid = false;
if (requireRaftDamageReceiver)
{
RaftDamageReceiver receiver = other.GetComponentInParent<RaftDamageReceiver>();
if (receiver != null)
valid = true;
}
if (!string.IsNullOrEmpty(targetTag))
{
if (other.CompareTag(targetTag))
valid = true;
}
if (!requireRaftDamageReceiver && string.IsNullOrEmpty(targetTag))
{
valid = true;
}
return valid;
}
private IEnumerator TriggerRoutine()
{
if (triggerDelay > 0f)
yield return new WaitForSeconds(triggerDelay);
if (stalactites == null || stalactites.Length == 0)
{
if (showDebugLog)
Debug.LogWarning("[StalactiteFallTrigger] 연결된 FallingStalactite가 없습니다.", this);
yield break;
}
foreach (FallingStalactite stalactite in stalactites)
{
if (stalactite == null)
continue;
stalactite.TriggerFall();
if (intervalBetweenStalactites > 0f)
yield return new WaitForSeconds(intervalBetweenStalactites);
}
triggerRoutine = null;
}
public void ResetTrigger()
{
hasTriggered = false;
if (triggerRoutine != null)
{
StopCoroutine(triggerRoutine);
triggerRoutine = null;
}
if (showDebugLog)
Debug.Log($"[StalactiteFallTrigger] {name} 리셋", this);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8d76523d73ab7184fa82d121e34448bd