육지도착

This commit is contained in:
2026-06-22 16:58:32 +09:00
parent f7a71e11d6
commit 74afff5be8
23 changed files with 1500 additions and 54 deletions

View 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);
}
}