육지도착
This commit is contained in:
Binary file not shown.
160
Assets/02_Scripts/Cave/ClamBiteDetector.cs
Normal file
160
Assets/02_Scripts/Cave/ClamBiteDetector.cs
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
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 biteOncePerClose = true;
|
||||||
|
|
||||||
|
[Header("Target Tags")]
|
||||||
|
[SerializeField] private string handTag = "PlayerHand";
|
||||||
|
[SerializeField] private string fragmentTag = "MemoryFragment";
|
||||||
|
|
||||||
|
[Header("Debug")]
|
||||||
|
[SerializeField] private bool showDebugLog = true;
|
||||||
|
|
||||||
|
private bool hasBittenThisClose;
|
||||||
|
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>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnEnable()
|
||||||
|
{
|
||||||
|
if (clam != null)
|
||||||
|
{
|
||||||
|
clam.onCloseStarted.AddListener(EnableBiteWindow);
|
||||||
|
clam.onClosed.AddListener(DisableBiteWindow);
|
||||||
|
clam.onOpened.AddListener(ResetBiteState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDisable()
|
||||||
|
{
|
||||||
|
if (clam != null)
|
||||||
|
{
|
||||||
|
clam.onCloseStarted.RemoveListener(EnableBiteWindow);
|
||||||
|
clam.onClosed.RemoveListener(DisableBiteWindow);
|
||||||
|
clam.onOpened.RemoveListener(ResetBiteState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTriggerEnter(Collider other)
|
||||||
|
{
|
||||||
|
collidersInside.Add(other);
|
||||||
|
|
||||||
|
if (IsBiteWindowOpen())
|
||||||
|
TryBite(other);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTriggerStay(Collider other)
|
||||||
|
{
|
||||||
|
collidersInside.Add(other);
|
||||||
|
|
||||||
|
if (IsBiteWindowOpen())
|
||||||
|
TryBite(other);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTriggerExit(Collider other)
|
||||||
|
{
|
||||||
|
collidersInside.Remove(other);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnableBiteWindow()
|
||||||
|
{
|
||||||
|
hasBittenThisClose = false;
|
||||||
|
|
||||||
|
if (biteZoneCollider != null)
|
||||||
|
biteZoneCollider.enabled = true;
|
||||||
|
|
||||||
|
if (showDebugLog)
|
||||||
|
Debug.Log("[ClamBiteDetector] 조개 물림 판정 ON", this);
|
||||||
|
|
||||||
|
foreach (Collider col in collidersInside)
|
||||||
|
{
|
||||||
|
if (col != null)
|
||||||
|
TryBite(col);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DisableBiteWindow()
|
||||||
|
{
|
||||||
|
if (biteZoneCollider != null)
|
||||||
|
biteZoneCollider.enabled = false;
|
||||||
|
|
||||||
|
collidersInside.Clear();
|
||||||
|
|
||||||
|
if (showDebugLog)
|
||||||
|
Debug.Log("[ClamBiteDetector] 조개 물림 판정 OFF", this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResetBiteState()
|
||||||
|
{
|
||||||
|
hasBittenThisClose = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsBiteWindowOpen()
|
||||||
|
{
|
||||||
|
if (biteZoneCollider == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return biteZoneCollider.enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TryBite(Collider other)
|
||||||
|
{
|
||||||
|
if (other == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (biteOncePerClose && hasBittenThisClose)
|
||||||
|
return;
|
||||||
|
|
||||||
|
bool isHand = other.CompareTag(handTag) || other.GetComponentInParent<XRHandMarker>() != null;
|
||||||
|
bool isFragment = other.CompareTag(fragmentTag) || other.GetComponentInParent<MemoryFragmentReset>() != null;
|
||||||
|
|
||||||
|
if (!isHand && !isFragment)
|
||||||
|
return;
|
||||||
|
|
||||||
|
hasBittenThisClose = true;
|
||||||
|
|
||||||
|
if (health != null)
|
||||||
|
health.TakeDamage(biteDamage);
|
||||||
|
|
||||||
|
if (memoryFragment != null)
|
||||||
|
memoryFragment.ResetFragment();
|
||||||
|
|
||||||
|
if (showDebugLog)
|
||||||
|
{
|
||||||
|
Debug.Log($"[ClamBiteDetector] 조개에게 물림. 데미지 {biteDamage}, 기억의 조각 리셋", this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/02_Scripts/Cave/ClamBiteDetector.cs.meta
Normal file
2
Assets/02_Scripts/Cave/ClamBiteDetector.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 9fc0bb91f57b5a74392435f20649c3e9
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Collections;
|
using System.Collections;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
using UnityEngine.Events;
|
||||||
|
|
||||||
public class ClamOpenClose : MonoBehaviour
|
public class ClamOpenClose : MonoBehaviour
|
||||||
{
|
{
|
||||||
@@ -22,7 +23,7 @@ public class ClamOpenClose : MonoBehaviour
|
|||||||
[SerializeField] private float maxOpenTime = 2.0f;
|
[SerializeField] private float maxOpenTime = 2.0f;
|
||||||
|
|
||||||
[SerializeField] private float openDuration = 1.5f;
|
[SerializeField] private float openDuration = 1.5f;
|
||||||
[SerializeField] private float closeDuration = 0.18f;
|
[SerializeField] private float closeDuration = 0.2f;
|
||||||
|
|
||||||
[Header("Motion")]
|
[Header("Motion")]
|
||||||
[SerializeField] private AnimationCurve openCurve = AnimationCurve.EaseInOut(0, 0, 1, 1);
|
[SerializeField] private AnimationCurve openCurve = AnimationCurve.EaseInOut(0, 0, 1, 1);
|
||||||
@@ -32,15 +33,20 @@ public class ClamOpenClose : MonoBehaviour
|
|||||||
[SerializeField] private bool useSnapClose = true;
|
[SerializeField] private bool useSnapClose = true;
|
||||||
|
|
||||||
[Tooltip("닫힐 때 살짝 더 닫히는 오버슈트 각도입니다.")]
|
[Tooltip("닫힐 때 살짝 더 닫히는 오버슈트 각도입니다.")]
|
||||||
[SerializeField] private float snapCloseOvershootX = 5f;
|
[SerializeField] private float snapCloseOvershootX = 3f;
|
||||||
|
|
||||||
[Tooltip("쾅 하고 닫힌 뒤 원래 닫힌 위치로 돌아오는 시간입니다.")]
|
[Tooltip("쾅 하고 닫힌 뒤 원래 닫힌 위치로 돌아오는 시간입니다.")]
|
||||||
[SerializeField] private float snapCloseOvershootDuration = 0.06f;
|
[SerializeField] private float snapCloseOvershootDuration = 0.02f;
|
||||||
|
|
||||||
[Header("Start Option")]
|
[Header("Start Option")]
|
||||||
[SerializeField] private bool startAutomatically = true;
|
[SerializeField] private bool startAutomatically = true;
|
||||||
[SerializeField] private bool startOpened = false;
|
[SerializeField] private bool startOpened = false;
|
||||||
|
|
||||||
|
[Header("Events")]
|
||||||
|
public UnityEvent onOpened;
|
||||||
|
public UnityEvent onCloseStarted;
|
||||||
|
public UnityEvent onClosed;
|
||||||
|
|
||||||
private Quaternion upClosedRot;
|
private Quaternion upClosedRot;
|
||||||
private Quaternion downClosedRot;
|
private Quaternion downClosedRot;
|
||||||
|
|
||||||
@@ -49,8 +55,10 @@ public class ClamOpenClose : MonoBehaviour
|
|||||||
|
|
||||||
private Coroutine routine;
|
private Coroutine routine;
|
||||||
private bool isOpen;
|
private bool isOpen;
|
||||||
|
private bool isClosing;
|
||||||
|
|
||||||
public bool IsOpen => isOpen;
|
public bool IsOpen => isOpen;
|
||||||
|
public bool IsClosing => isClosing;
|
||||||
|
|
||||||
private void Awake()
|
private void Awake()
|
||||||
{
|
{
|
||||||
@@ -72,13 +80,11 @@ private void Awake()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 현재 Scene에서 맞춰둔 로컬 회전을 닫힌 상태로 저장
|
|
||||||
upClosedRot = upShell.localRotation;
|
upClosedRot = upShell.localRotation;
|
||||||
|
|
||||||
if (downShell != null)
|
if (downShell != null)
|
||||||
downClosedRot = downShell.localRotation;
|
downClosedRot = downShell.localRotation;
|
||||||
|
|
||||||
// 닫힌 상태 기준으로 X축 회전 추가
|
|
||||||
upOpenedRot = upClosedRot * Quaternion.Euler(upShellOpenX, 0f, 0f);
|
upOpenedRot = upClosedRot * Quaternion.Euler(upShellOpenX, 0f, 0f);
|
||||||
|
|
||||||
if (downShell != null)
|
if (downShell != null)
|
||||||
@@ -120,7 +126,8 @@ private IEnumerator OpenCloseRoutine()
|
|||||||
float closedWait = Random.Range(minClosedTime, maxClosedTime);
|
float closedWait = Random.Range(minClosedTime, maxClosedTime);
|
||||||
yield return new WaitForSeconds(closedWait);
|
yield return new WaitForSeconds(closedWait);
|
||||||
|
|
||||||
// 천천히 열기
|
isClosing = false;
|
||||||
|
|
||||||
yield return MoveShells(
|
yield return MoveShells(
|
||||||
upClosedRot,
|
upClosedRot,
|
||||||
upOpenedRot,
|
upOpenedRot,
|
||||||
@@ -131,11 +138,15 @@ private IEnumerator OpenCloseRoutine()
|
|||||||
);
|
);
|
||||||
|
|
||||||
isOpen = true;
|
isOpen = true;
|
||||||
|
isClosing = false;
|
||||||
|
onOpened?.Invoke();
|
||||||
|
|
||||||
float openWait = Random.Range(minOpenTime, maxOpenTime);
|
float openWait = Random.Range(minOpenTime, maxOpenTime);
|
||||||
yield return new WaitForSeconds(openWait);
|
yield return new WaitForSeconds(openWait);
|
||||||
|
|
||||||
// 빠르게 닫기
|
isClosing = true;
|
||||||
|
onCloseStarted?.Invoke();
|
||||||
|
|
||||||
yield return MoveShells(
|
yield return MoveShells(
|
||||||
upOpenedRot,
|
upOpenedRot,
|
||||||
upClosedRot,
|
upClosedRot,
|
||||||
@@ -147,11 +158,13 @@ private IEnumerator OpenCloseRoutine()
|
|||||||
|
|
||||||
isOpen = false;
|
isOpen = false;
|
||||||
|
|
||||||
// 쾅 닫히는 느낌
|
|
||||||
if (useSnapClose)
|
if (useSnapClose)
|
||||||
{
|
{
|
||||||
yield return SnapCloseEffect();
|
yield return SnapCloseEffect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isClosing = false;
|
||||||
|
onClosed?.Invoke();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,7 +210,6 @@ private IEnumerator SnapCloseEffect()
|
|||||||
if (downShell != null)
|
if (downShell != null)
|
||||||
downSnapRot = downClosedRot * Quaternion.Euler(-snapCloseOvershootX * 0.3f, 0f, 0f);
|
downSnapRot = downClosedRot * Quaternion.Euler(-snapCloseOvershootX * 0.3f, 0f, 0f);
|
||||||
|
|
||||||
// 살짝 더 닫힘
|
|
||||||
if (upShell != null)
|
if (upShell != null)
|
||||||
upShell.localRotation = upSnapRot;
|
upShell.localRotation = upSnapRot;
|
||||||
|
|
||||||
@@ -206,7 +218,6 @@ private IEnumerator SnapCloseEffect()
|
|||||||
|
|
||||||
yield return new WaitForSeconds(snapCloseOvershootDuration);
|
yield return new WaitForSeconds(snapCloseOvershootDuration);
|
||||||
|
|
||||||
// 원래 닫힌 상태로 복귀
|
|
||||||
if (upShell != null)
|
if (upShell != null)
|
||||||
upShell.localRotation = upClosedRot;
|
upShell.localRotation = upClosedRot;
|
||||||
|
|
||||||
@@ -223,6 +234,7 @@ private void SetClosedImmediately()
|
|||||||
downShell.localRotation = downClosedRot;
|
downShell.localRotation = downClosedRot;
|
||||||
|
|
||||||
isOpen = false;
|
isOpen = false;
|
||||||
|
isClosing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetOpenedImmediately()
|
private void SetOpenedImmediately()
|
||||||
@@ -234,5 +246,6 @@ private void SetOpenedImmediately()
|
|||||||
downShell.localRotation = downOpenedRot;
|
downShell.localRotation = downOpenedRot;
|
||||||
|
|
||||||
isOpen = true;
|
isOpen = true;
|
||||||
|
isClosing = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
32
Assets/02_Scripts/Cave/DamageObstacle.cs
Normal file
32
Assets/02_Scripts/Cave/DamageObstacle.cs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
public class DamageObstacle : MonoBehaviour
|
||||||
|
{
|
||||||
|
public enum ObstacleType
|
||||||
|
{
|
||||||
|
Rock,
|
||||||
|
Rhino,
|
||||||
|
ClamBite
|
||||||
|
}
|
||||||
|
|
||||||
|
[Header("Damage")]
|
||||||
|
[SerializeField] private ObstacleType obstacleType = ObstacleType.Rock;
|
||||||
|
[SerializeField] private int damage = 10;
|
||||||
|
|
||||||
|
[Header("Options")]
|
||||||
|
[SerializeField] private bool canDamage = true;
|
||||||
|
|
||||||
|
public ObstacleType Type => obstacleType;
|
||||||
|
public int Damage => damage;
|
||||||
|
public bool CanDamage => canDamage;
|
||||||
|
|
||||||
|
public void SetCanDamage(bool value)
|
||||||
|
{
|
||||||
|
canDamage = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetDamage(int value)
|
||||||
|
{
|
||||||
|
damage = Mathf.Max(0, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/02_Scripts/Cave/DamageObstacle.cs.meta
Normal file
2
Assets/02_Scripts/Cave/DamageObstacle.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 310877c91d22e1142b0ea9977b24d3dc
|
||||||
85
Assets/02_Scripts/Cave/MemoryFragmentReset.cs
Normal file
85
Assets/02_Scripts/Cave/MemoryFragmentReset.cs
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.XR.Interaction.Toolkit.Interactables;
|
||||||
|
|
||||||
|
public class MemoryFragmentReset : MonoBehaviour
|
||||||
|
{
|
||||||
|
[Header("References")]
|
||||||
|
[SerializeField] private XRGrabInteractable grabInteractable;
|
||||||
|
[SerializeField] private Rigidbody rb;
|
||||||
|
|
||||||
|
[Header("Reset")]
|
||||||
|
[SerializeField] private Transform resetPoint;
|
||||||
|
|
||||||
|
[Tooltip("리셋 후 다시 잡을 수 있게 되기까지의 시간")]
|
||||||
|
[SerializeField] private float reEnableDelay = 0.15f;
|
||||||
|
|
||||||
|
[Header("Debug")]
|
||||||
|
[SerializeField] private bool showDebugLog = true;
|
||||||
|
|
||||||
|
private Vector3 startPosition;
|
||||||
|
private Quaternion startRotation;
|
||||||
|
private Transform startParent;
|
||||||
|
|
||||||
|
private Coroutine resetRoutine;
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
if (grabInteractable == null)
|
||||||
|
grabInteractable = GetComponent<XRGrabInteractable>();
|
||||||
|
|
||||||
|
if (rb == null)
|
||||||
|
rb = GetComponent<Rigidbody>();
|
||||||
|
|
||||||
|
startPosition = transform.position;
|
||||||
|
startRotation = transform.rotation;
|
||||||
|
startParent = transform.parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetFragment()
|
||||||
|
{
|
||||||
|
if (resetRoutine != null)
|
||||||
|
StopCoroutine(resetRoutine);
|
||||||
|
|
||||||
|
resetRoutine = StartCoroutine(ResetRoutine());
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerator ResetRoutine()
|
||||||
|
{
|
||||||
|
if (grabInteractable != null)
|
||||||
|
grabInteractable.enabled = false;
|
||||||
|
|
||||||
|
if (rb != null)
|
||||||
|
{
|
||||||
|
rb.linearVelocity = Vector3.zero;
|
||||||
|
rb.angularVelocity = Vector3.zero;
|
||||||
|
rb.isKinematic = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
transform.SetParent(startParent, true);
|
||||||
|
|
||||||
|
if (resetPoint != null)
|
||||||
|
{
|
||||||
|
transform.position = resetPoint.position;
|
||||||
|
transform.rotation = resetPoint.rotation;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
transform.position = startPosition;
|
||||||
|
transform.rotation = startRotation;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield return new WaitForSeconds(reEnableDelay);
|
||||||
|
|
||||||
|
if (rb != null)
|
||||||
|
rb.isKinematic = false;
|
||||||
|
|
||||||
|
if (grabInteractable != null)
|
||||||
|
grabInteractable.enabled = true;
|
||||||
|
|
||||||
|
if (showDebugLog)
|
||||||
|
Debug.Log("[MemoryFragmentReset] 기억의 조각을 원래 위치로 되돌렸습니다.", this);
|
||||||
|
|
||||||
|
resetRoutine = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/02_Scripts/Cave/MemoryFragmentReset.cs.meta
Normal file
2
Assets/02_Scripts/Cave/MemoryFragmentReset.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 7c8bb23fa35921f41b4e864bd95c0c77
|
||||||
156
Assets/02_Scripts/Cave/RaftDamageReceiver.cs
Normal file
156
Assets/02_Scripts/Cave/RaftDamageReceiver.cs
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.XR;
|
||||||
|
|
||||||
|
public class RaftDamageReceiver : MonoBehaviour
|
||||||
|
{
|
||||||
|
[Header("References")]
|
||||||
|
[SerializeField] private RaftHealth raftHealth;
|
||||||
|
|
||||||
|
[Header("Damage Cooldown")]
|
||||||
|
[SerializeField] private float sameObstacleDamageCooldown = 1.0f;
|
||||||
|
[SerializeField] private float globalDamageCooldown = 0.2f;
|
||||||
|
|
||||||
|
[Header("Haptic Feedback")]
|
||||||
|
[SerializeField] private bool useHapticFeedback = true;
|
||||||
|
|
||||||
|
[Range(0f, 1f)]
|
||||||
|
[SerializeField] private float hapticAmplitude = 0.6f;
|
||||||
|
|
||||||
|
[SerializeField] private float hapticDuration = 0.15f;
|
||||||
|
|
||||||
|
[Tooltip("체력 데미지 수치에 따라 햅틱 세기를 살짝 키웁니다.")]
|
||||||
|
[SerializeField] private bool scaleHapticByDamage = true;
|
||||||
|
|
||||||
|
[Header("Debug")]
|
||||||
|
[SerializeField] private bool showDebugLog = true;
|
||||||
|
|
||||||
|
private readonly Dictionary<DamageObstacle, float> lastDamageTimeByObstacle = new();
|
||||||
|
private float lastGlobalDamageTime = -999f;
|
||||||
|
|
||||||
|
private readonly List<InputDevice> hapticDevices = new();
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
if (raftHealth == null)
|
||||||
|
{
|
||||||
|
raftHealth = GetComponentInParent<RaftHealth>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (raftHealth == null)
|
||||||
|
{
|
||||||
|
raftHealth = FindFirstObjectByType<RaftHealth>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTriggerEnter(Collider other)
|
||||||
|
{
|
||||||
|
TryDamage(other);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTriggerStay(Collider other)
|
||||||
|
{
|
||||||
|
TryDamage(other);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TryDamage(Collider other)
|
||||||
|
{
|
||||||
|
if (raftHealth == null)
|
||||||
|
{
|
||||||
|
if (showDebugLog)
|
||||||
|
Debug.LogWarning("[RaftDamageReceiver] RaftHealth가 연결되지 않았습니다.", this);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DamageObstacle obstacle = other.GetComponentInParent<DamageObstacle>();
|
||||||
|
|
||||||
|
if (obstacle == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!obstacle.CanDamage)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (raftHealth.IsDead)
|
||||||
|
return;
|
||||||
|
|
||||||
|
float now = Time.time;
|
||||||
|
|
||||||
|
if (now < lastGlobalDamageTime + globalDamageCooldown)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (lastDamageTimeByObstacle.TryGetValue(obstacle, out float lastObstacleTime))
|
||||||
|
{
|
||||||
|
if (now < lastObstacleTime + sameObstacleDamageCooldown)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int damage = obstacle.Damage;
|
||||||
|
|
||||||
|
if (damage <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
raftHealth.TakeDamage(damage);
|
||||||
|
|
||||||
|
lastGlobalDamageTime = now;
|
||||||
|
lastDamageTimeByObstacle[obstacle] = now;
|
||||||
|
|
||||||
|
PlayHaptic(damage);
|
||||||
|
|
||||||
|
if (showDebugLog)
|
||||||
|
{
|
||||||
|
Debug.Log($"[RaftDamageReceiver] {obstacle.name} 충돌. 데미지: {damage}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PlayHaptic(int damage)
|
||||||
|
{
|
||||||
|
if (!useHapticFeedback)
|
||||||
|
return;
|
||||||
|
|
||||||
|
float amplitude = hapticAmplitude;
|
||||||
|
|
||||||
|
if (scaleHapticByDamage)
|
||||||
|
{
|
||||||
|
// 10 데미지 = 기본값, 15 이상 = 조금 더 강하게
|
||||||
|
float damageScale = Mathf.Clamp(damage / 10f, 1f, 1.5f);
|
||||||
|
amplitude *= damageScale;
|
||||||
|
}
|
||||||
|
|
||||||
|
amplitude = Mathf.Clamp01(amplitude);
|
||||||
|
|
||||||
|
SendHapticToDevice(XRNode.LeftHand, amplitude, hapticDuration);
|
||||||
|
SendHapticToDevice(XRNode.RightHand, amplitude, hapticDuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SendHapticToDevice(XRNode node, float amplitude, float duration)
|
||||||
|
{
|
||||||
|
InputDevice device = InputDevices.GetDeviceAtXRNode(node);
|
||||||
|
|
||||||
|
if (!device.isValid)
|
||||||
|
{
|
||||||
|
if (showDebugLog)
|
||||||
|
Debug.Log($"[RaftDamageReceiver] {node} 컨트롤러를 찾지 못했습니다.");
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!device.TryGetHapticCapabilities(out HapticCapabilities capabilities))
|
||||||
|
{
|
||||||
|
if (showDebugLog)
|
||||||
|
Debug.Log($"[RaftDamageReceiver] {node} 햅틱 기능을 확인할 수 없습니다.");
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!capabilities.supportsImpulse)
|
||||||
|
{
|
||||||
|
if (showDebugLog)
|
||||||
|
Debug.Log($"[RaftDamageReceiver] {node} 컨트롤러가 impulse 햅틱을 지원하지 않습니다.");
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
device.SendHapticImpulse(0u, amplitude, duration);
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/02_Scripts/Cave/RaftDamageReceiver.cs.meta
Normal file
2
Assets/02_Scripts/Cave/RaftDamageReceiver.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 4d64021aa9910eb4bb87691a2cdc6697
|
||||||
82
Assets/02_Scripts/Cave/RaftHealth.cs
Normal file
82
Assets/02_Scripts/Cave/RaftHealth.cs
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.Events;
|
||||||
|
|
||||||
|
public class RaftHealth : MonoBehaviour
|
||||||
|
{
|
||||||
|
[Header("Health")]
|
||||||
|
[SerializeField] private int maxHealth = 100;
|
||||||
|
[SerializeField] private int currentHealth = 100;
|
||||||
|
|
||||||
|
[Header("Options")]
|
||||||
|
[SerializeField] private bool resetHealthOnStart = true;
|
||||||
|
|
||||||
|
[Header("Events")]
|
||||||
|
public UnityEvent<int, int> onHealthChanged;
|
||||||
|
public UnityEvent onDead;
|
||||||
|
|
||||||
|
public int MaxHealth => maxHealth;
|
||||||
|
public int CurrentHealth => currentHealth;
|
||||||
|
public bool IsDead => currentHealth <= 0;
|
||||||
|
|
||||||
|
private void Start()
|
||||||
|
{
|
||||||
|
if (resetHealthOnStart)
|
||||||
|
{
|
||||||
|
ResetHealth();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetHealth()
|
||||||
|
{
|
||||||
|
currentHealth = maxHealth;
|
||||||
|
currentHealth = Mathf.Clamp(currentHealth, 0, maxHealth);
|
||||||
|
|
||||||
|
Debug.Log($"[RaftHealth] 체력 초기화: {currentHealth}/{maxHealth}");
|
||||||
|
|
||||||
|
onHealthChanged?.Invoke(currentHealth, maxHealth);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void TakeDamage(int damage)
|
||||||
|
{
|
||||||
|
if (damage <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (IsDead)
|
||||||
|
return;
|
||||||
|
|
||||||
|
currentHealth -= damage;
|
||||||
|
currentHealth = Mathf.Clamp(currentHealth, 0, maxHealth);
|
||||||
|
|
||||||
|
Debug.Log($"[RaftHealth] 데미지 {damage} 받음. 현재 체력: {currentHealth}/{maxHealth}");
|
||||||
|
|
||||||
|
onHealthChanged?.Invoke(currentHealth, maxHealth);
|
||||||
|
|
||||||
|
if (currentHealth <= 0)
|
||||||
|
{
|
||||||
|
Die();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Heal(int amount)
|
||||||
|
{
|
||||||
|
if (amount <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (IsDead)
|
||||||
|
return;
|
||||||
|
|
||||||
|
currentHealth += amount;
|
||||||
|
currentHealth = Mathf.Clamp(currentHealth, 0, maxHealth);
|
||||||
|
|
||||||
|
Debug.Log($"[RaftHealth] 회복 {amount}. 현재 체력: {currentHealth}/{maxHealth}");
|
||||||
|
|
||||||
|
onHealthChanged?.Invoke(currentHealth, maxHealth);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Die()
|
||||||
|
{
|
||||||
|
Debug.Log("[RaftHealth] 체력이 0이 되었습니다. 뗏목 실패 처리.");
|
||||||
|
|
||||||
|
onDead?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/02_Scripts/Cave/RaftHealth.cs.meta
Normal file
2
Assets/02_Scripts/Cave/RaftHealth.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: a21b5472b91813b44bf392e78726081a
|
||||||
77
Assets/02_Scripts/Cave/RaftHealthUI.cs
Normal file
77
Assets/02_Scripts/Cave/RaftHealthUI.cs
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.UI;
|
||||||
|
using TMPro;
|
||||||
|
|
||||||
|
public class RaftHealthUI : MonoBehaviour
|
||||||
|
{
|
||||||
|
[Header("References")]
|
||||||
|
[SerializeField] private RaftHealth raftHealth;
|
||||||
|
[SerializeField] private Image hpFillImage;
|
||||||
|
[SerializeField] private TMP_Text hpText;
|
||||||
|
|
||||||
|
[Header("Text")]
|
||||||
|
[SerializeField] private string hpPrefix = "HP";
|
||||||
|
|
||||||
|
[Header("Options")]
|
||||||
|
[SerializeField] private bool autoFindHealth = true;
|
||||||
|
[SerializeField] private bool hideWhenNoHealth = false;
|
||||||
|
|
||||||
|
[Header("Debug")]
|
||||||
|
[SerializeField] private bool showDebugLog = true;
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
if (autoFindHealth && raftHealth == null)
|
||||||
|
{
|
||||||
|
raftHealth = FindFirstObjectByType<RaftHealth>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnEnable()
|
||||||
|
{
|
||||||
|
if (raftHealth != null)
|
||||||
|
{
|
||||||
|
raftHealth.onHealthChanged.AddListener(UpdateHealthUI);
|
||||||
|
UpdateHealthUI(raftHealth.CurrentHealth, raftHealth.MaxHealth);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (hideWhenNoHealth)
|
||||||
|
gameObject.SetActive(false);
|
||||||
|
|
||||||
|
if (showDebugLog)
|
||||||
|
Debug.LogWarning("[RaftHealthUI] RaftHealth가 연결되지 않았습니다.", this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDisable()
|
||||||
|
{
|
||||||
|
if (raftHealth != null)
|
||||||
|
{
|
||||||
|
raftHealth.onHealthChanged.RemoveListener(UpdateHealthUI);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateHealthUI(int currentHealth, int maxHealth)
|
||||||
|
{
|
||||||
|
if (maxHealth <= 0)
|
||||||
|
maxHealth = 1;
|
||||||
|
|
||||||
|
float ratio = Mathf.Clamp01((float)currentHealth / maxHealth);
|
||||||
|
|
||||||
|
if (hpFillImage != null)
|
||||||
|
{
|
||||||
|
hpFillImage.fillAmount = ratio;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hpText != null)
|
||||||
|
{
|
||||||
|
hpText.text = $"{currentHealth} / {maxHealth}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showDebugLog)
|
||||||
|
{
|
||||||
|
Debug.Log($"[RaftHealthUI] 체력 UI 갱신: {currentHealth}/{maxHealth}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/02_Scripts/Cave/RaftHealthUI.cs.meta
Normal file
2
Assets/02_Scripts/Cave/RaftHealthUI.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 33f1269210c01984da23f85d35082e80
|
||||||
@@ -16,6 +16,10 @@ public class RaftRiverController : MonoBehaviour
|
|||||||
[SerializeField] private float forwardSpeed = 5f;
|
[SerializeField] private float forwardSpeed = 5f;
|
||||||
[SerializeField] private float turnSpeed = 4f;
|
[SerializeField] private float turnSpeed = 4f;
|
||||||
|
|
||||||
|
[Header("Start Speed Control")]
|
||||||
|
[Tooltip("0이면 정지, 1이면 정상 속도입니다. 시작 가속용으로 사용합니다.")]
|
||||||
|
[SerializeField] private float speedMultiplier = 1f;
|
||||||
|
|
||||||
[Header("Side Control")]
|
[Header("Side Control")]
|
||||||
[SerializeField] private float sideMoveSpeed = 16f;
|
[SerializeField] private float sideMoveSpeed = 16f;
|
||||||
[SerializeField] private float sideAcceleration = 40f;
|
[SerializeField] private float sideAcceleration = 40f;
|
||||||
@@ -25,8 +29,10 @@ public class RaftRiverController : MonoBehaviour
|
|||||||
|
|
||||||
[Header("Path Follow Feel")]
|
[Header("Path Follow Feel")]
|
||||||
[SerializeField] private float pathFollowSmoothTime = 0.28f;
|
[SerializeField] private float pathFollowSmoothTime = 0.28f;
|
||||||
|
|
||||||
[Range(0f, 1f)]
|
[Range(0f, 1f)]
|
||||||
[SerializeField] private float rotationVelocityBlend = 0.45f;
|
[SerializeField] private float rotationVelocityBlend = 0.45f;
|
||||||
|
|
||||||
[SerializeField] private float steeringYawAngle = 18f;
|
[SerializeField] private float steeringYawAngle = 18f;
|
||||||
|
|
||||||
[Header("Manual Steering")]
|
[Header("Manual Steering")]
|
||||||
@@ -38,10 +44,27 @@ public class RaftRiverController : MonoBehaviour
|
|||||||
[SerializeField] private float arrivalSlowDownDistance = 12f;
|
[SerializeField] private float arrivalSlowDownDistance = 12f;
|
||||||
[SerializeField] private float arrivalMinSpeed = 0.8f;
|
[SerializeField] private float arrivalMinSpeed = 0.8f;
|
||||||
|
|
||||||
|
[Header("Final Stop Guard")]
|
||||||
|
[Tooltip("마지막 포인트에 가까워지면 거리 판정으로 도착 처리합니다.")]
|
||||||
|
[SerializeField] private float finalPointReachDistance = 3.0f;
|
||||||
|
|
||||||
|
[Tooltip("마지막 포인트 근처 몇 미터 안에서 지나침 감지를 할지 설정합니다.")]
|
||||||
|
[SerializeField] private float finalStopGuardDistance = 20f;
|
||||||
|
|
||||||
|
[Tooltip("목표점과의 X/Z 차이가 이 값 이상 다시 커지면 지나친 것으로 판단합니다.")]
|
||||||
|
[SerializeField] private float finalStopGuardAxisEpsilon = 0.05f;
|
||||||
|
|
||||||
|
[Tooltip("마지막 구간 방향 기준, 마지막 포인트를 넘어가면 즉시 도착 처리합니다.")]
|
||||||
|
[SerializeField] private bool stopWhenPassedFinalPlane = true;
|
||||||
|
|
||||||
|
[Tooltip("도착 처리 시 마지막 포인트 위치로 뗏목을 스냅할지 여부입니다.")]
|
||||||
|
[SerializeField] private bool snapToFinalPointOnArrive = true;
|
||||||
|
|
||||||
[Header("Events")]
|
[Header("Events")]
|
||||||
public UnityEvent onArrived;
|
public UnityEvent onArrived;
|
||||||
|
|
||||||
private int currentPointIndex = 0;
|
private int currentPointIndex = 0;
|
||||||
|
|
||||||
private float sideOffset = 0f;
|
private float sideOffset = 0f;
|
||||||
private float sideVelocity = 0f;
|
private float sideVelocity = 0f;
|
||||||
private float currentSteeringInput = 0f;
|
private float currentSteeringInput = 0f;
|
||||||
@@ -53,6 +76,9 @@ public class RaftRiverController : MonoBehaviour
|
|||||||
private Vector3 previousPosition;
|
private Vector3 previousPosition;
|
||||||
private Vector3 startCenterPosition;
|
private Vector3 startCenterPosition;
|
||||||
|
|
||||||
|
private Vector3 previousFinalDelta;
|
||||||
|
private bool hasPreviousFinalDelta;
|
||||||
|
|
||||||
private bool isFinished = false;
|
private bool isFinished = false;
|
||||||
private bool warnedMissingSteeringKey;
|
private bool warnedMissingSteeringKey;
|
||||||
|
|
||||||
@@ -76,10 +102,21 @@ private void Start()
|
|||||||
|
|
||||||
currentCenterPosition = transform.position;
|
currentCenterPosition = transform.position;
|
||||||
startCenterPosition = currentCenterPosition;
|
startCenterPosition = currentCenterPosition;
|
||||||
|
|
||||||
currentForward = transform.forward;
|
currentForward = transform.forward;
|
||||||
currentRight = transform.right;
|
currentForward.y = 0f;
|
||||||
|
|
||||||
|
if (currentForward.sqrMagnitude < 0.001f)
|
||||||
|
currentForward = Vector3.forward;
|
||||||
|
|
||||||
|
currentForward.Normalize();
|
||||||
|
|
||||||
|
currentRight = Vector3.Cross(Vector3.up, currentForward).normalized;
|
||||||
|
|
||||||
previousPosition = transform.position;
|
previousPosition = transform.position;
|
||||||
currentPointIndex = 0;
|
currentPointIndex = 0;
|
||||||
|
|
||||||
|
ResetFinalStopGuard();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Update()
|
private void Update()
|
||||||
@@ -88,24 +125,35 @@ private void Update()
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
HandleSideControl();
|
HandleSideControl();
|
||||||
MoveAlongPath();
|
|
||||||
|
bool arrived = MoveAlongPath();
|
||||||
|
|
||||||
|
if (arrived)
|
||||||
|
return;
|
||||||
|
|
||||||
ApplyRaftPositionAndRotation();
|
ApplyRaftPositionAndRotation();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void MoveAlongPath()
|
private bool MoveAlongPath()
|
||||||
{
|
{
|
||||||
SkipMissingPathPoints();
|
SkipMissingPathPoints();
|
||||||
|
|
||||||
int lastPointIndex = GetLastValidPathPointIndex();
|
int lastPointIndex = GetLastValidPathPointIndex();
|
||||||
|
|
||||||
if (lastPointIndex < 0 || currentPointIndex >= pathPoints.Length)
|
if (lastPointIndex < 0 || currentPointIndex >= pathPoints.Length)
|
||||||
{
|
{
|
||||||
FinishRaftRide();
|
FinishRaftRide();
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Transform targetPoint = pathPoints[currentPointIndex];
|
Transform targetPoint = pathPoints[currentPointIndex];
|
||||||
|
|
||||||
|
if (targetPoint == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
Vector3 toTarget = GetFlatVectorTo(targetPoint.position);
|
Vector3 toTarget = GetFlatVectorTo(targetPoint.position);
|
||||||
float distance = toTarget.magnitude;
|
float distance = toTarget.magnitude;
|
||||||
|
|
||||||
bool isLastTarget = currentPointIndex == lastPointIndex;
|
bool isLastTarget = currentPointIndex == lastPointIndex;
|
||||||
|
|
||||||
while (!isLastTarget && ShouldAdvancePathPoint(currentPointIndex, distance))
|
while (!isLastTarget && ShouldAdvancePathPoint(currentPointIndex, distance))
|
||||||
@@ -115,46 +163,56 @@ private void MoveAlongPath()
|
|||||||
|
|
||||||
if (currentPointIndex >= pathPoints.Length)
|
if (currentPointIndex >= pathPoints.Length)
|
||||||
{
|
{
|
||||||
SnapToPathPoint(targetPoint);
|
FinishAtFinalPoint();
|
||||||
FinishRaftRide();
|
return true;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
targetPoint = pathPoints[currentPointIndex];
|
targetPoint = pathPoints[currentPointIndex];
|
||||||
|
|
||||||
|
if (targetPoint == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
toTarget = GetFlatVectorTo(targetPoint.position);
|
toTarget = GetFlatVectorTo(targetPoint.position);
|
||||||
distance = toTarget.magnitude;
|
distance = toTarget.magnitude;
|
||||||
isLastTarget = currentPointIndex == lastPointIndex;
|
isLastTarget = currentPointIndex == lastPointIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLastTarget && distance <= pointReachDistance)
|
if (isLastTarget && IsCloseEnoughToFinalPoint(distance))
|
||||||
{
|
{
|
||||||
SnapToPathPoint(targetPoint);
|
FinishAtFinalPoint();
|
||||||
FinishRaftRide();
|
return true;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toTarget.sqrMagnitude < 0.001f)
|
if (toTarget.sqrMagnitude < 0.001f)
|
||||||
return;
|
return false;
|
||||||
|
|
||||||
Vector3 pathForward = toTarget.normalized;
|
Vector3 pathForward = toTarget.normalized;
|
||||||
currentForward = GetTravelForward(pathForward);
|
currentForward = GetTravelForward(pathForward);
|
||||||
|
|
||||||
float currentSpeed = GetCurrentForwardSpeed(distance);
|
float currentSpeed = GetCurrentForwardSpeed(distance);
|
||||||
float moveDistance = currentSpeed * Time.deltaTime;
|
float moveDistance = currentSpeed * Time.deltaTime;
|
||||||
|
|
||||||
if (isLastTarget && moveDistance >= distance)
|
if (isLastTarget && moveDistance >= distance)
|
||||||
{
|
{
|
||||||
SnapToPathPoint(targetPoint);
|
FinishAtFinalPoint();
|
||||||
FinishRaftRide();
|
return true;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
currentCenterPosition += currentForward * moveDistance;
|
currentCenterPosition += currentForward * moveDistance;
|
||||||
currentRight = Vector3.Cross(Vector3.up, currentForward).normalized;
|
|
||||||
|
if (currentForward.sqrMagnitude > 0.001f)
|
||||||
|
currentRight = Vector3.Cross(Vector3.up, currentForward).normalized;
|
||||||
|
|
||||||
|
if (TryFinishAtFinalPointByGuards())
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleSideControl()
|
private void HandleSideControl()
|
||||||
{
|
{
|
||||||
float input = 0f;
|
float input = 0f;
|
||||||
|
|
||||||
ResolveSteeringKey();
|
ResolveSteeringKey();
|
||||||
|
|
||||||
if (steeringKey != null)
|
if (steeringKey != null)
|
||||||
@@ -178,6 +236,7 @@ private void HandleSideControl()
|
|||||||
currentSteeringInput = input;
|
currentSteeringInput = input;
|
||||||
|
|
||||||
float targetSideVelocity = input * sideMoveSpeed;
|
float targetSideVelocity = input * sideMoveSpeed;
|
||||||
|
|
||||||
sideVelocity = Mathf.MoveTowards(
|
sideVelocity = Mathf.MoveTowards(
|
||||||
sideVelocity,
|
sideVelocity,
|
||||||
targetSideVelocity,
|
targetSideVelocity,
|
||||||
@@ -202,6 +261,7 @@ private void ApplyRaftPositionAndRotation()
|
|||||||
targetPosition.y = transform.position.y;
|
targetPosition.y = transform.position.y;
|
||||||
|
|
||||||
float smoothTime = Mathf.Max(0.01f, pathFollowSmoothTime);
|
float smoothTime = Mathf.Max(0.01f, pathFollowSmoothTime);
|
||||||
|
|
||||||
transform.position = Vector3.SmoothDamp(
|
transform.position = Vector3.SmoothDamp(
|
||||||
transform.position,
|
transform.position,
|
||||||
targetPosition,
|
targetPosition,
|
||||||
@@ -215,6 +275,7 @@ private void ApplyRaftPositionAndRotation()
|
|||||||
frameVelocity.y = 0f;
|
frameVelocity.y = 0f;
|
||||||
|
|
||||||
Vector3 lookDirection = currentForward;
|
Vector3 lookDirection = currentForward;
|
||||||
|
|
||||||
if (frameVelocity.sqrMagnitude > 0.0001f)
|
if (frameVelocity.sqrMagnitude > 0.0001f)
|
||||||
{
|
{
|
||||||
lookDirection = Vector3.Slerp(
|
lookDirection = Vector3.Slerp(
|
||||||
@@ -224,6 +285,9 @@ private void ApplyRaftPositionAndRotation()
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (lookDirection.sqrMagnitude < 0.001f)
|
||||||
|
return;
|
||||||
|
|
||||||
Quaternion targetRotation =
|
Quaternion targetRotation =
|
||||||
Quaternion.LookRotation(lookDirection, Vector3.up) *
|
Quaternion.LookRotation(lookDirection, Vector3.up) *
|
||||||
Quaternion.Euler(0f, currentSteeringInput * steeringYawAngle, 0f);
|
Quaternion.Euler(0f, currentSteeringInput * steeringYawAngle, 0f);
|
||||||
@@ -236,25 +300,179 @@ private void ApplyRaftPositionAndRotation()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool IsCloseEnoughToFinalPoint(float distance)
|
||||||
|
{
|
||||||
|
float reachDistance = Mathf.Max(pointReachDistance, finalPointReachDistance);
|
||||||
|
return distance <= reachDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryFinishAtFinalPointByGuards()
|
||||||
|
{
|
||||||
|
int lastPointIndex = GetLastValidPathPointIndex();
|
||||||
|
|
||||||
|
if (lastPointIndex < 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (currentPointIndex != lastPointIndex)
|
||||||
|
{
|
||||||
|
ResetFinalStopGuard();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Transform finalPoint = pathPoints[lastPointIndex];
|
||||||
|
|
||||||
|
if (finalPoint == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
Vector3 finalDelta = finalPoint.position - currentCenterPosition;
|
||||||
|
finalDelta.y = 0f;
|
||||||
|
|
||||||
|
float distanceToFinal = finalDelta.magnitude;
|
||||||
|
|
||||||
|
if (distanceToFinal <= finalPointReachDistance)
|
||||||
|
{
|
||||||
|
Debug.Log("[RaftRiverController] Final point reach distance 안에 들어와 도착 처리합니다.");
|
||||||
|
FinishAtFinalPoint();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stopWhenPassedFinalPlane && HasPassedFinalPlane())
|
||||||
|
{
|
||||||
|
Debug.Log("[RaftRiverController] 마지막 도착 평면을 지나 도착 처리합니다.");
|
||||||
|
FinishAtFinalPoint();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TryFinishIfMovingAwayFromFinalPoint(finalDelta, distanceToFinal))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool HasPassedFinalPlane()
|
||||||
|
{
|
||||||
|
int lastPointIndex = GetLastValidPathPointIndex();
|
||||||
|
int penultimateIndex = GetPenultimateValidPathPointIndex();
|
||||||
|
|
||||||
|
if (lastPointIndex < 0 || penultimateIndex < 0 || lastPointIndex == penultimateIndex)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
Transform finalPoint = pathPoints[lastPointIndex];
|
||||||
|
Transform previousPoint = pathPoints[penultimateIndex];
|
||||||
|
|
||||||
|
if (finalPoint == null || previousPoint == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
Vector3 previousPos = previousPoint.position;
|
||||||
|
Vector3 finalPos = finalPoint.position;
|
||||||
|
Vector3 centerPos = currentCenterPosition;
|
||||||
|
|
||||||
|
previousPos.y = 0f;
|
||||||
|
finalPos.y = 0f;
|
||||||
|
centerPos.y = 0f;
|
||||||
|
|
||||||
|
Vector3 finalSegmentDirection = finalPos - previousPos;
|
||||||
|
|
||||||
|
if (finalSegmentDirection.sqrMagnitude < 0.001f)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
finalSegmentDirection.Normalize();
|
||||||
|
|
||||||
|
Vector3 fromFinalToRaft = centerPos - finalPos;
|
||||||
|
|
||||||
|
float passedAmount = Vector3.Dot(fromFinalToRaft, finalSegmentDirection);
|
||||||
|
|
||||||
|
return passedAmount >= 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryFinishIfMovingAwayFromFinalPoint(Vector3 finalDelta, float distanceToFinal)
|
||||||
|
{
|
||||||
|
if (distanceToFinal > finalStopGuardDistance)
|
||||||
|
{
|
||||||
|
previousFinalDelta = finalDelta;
|
||||||
|
hasPreviousFinalDelta = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasPreviousFinalDelta)
|
||||||
|
{
|
||||||
|
previousFinalDelta = finalDelta;
|
||||||
|
hasPreviousFinalDelta = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector3 previousAbs = new Vector3(
|
||||||
|
Mathf.Abs(previousFinalDelta.x),
|
||||||
|
0f,
|
||||||
|
Mathf.Abs(previousFinalDelta.z)
|
||||||
|
);
|
||||||
|
|
||||||
|
Vector3 currentAbs = new Vector3(
|
||||||
|
Mathf.Abs(finalDelta.x),
|
||||||
|
0f,
|
||||||
|
Mathf.Abs(finalDelta.z)
|
||||||
|
);
|
||||||
|
|
||||||
|
bool xMovingAway = currentAbs.x > previousAbs.x + finalStopGuardAxisEpsilon;
|
||||||
|
bool zMovingAway = currentAbs.z > previousAbs.z + finalStopGuardAxisEpsilon;
|
||||||
|
|
||||||
|
if (xMovingAway || zMovingAway)
|
||||||
|
{
|
||||||
|
Debug.Log("[RaftRiverController] 마지막 포인트에서 멀어지는 것으로 판단하여 도착 처리합니다.");
|
||||||
|
FinishAtFinalPoint();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
previousFinalDelta = finalDelta;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FinishAtFinalPoint()
|
||||||
|
{
|
||||||
|
int lastPointIndex = GetLastValidPathPointIndex();
|
||||||
|
|
||||||
|
if (lastPointIndex >= 0 && pathPoints[lastPointIndex] != null && snapToFinalPointOnArrive)
|
||||||
|
{
|
||||||
|
SnapToPathPoint(pathPoints[lastPointIndex]);
|
||||||
|
}
|
||||||
|
|
||||||
|
FinishRaftRide();
|
||||||
|
}
|
||||||
|
|
||||||
private void FinishRaftRide()
|
private void FinishRaftRide()
|
||||||
{
|
{
|
||||||
if (isFinished)
|
if (isFinished)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
isFinished = true;
|
isFinished = true;
|
||||||
|
SetSpeedMultiplier(0f);
|
||||||
|
|
||||||
|
sideVelocity = 0f;
|
||||||
|
currentSteeringInput = 0f;
|
||||||
|
positionSmoothVelocity = Vector3.zero;
|
||||||
|
|
||||||
Debug.Log("[RaftRiverController] Arrived at destination. Raft stopped.");
|
Debug.Log("[RaftRiverController] Arrived at destination. Raft stopped.");
|
||||||
|
|
||||||
onArrived?.Invoke();
|
onArrived?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void StopRaft()
|
public void StopRaft()
|
||||||
{
|
{
|
||||||
isFinished = true;
|
isFinished = true;
|
||||||
|
sideVelocity = 0f;
|
||||||
|
currentSteeringInput = 0f;
|
||||||
|
positionSmoothVelocity = Vector3.zero;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ResumeRaft()
|
public void ResumeRaft()
|
||||||
{
|
{
|
||||||
isFinished = false;
|
isFinished = false;
|
||||||
|
ResetFinalStopGuard();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetSpeedMultiplier(float value)
|
||||||
|
{
|
||||||
|
speedMultiplier = Mathf.Clamp01(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetSteeringKey(SteeringKeyXR newSteeringKey)
|
public void SetSteeringKey(SteeringKeyXR newSteeringKey)
|
||||||
@@ -286,7 +504,11 @@ private Vector3 GetTravelForward(Vector3 fallbackForward)
|
|||||||
|
|
||||||
if (steeringKey != null && steeringKey.IsGrabbed)
|
if (steeringKey != null && steeringKey.IsGrabbed)
|
||||||
{
|
{
|
||||||
float turnAmount = currentSteeringInput * Mathf.Max(0f, grabbedSteeringTurnSpeed) * Time.deltaTime;
|
float turnAmount =
|
||||||
|
currentSteeringInput *
|
||||||
|
Mathf.Max(0f, grabbedSteeringTurnSpeed) *
|
||||||
|
Time.deltaTime;
|
||||||
|
|
||||||
travelForward = Quaternion.Euler(0f, turnAmount, 0f) * travelForward.normalized;
|
travelForward = Quaternion.Euler(0f, turnAmount, 0f) * travelForward.normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,15 +529,22 @@ private bool ShouldAdvancePathPoint(int targetIndex, float distanceToTarget)
|
|||||||
|
|
||||||
private bool HasPassedPathPoint(int targetIndex)
|
private bool HasPassedPathPoint(int targetIndex)
|
||||||
{
|
{
|
||||||
if (pathPoints == null || targetIndex < 0 || targetIndex >= pathPoints.Length || pathPoints[targetIndex] == null)
|
if (pathPoints == null ||
|
||||||
|
targetIndex < 0 ||
|
||||||
|
targetIndex >= pathPoints.Length ||
|
||||||
|
pathPoints[targetIndex] == null)
|
||||||
|
{
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
Vector3 anchorPosition = GetPreviousPathAnchorPosition(targetIndex);
|
Vector3 anchorPosition = GetPreviousPathAnchorPosition(targetIndex);
|
||||||
Vector3 targetPosition = pathPoints[targetIndex].position;
|
Vector3 targetPosition = pathPoints[targetIndex].position;
|
||||||
|
|
||||||
anchorPosition.y = currentCenterPosition.y;
|
anchorPosition.y = currentCenterPosition.y;
|
||||||
targetPosition.y = currentCenterPosition.y;
|
targetPosition.y = currentCenterPosition.y;
|
||||||
|
|
||||||
Vector3 segment = targetPosition - anchorPosition;
|
Vector3 segment = targetPosition - anchorPosition;
|
||||||
|
|
||||||
if (segment.sqrMagnitude < 0.001f)
|
if (segment.sqrMagnitude < 0.001f)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
@@ -348,12 +577,16 @@ private void SnapToPathPoint(Transform point)
|
|||||||
finalPosition.y = transform.position.y;
|
finalPosition.y = transform.position.y;
|
||||||
|
|
||||||
currentCenterPosition = finalPosition;
|
currentCenterPosition = finalPosition;
|
||||||
|
|
||||||
sideOffset = 0f;
|
sideOffset = 0f;
|
||||||
sideVelocity = 0f;
|
sideVelocity = 0f;
|
||||||
currentSteeringInput = 0f;
|
currentSteeringInput = 0f;
|
||||||
positionSmoothVelocity = Vector3.zero;
|
positionSmoothVelocity = Vector3.zero;
|
||||||
previousPosition = finalPosition;
|
previousPosition = finalPosition;
|
||||||
|
|
||||||
transform.position = finalPosition;
|
transform.position = finalPosition;
|
||||||
|
|
||||||
|
ResetFinalStopGuard();
|
||||||
}
|
}
|
||||||
|
|
||||||
private int GetPenultimateValidPathPointIndex()
|
private int GetPenultimateValidPathPointIndex()
|
||||||
@@ -383,18 +616,25 @@ private int GetPenultimateValidPathPointIndex()
|
|||||||
private float GetArrivalSlowDownDistance()
|
private float GetArrivalSlowDownDistance()
|
||||||
{
|
{
|
||||||
float fallbackDistance = Mathf.Max(pointReachDistance + 0.01f, arrivalSlowDownDistance);
|
float fallbackDistance = Mathf.Max(pointReachDistance + 0.01f, arrivalSlowDownDistance);
|
||||||
|
|
||||||
int penultimateIndex = GetPenultimateValidPathPointIndex();
|
int penultimateIndex = GetPenultimateValidPathPointIndex();
|
||||||
int lastPointIndex = GetLastValidPathPointIndex();
|
int lastPointIndex = GetLastValidPathPointIndex();
|
||||||
|
|
||||||
if (penultimateIndex < 0 || lastPointIndex < 0 || penultimateIndex == lastPointIndex)
|
if (penultimateIndex < 0 ||
|
||||||
|
lastPointIndex < 0 ||
|
||||||
|
penultimateIndex == lastPointIndex)
|
||||||
|
{
|
||||||
return fallbackDistance;
|
return fallbackDistance;
|
||||||
|
}
|
||||||
|
|
||||||
Vector3 penultimatePosition = pathPoints[penultimateIndex].position;
|
Vector3 penultimatePosition = pathPoints[penultimateIndex].position;
|
||||||
Vector3 finalPosition = pathPoints[lastPointIndex].position;
|
Vector3 finalPosition = pathPoints[lastPointIndex].position;
|
||||||
|
|
||||||
penultimatePosition.y = 0f;
|
penultimatePosition.y = 0f;
|
||||||
finalPosition.y = 0f;
|
finalPosition.y = 0f;
|
||||||
|
|
||||||
float finalSegmentDistance = Vector3.Distance(penultimatePosition, finalPosition);
|
float finalSegmentDistance = Vector3.Distance(penultimatePosition, finalPosition);
|
||||||
|
|
||||||
if (finalSegmentDistance <= pointReachDistance)
|
if (finalSegmentDistance <= pointReachDistance)
|
||||||
return fallbackDistance;
|
return fallbackDistance;
|
||||||
|
|
||||||
@@ -403,22 +643,46 @@ private float GetArrivalSlowDownDistance()
|
|||||||
|
|
||||||
private float GetCurrentForwardSpeed(float distanceToTarget)
|
private float GetCurrentForwardSpeed(float distanceToTarget)
|
||||||
{
|
{
|
||||||
|
float baseSpeed;
|
||||||
|
|
||||||
if (currentPointIndex != GetLastValidPathPointIndex())
|
if (currentPointIndex != GetLastValidPathPointIndex())
|
||||||
return forwardSpeed;
|
{
|
||||||
|
baseSpeed = forwardSpeed;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
float maxSpeed = Mathf.Max(0f, forwardSpeed);
|
||||||
|
|
||||||
float maxSpeed = Mathf.Max(0f, forwardSpeed);
|
if (maxSpeed <= 0.01f)
|
||||||
if (maxSpeed <= 0.01f)
|
{
|
||||||
return maxSpeed;
|
baseSpeed = maxSpeed;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
float slowDownDistance = GetArrivalSlowDownDistance();
|
||||||
|
|
||||||
float slowDownDistance = GetArrivalSlowDownDistance();
|
if (slowDownDistance <= pointReachDistance)
|
||||||
if (slowDownDistance <= pointReachDistance)
|
{
|
||||||
return maxSpeed;
|
baseSpeed = maxSpeed;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
float minSpeed = Mathf.Clamp(arrivalMinSpeed, 0.01f, maxSpeed);
|
||||||
|
|
||||||
float minSpeed = Mathf.Clamp(arrivalMinSpeed, 0.01f, maxSpeed);
|
float speedRatio = Mathf.InverseLerp(
|
||||||
float speedRatio = Mathf.InverseLerp(pointReachDistance, slowDownDistance, distanceToTarget);
|
pointReachDistance,
|
||||||
speedRatio = speedRatio * speedRatio * (3f - 2f * speedRatio);
|
slowDownDistance,
|
||||||
|
distanceToTarget
|
||||||
|
);
|
||||||
|
|
||||||
return Mathf.Lerp(minSpeed, maxSpeed, speedRatio);
|
speedRatio = speedRatio * speedRatio * (3f - 2f * speedRatio);
|
||||||
|
|
||||||
|
baseSpeed = Mathf.Lerp(minSpeed, maxSpeed, speedRatio);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseSpeed * speedMultiplier;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int GetLastValidPathPointIndex()
|
private int GetLastValidPathPointIndex()
|
||||||
@@ -443,6 +707,12 @@ private void SkipMissingPathPoints()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ResetFinalStopGuard()
|
||||||
|
{
|
||||||
|
previousFinalDelta = Vector3.zero;
|
||||||
|
hasPreviousFinalDelta = false;
|
||||||
|
}
|
||||||
|
|
||||||
private float ReadLegacyHorizontalInput()
|
private float ReadLegacyHorizontalInput()
|
||||||
{
|
{
|
||||||
#if ENABLE_LEGACY_INPUT_MANAGER
|
#if ENABLE_LEGACY_INPUT_MANAGER
|
||||||
|
|||||||
238
Assets/02_Scripts/Cave/RaftStartManager.cs
Normal file
238
Assets/02_Scripts/Cave/RaftStartManager.cs
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
public class RaftStartManager : MonoBehaviour
|
||||||
|
{
|
||||||
|
private enum StartState
|
||||||
|
{
|
||||||
|
Ready,
|
||||||
|
WaitingForKeyGrab,
|
||||||
|
Starting,
|
||||||
|
Riding,
|
||||||
|
Arrived,
|
||||||
|
Failed
|
||||||
|
}
|
||||||
|
|
||||||
|
[Header("References")]
|
||||||
|
[SerializeField] private RaftRiverController raftController;
|
||||||
|
[SerializeField] private SteeringKeyXR steeringKey;
|
||||||
|
|
||||||
|
[Tooltip("현재는 캡슐 요정. 나중에 요정 캐릭터 모델로 교체할 부모 오브젝트를 넣으면 됩니다.")]
|
||||||
|
[SerializeField] private GameObject fairyObject;
|
||||||
|
|
||||||
|
[Header("Obstacles")]
|
||||||
|
[Tooltip("뗏목 출발 시 함께 작동할 코뿔소들입니다.")]
|
||||||
|
[SerializeField] private RhinoObstacle[] rhinos;
|
||||||
|
|
||||||
|
[Header("Health")]
|
||||||
|
[SerializeField] private RaftHealth raftHealth;
|
||||||
|
|
||||||
|
[Header("Start Settings")]
|
||||||
|
[SerializeField] private bool waitForKeyGrab = true;
|
||||||
|
|
||||||
|
[Tooltip("키를 잡은 뒤 뗏목이 완전히 출발 속도에 도달하기까지 걸리는 시간")]
|
||||||
|
[SerializeField] private float startAccelerationDuration = 2.0f;
|
||||||
|
|
||||||
|
[Tooltip("출발 직후 바로 요정이 사라질지 여부")]
|
||||||
|
[SerializeField] private bool hideFairyOnStart = true;
|
||||||
|
|
||||||
|
[Header("Debug")]
|
||||||
|
[SerializeField] private bool showDebugLog = true;
|
||||||
|
|
||||||
|
private StartState state = StartState.Ready;
|
||||||
|
private Coroutine startRoutine;
|
||||||
|
|
||||||
|
private void Start()
|
||||||
|
{
|
||||||
|
SetupStartState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Update()
|
||||||
|
{
|
||||||
|
if (state != StartState.WaitingForKeyGrab)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!waitForKeyGrab)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (steeringKey != null && steeringKey.IsGrabbed)
|
||||||
|
{
|
||||||
|
BeginRaftRide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetupStartState()
|
||||||
|
{
|
||||||
|
ResolveReferences();
|
||||||
|
|
||||||
|
state = StartState.WaitingForKeyGrab;
|
||||||
|
|
||||||
|
if (raftController != null)
|
||||||
|
{
|
||||||
|
raftController.StopRaft();
|
||||||
|
raftController.SetSpeedMultiplier(0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (raftHealth != null)
|
||||||
|
{
|
||||||
|
raftHealth.ResetHealth();
|
||||||
|
}
|
||||||
|
|
||||||
|
StopAllRhinos();
|
||||||
|
|
||||||
|
if (fairyObject != null)
|
||||||
|
{
|
||||||
|
fairyObject.SetActive(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showDebugLog)
|
||||||
|
{
|
||||||
|
Debug.Log("[RaftStartManager] 시작 준비 완료. 키를 잡으면 뗏목이 출발합니다.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResolveReferences()
|
||||||
|
{
|
||||||
|
if (raftController == null)
|
||||||
|
raftController = FindFirstObjectByType<RaftRiverController>();
|
||||||
|
|
||||||
|
if (steeringKey == null)
|
||||||
|
steeringKey = FindFirstObjectByType<SteeringKeyXR>();
|
||||||
|
|
||||||
|
if (raftHealth == null)
|
||||||
|
raftHealth = FindFirstObjectByType<RaftHealth>();
|
||||||
|
|
||||||
|
if (rhinos == null || rhinos.Length == 0)
|
||||||
|
rhinos = FindObjectsByType<RhinoObstacle>(FindObjectsSortMode.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void BeginRaftRide()
|
||||||
|
{
|
||||||
|
if (state == StartState.Starting || state == StartState.Riding)
|
||||||
|
return;
|
||||||
|
|
||||||
|
state = StartState.Starting;
|
||||||
|
|
||||||
|
if (hideFairyOnStart && fairyObject != null)
|
||||||
|
{
|
||||||
|
fairyObject.SetActive(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startRoutine != null)
|
||||||
|
StopCoroutine(startRoutine);
|
||||||
|
|
||||||
|
startRoutine = StartCoroutine(StartRaftSmoothly());
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerator StartRaftSmoothly()
|
||||||
|
{
|
||||||
|
if (raftController == null)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[RaftStartManager] RaftRiverController가 연결되지 않았습니다.", this);
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
raftController.SetSpeedMultiplier(0f);
|
||||||
|
raftController.ResumeRaft();
|
||||||
|
|
||||||
|
StartAllRhinos();
|
||||||
|
|
||||||
|
if (showDebugLog)
|
||||||
|
{
|
||||||
|
Debug.Log("[RaftStartManager] 뗏목 출발 시작. 코뿔소 장애물도 시작합니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
float timer = 0f;
|
||||||
|
float duration = Mathf.Max(0.01f, startAccelerationDuration);
|
||||||
|
|
||||||
|
while (timer < duration)
|
||||||
|
{
|
||||||
|
timer += Time.deltaTime;
|
||||||
|
|
||||||
|
float t = Mathf.Clamp01(timer / duration);
|
||||||
|
float smoothT = t * t * (3f - 2f * t);
|
||||||
|
|
||||||
|
raftController.SetSpeedMultiplier(smoothT);
|
||||||
|
|
||||||
|
yield return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
raftController.SetSpeedMultiplier(1f);
|
||||||
|
|
||||||
|
state = StartState.Riding;
|
||||||
|
|
||||||
|
if (showDebugLog)
|
||||||
|
{
|
||||||
|
Debug.Log("[RaftStartManager] 뗏목 정상 운항 속도 도달.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnRaftArrived()
|
||||||
|
{
|
||||||
|
if (state == StartState.Arrived)
|
||||||
|
return;
|
||||||
|
|
||||||
|
state = StartState.Arrived;
|
||||||
|
|
||||||
|
if (raftController != null)
|
||||||
|
{
|
||||||
|
raftController.SetSpeedMultiplier(0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
StopAllRhinos();
|
||||||
|
|
||||||
|
if (showDebugLog)
|
||||||
|
{
|
||||||
|
Debug.Log("[RaftStartManager] 목적지 도착. 뗏목 구간 종료. 코뿔소 장애물 정지.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnRaftFailed()
|
||||||
|
{
|
||||||
|
if (state == StartState.Failed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
state = StartState.Failed;
|
||||||
|
|
||||||
|
if (raftController != null)
|
||||||
|
{
|
||||||
|
raftController.StopRaft();
|
||||||
|
raftController.SetSpeedMultiplier(0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
StopAllRhinos();
|
||||||
|
|
||||||
|
if (showDebugLog)
|
||||||
|
{
|
||||||
|
Debug.Log("[RaftStartManager] 체력 0. 뗏목 구간 실패. 코뿔소 장애물 정지.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StartAllRhinos()
|
||||||
|
{
|
||||||
|
if (rhinos == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (RhinoObstacle rhino in rhinos)
|
||||||
|
{
|
||||||
|
if (rhino == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
rhino.StartRhino();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StopAllRhinos()
|
||||||
|
{
|
||||||
|
if (rhinos == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (RhinoObstacle rhino in rhinos)
|
||||||
|
{
|
||||||
|
if (rhino == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
rhino.StopRhino();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/02_Scripts/Cave/RaftStartManager.cs.meta
Normal file
2
Assets/02_Scripts/Cave/RaftStartManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 24e1027c38bc1e34b9b4d80397ad481a
|
||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/02_Scripts/Cave/RhinoObstacle.cs.meta
Normal file
2
Assets/02_Scripts/Cave/RhinoObstacle.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 1650344929a31bf469215ce025b8fd1d
|
||||||
5
Assets/02_Scripts/Cave/XRHandMarker.cs
Normal file
5
Assets/02_Scripts/Cave/XRHandMarker.cs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
public class XRHandMarker : MonoBehaviour
|
||||||
|
{
|
||||||
|
}
|
||||||
2
Assets/02_Scripts/Cave/XRHandMarker.cs.meta
Normal file
2
Assets/02_Scripts/Cave/XRHandMarker.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 559f6e10a7fe4e2468ff96e9693b444e
|
||||||
@@ -7,11 +7,11 @@ AnimatorState:
|
|||||||
m_CorrespondingSourceObject: {fileID: 0}
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
m_PrefabInstance: {fileID: 0}
|
m_PrefabInstance: {fileID: 0}
|
||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
m_Name: WhiteRhino_Skelmesh|Rhino_Combat_Atk_Hit
|
m_Name: Hit
|
||||||
m_Speed: 1
|
m_Speed: 1
|
||||||
m_CycleOffset: 0
|
m_CycleOffset: 0
|
||||||
m_Transitions:
|
m_Transitions:
|
||||||
- {fileID: 8273947539310789631}
|
- {fileID: 5211168254593058196}
|
||||||
m_StateMachineBehaviours: []
|
m_StateMachineBehaviours: []
|
||||||
m_Position: {x: 50, y: 50, z: 0}
|
m_Position: {x: 50, y: 50, z: 0}
|
||||||
m_IKOnFeet: 0
|
m_IKOnFeet: 0
|
||||||
@@ -36,7 +36,7 @@ AnimatorStateTransition:
|
|||||||
m_Name:
|
m_Name:
|
||||||
m_Conditions:
|
m_Conditions:
|
||||||
- m_ConditionMode: 1
|
- m_ConditionMode: 1
|
||||||
m_ConditionEvent: isIdle
|
m_ConditionEvent: Hit
|
||||||
m_EventTreshold: 0
|
m_EventTreshold: 0
|
||||||
m_DstStateMachine: {fileID: 0}
|
m_DstStateMachine: {fileID: 0}
|
||||||
m_DstState: {fileID: -5762510348686555522}
|
m_DstState: {fileID: -5762510348686555522}
|
||||||
@@ -47,7 +47,7 @@ AnimatorStateTransition:
|
|||||||
m_TransitionDuration: 0.25
|
m_TransitionDuration: 0.25
|
||||||
m_TransitionOffset: 0
|
m_TransitionOffset: 0
|
||||||
m_ExitTime: 0.95
|
m_ExitTime: 0.95
|
||||||
m_HasExitTime: 1
|
m_HasExitTime: 0
|
||||||
m_HasFixedDuration: 1
|
m_HasFixedDuration: 1
|
||||||
m_InterruptionSource: 0
|
m_InterruptionSource: 0
|
||||||
m_OrderedInterruption: 1
|
m_OrderedInterruption: 1
|
||||||
@@ -63,7 +63,7 @@ AnimatorStateMachine:
|
|||||||
m_ChildStates:
|
m_ChildStates:
|
||||||
- serializedVersion: 1
|
- serializedVersion: 1
|
||||||
m_State: {fileID: 2482721225991305447}
|
m_State: {fileID: 2482721225991305447}
|
||||||
m_Position: {x: 330, y: 310, z: 0}
|
m_Position: {x: 300, y: 250, z: 0}
|
||||||
- serializedVersion: 1
|
- serializedVersion: 1
|
||||||
m_State: {fileID: -5762510348686555522}
|
m_State: {fileID: -5762510348686555522}
|
||||||
m_Position: {x: 670, y: 290, z: 0}
|
m_Position: {x: 670, y: 290, z: 0}
|
||||||
@@ -86,8 +86,8 @@ AnimatorController:
|
|||||||
m_Name: Rhinos
|
m_Name: Rhinos
|
||||||
serializedVersion: 5
|
serializedVersion: 5
|
||||||
m_AnimatorParameters:
|
m_AnimatorParameters:
|
||||||
- m_Name: isIdle
|
- m_Name: Hit
|
||||||
m_Type: 4
|
m_Type: 9
|
||||||
m_DefaultFloat: 0
|
m_DefaultFloat: 0
|
||||||
m_DefaultInt: 0
|
m_DefaultInt: 0
|
||||||
m_DefaultBool: 0
|
m_DefaultBool: 0
|
||||||
@@ -112,7 +112,7 @@ AnimatorState:
|
|||||||
m_CorrespondingSourceObject: {fileID: 0}
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
m_PrefabInstance: {fileID: 0}
|
m_PrefabInstance: {fileID: 0}
|
||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
m_Name: WhiteRhino_Skelmesh|AA_WhiteRhino_Loco_Idle_Normal
|
m_Name: Idle
|
||||||
m_Speed: 1
|
m_Speed: 1
|
||||||
m_CycleOffset: 0
|
m_CycleOffset: 0
|
||||||
m_Transitions:
|
m_Transitions:
|
||||||
@@ -132,7 +132,7 @@ AnimatorState:
|
|||||||
m_MirrorParameter:
|
m_MirrorParameter:
|
||||||
m_CycleOffsetParameter:
|
m_CycleOffsetParameter:
|
||||||
m_TimeParameter:
|
m_TimeParameter:
|
||||||
--- !u!1101 &8273947539310789631
|
--- !u!1101 &5211168254593058196
|
||||||
AnimatorStateTransition:
|
AnimatorStateTransition:
|
||||||
m_ObjectHideFlags: 1
|
m_ObjectHideFlags: 1
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
@@ -141,7 +141,7 @@ AnimatorStateTransition:
|
|||||||
m_Name:
|
m_Name:
|
||||||
m_Conditions: []
|
m_Conditions: []
|
||||||
m_DstStateMachine: {fileID: 0}
|
m_DstStateMachine: {fileID: 0}
|
||||||
m_DstState: {fileID: -5762510348686555522}
|
m_DstState: {fileID: 2482721225991305447}
|
||||||
m_Solo: 0
|
m_Solo: 0
|
||||||
m_Mute: 0
|
m_Mute: 0
|
||||||
m_IsExit: 0
|
m_IsExit: 0
|
||||||
|
|||||||
BIN
ProjectSettings/TagManager.asset
LFS
BIN
ProjectSettings/TagManager.asset
LFS
Binary file not shown.
Reference in New Issue
Block a user