육지도착
This commit is contained in:
310
Assets/02_Scripts/Cave/RhinoObstacle.cs
Normal file
310
Assets/02_Scripts/Cave/RhinoObstacle.cs
Normal file
@@ -0,0 +1,310 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
public class RhinoObstacle : MonoBehaviour
|
||||
{
|
||||
[Header("References")]
|
||||
[SerializeField] private Transform rhinoRoot;
|
||||
[SerializeField] private Animator animator;
|
||||
[SerializeField] private DamageObstacle damageObstacle;
|
||||
|
||||
[Tooltip("수면 위에 있을 때만 켤 코뿔소 충돌 콜라이더들입니다. RhinoHitBox의 Collider를 넣으세요.")]
|
||||
[SerializeField] private Collider[] damageColliders;
|
||||
|
||||
[Header("Position")]
|
||||
[SerializeField] private Transform underwaterPoint;
|
||||
[SerializeField] private Transform surfacePoint;
|
||||
|
||||
[Header("Timing")]
|
||||
[SerializeField] private float minHiddenTime = 2.0f;
|
||||
[SerializeField] private float maxHiddenTime = 5.0f;
|
||||
|
||||
[SerializeField] private float riseDuration = 0.8f;
|
||||
[SerializeField] private float surfaceIdleTime = 0.4f;
|
||||
[SerializeField] private float attackStayTime = 1.2f;
|
||||
[SerializeField] private float diveDuration = 0.7f;
|
||||
|
||||
[Header("Animation")]
|
||||
[SerializeField] private string idleStateName = "Idle";
|
||||
[SerializeField] private string hitTriggerName = "Hit";
|
||||
[SerializeField] private float idleCrossFadeDuration = 0.1f;
|
||||
|
||||
[Header("Options")]
|
||||
[SerializeField] private bool startAutomatically = true;
|
||||
[SerializeField] private bool loop = true;
|
||||
[SerializeField] private bool showDebugLog = true;
|
||||
|
||||
private Coroutine routine;
|
||||
private bool isRunning;
|
||||
private bool isSurfaced;
|
||||
|
||||
public bool IsSurfaced => isSurfaced;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
ResolveReferences();
|
||||
|
||||
if (animator != null)
|
||||
{
|
||||
animator.applyRootMotion = false;
|
||||
}
|
||||
|
||||
SetDamageActive(false);
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
MoveImmediatelyToUnderwater();
|
||||
ForceIdleAnimation();
|
||||
|
||||
if (startAutomatically)
|
||||
{
|
||||
StartRhino();
|
||||
}
|
||||
}
|
||||
|
||||
private void ResolveReferences()
|
||||
{
|
||||
if (rhinoRoot == null)
|
||||
rhinoRoot = transform;
|
||||
|
||||
if (animator == null)
|
||||
animator = GetComponentInChildren<Animator>();
|
||||
|
||||
if (damageObstacle == null)
|
||||
damageObstacle = GetComponentInChildren<DamageObstacle>();
|
||||
|
||||
if (damageColliders == null || damageColliders.Length == 0)
|
||||
{
|
||||
damageColliders = GetComponentsInChildren<Collider>(true);
|
||||
}
|
||||
}
|
||||
|
||||
public void StartRhino()
|
||||
{
|
||||
if (routine != null)
|
||||
{
|
||||
StopCoroutine(routine);
|
||||
}
|
||||
|
||||
isRunning = true;
|
||||
routine = StartCoroutine(RhinoRoutine());
|
||||
|
||||
Log("시작");
|
||||
}
|
||||
|
||||
public void StopRhino()
|
||||
{
|
||||
isRunning = false;
|
||||
|
||||
if (routine != null)
|
||||
{
|
||||
StopCoroutine(routine);
|
||||
routine = null;
|
||||
}
|
||||
|
||||
SetDamageActive(false);
|
||||
ForceIdleAnimation();
|
||||
MoveImmediatelyToUnderwater();
|
||||
|
||||
Log("정지");
|
||||
}
|
||||
|
||||
private IEnumerator RhinoRoutine()
|
||||
{
|
||||
while (isRunning)
|
||||
{
|
||||
// 1. 물속 대기
|
||||
isSurfaced = false;
|
||||
SetDamageActive(false);
|
||||
ForceIdleAnimation();
|
||||
|
||||
float hiddenWait = Random.Range(minHiddenTime, maxHiddenTime);
|
||||
Log($"물속 대기 {hiddenWait:0.0}초");
|
||||
|
||||
yield return new WaitForSeconds(hiddenWait);
|
||||
|
||||
if (!isRunning)
|
||||
break;
|
||||
|
||||
// 2. 수면 위로 떠오름
|
||||
Log("떠오름 시작");
|
||||
yield return MoveToSurface();
|
||||
|
||||
if (!isRunning)
|
||||
break;
|
||||
|
||||
// 3. 수면 위에 도착한 순간부터 충돌 가능
|
||||
isSurfaced = true;
|
||||
SetDamageActive(true);
|
||||
ForceIdleAnimation();
|
||||
|
||||
Log("수면 위 도착 / 데미지 콜라이더 ON");
|
||||
|
||||
yield return new WaitForSeconds(surfaceIdleTime);
|
||||
|
||||
if (!isRunning)
|
||||
break;
|
||||
|
||||
// 4. 공격 실행
|
||||
Log("공격 시작");
|
||||
PlayHitAnimation();
|
||||
|
||||
yield return new WaitForSeconds(attackStayTime);
|
||||
|
||||
// 5. 공격 종료 후 Idle 복귀
|
||||
Log("공격 종료 / Idle 복귀");
|
||||
ForceIdleAnimation();
|
||||
|
||||
if (!isRunning)
|
||||
break;
|
||||
|
||||
// 6. 잠수 시작 전 충돌 끄기
|
||||
isSurfaced = false;
|
||||
SetDamageActive(false);
|
||||
|
||||
Log("잠수 시작 / 데미지 콜라이더 OFF");
|
||||
yield return MoveToUnderwater();
|
||||
|
||||
if (!loop)
|
||||
break;
|
||||
}
|
||||
|
||||
routine = null;
|
||||
}
|
||||
|
||||
private IEnumerator MoveToSurface()
|
||||
{
|
||||
if (rhinoRoot == null || surfacePoint == null)
|
||||
yield break;
|
||||
|
||||
Vector3 startPos = rhinoRoot.position;
|
||||
Vector3 endPos = surfacePoint.position;
|
||||
|
||||
float timer = 0f;
|
||||
float duration = Mathf.Max(0.01f, riseDuration);
|
||||
|
||||
while (timer < duration)
|
||||
{
|
||||
timer += Time.deltaTime;
|
||||
|
||||
float t = Mathf.Clamp01(timer / duration);
|
||||
float smoothT = Smooth01(t);
|
||||
|
||||
rhinoRoot.position = Vector3.Lerp(startPos, endPos, smoothT);
|
||||
|
||||
yield return null;
|
||||
}
|
||||
|
||||
rhinoRoot.position = endPos;
|
||||
}
|
||||
|
||||
private IEnumerator MoveToUnderwater()
|
||||
{
|
||||
if (rhinoRoot == null || underwaterPoint == null)
|
||||
yield break;
|
||||
|
||||
Vector3 startPos = rhinoRoot.position;
|
||||
Vector3 endPos = underwaterPoint.position;
|
||||
|
||||
float timer = 0f;
|
||||
float duration = Mathf.Max(0.01f, diveDuration);
|
||||
|
||||
while (timer < duration)
|
||||
{
|
||||
timer += Time.deltaTime;
|
||||
|
||||
float t = Mathf.Clamp01(timer / duration);
|
||||
float smoothT = Smooth01(t);
|
||||
|
||||
rhinoRoot.position = Vector3.Lerp(startPos, endPos, smoothT);
|
||||
|
||||
yield return null;
|
||||
}
|
||||
|
||||
rhinoRoot.position = endPos;
|
||||
|
||||
isSurfaced = false;
|
||||
SetDamageActive(false);
|
||||
ForceIdleAnimation();
|
||||
|
||||
Log("잠수 완료");
|
||||
}
|
||||
|
||||
private void PlayHitAnimation()
|
||||
{
|
||||
if (animator == null)
|
||||
{
|
||||
LogWarning("Animator가 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(hitTriggerName))
|
||||
return;
|
||||
|
||||
animator.ResetTrigger(hitTriggerName);
|
||||
animator.SetTrigger(hitTriggerName);
|
||||
}
|
||||
|
||||
private void ForceIdleAnimation()
|
||||
{
|
||||
if (animator == null)
|
||||
return;
|
||||
|
||||
if (!string.IsNullOrEmpty(hitTriggerName))
|
||||
{
|
||||
animator.ResetTrigger(hitTriggerName);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(idleStateName))
|
||||
{
|
||||
animator.CrossFade(idleStateName, idleCrossFadeDuration, 0, 0f);
|
||||
}
|
||||
}
|
||||
|
||||
private void MoveImmediatelyToUnderwater()
|
||||
{
|
||||
if (rhinoRoot == null || underwaterPoint == null)
|
||||
return;
|
||||
|
||||
rhinoRoot.position = underwaterPoint.position;
|
||||
isSurfaced = false;
|
||||
SetDamageActive(false);
|
||||
}
|
||||
|
||||
private void SetDamageActive(bool active)
|
||||
{
|
||||
if (damageObstacle != null)
|
||||
{
|
||||
damageObstacle.SetCanDamage(active);
|
||||
}
|
||||
|
||||
if (damageColliders == null)
|
||||
return;
|
||||
|
||||
foreach (Collider col in damageColliders)
|
||||
{
|
||||
if (col == null)
|
||||
continue;
|
||||
|
||||
col.enabled = active;
|
||||
}
|
||||
}
|
||||
|
||||
private float Smooth01(float t)
|
||||
{
|
||||
return t * t * (3f - 2f * t);
|
||||
}
|
||||
|
||||
private void Log(string message)
|
||||
{
|
||||
if (showDebugLog)
|
||||
Debug.Log($"[RhinoObstacle] {name} / {message}");
|
||||
}
|
||||
|
||||
private void LogWarning(string message)
|
||||
{
|
||||
if (showDebugLog)
|
||||
Debug.LogWarning($"[RhinoObstacle] {name} / {message}", this);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user