using UnityEngine; using UnityEngine.Events; public class RaftRiverController : MonoBehaviour { [Header("Path")] [SerializeField] private Transform[] pathPoints; [Header("Steering Input")] [SerializeField] private SteeringKeyXR steeringKey; [Tooltip("Invert left/right steering input.")] [SerializeField] private bool reverseControl = true; [Header("Move Speed")] [SerializeField] private float forwardSpeed = 5f; [SerializeField] private float turnSpeed = 4f; [Header("Start Speed Control")] [Tooltip("0이면 정지, 1이면 정상 속도입니다. 시작 가속용으로 사용합니다.")] [SerializeField] private float speedMultiplier = 1f; [Header("Side Control")] [SerializeField] private float sideMoveSpeed = 16f; [SerializeField] private float sideAcceleration = 40f; [Tooltip("Maximum side movement from the raft center line.")] [SerializeField] private float maxSideOffset = 16f; [Header("Path Follow Feel")] [SerializeField] private float pathFollowSmoothTime = 0.28f; [Range(0f, 1f)] [SerializeField] private float rotationVelocityBlend = 0.45f; [SerializeField] private float steeringYawAngle = 18f; [Header("Manual Steering")] [Tooltip("Degrees per second SteeringKeyXR can turn the raft travel direction while held.")] [SerializeField] private float grabbedSteeringTurnSpeed = 75f; [Header("Arrival")] [SerializeField] private float pointReachDistance = 1.5f; [SerializeField] private float arrivalSlowDownDistance = 12f; [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")] public UnityEvent onArrived; private int currentPointIndex = 0; private float sideOffset = 0f; private float sideVelocity = 0f; private float currentSteeringInput = 0f; private Vector3 currentCenterPosition; private Vector3 currentForward; private Vector3 currentRight; private Vector3 positionSmoothVelocity; private Vector3 previousPosition; private Vector3 startCenterPosition; private Vector3 previousFinalDelta; private bool hasPreviousFinalDelta; private bool isFinished = false; private bool warnedMissingSteeringKey; public bool IsFinished => isFinished; private void Awake() { ResolveSteeringKey(); } private void Start() { ResolveSteeringKey(); if (pathPoints == null || pathPoints.Length == 0) { Debug.LogWarning("[RaftRiverController] Path Points are empty.", this); enabled = false; return; } currentCenterPosition = transform.position; startCenterPosition = currentCenterPosition; currentForward = transform.forward; currentForward.y = 0f; if (currentForward.sqrMagnitude < 0.001f) currentForward = Vector3.forward; currentForward.Normalize(); currentRight = Vector3.Cross(Vector3.up, currentForward).normalized; previousPosition = transform.position; currentPointIndex = 0; ResetFinalStopGuard(); } private void Update() { if (isFinished) return; HandleSideControl(); bool arrived = MoveAlongPath(); if (arrived) return; ApplyRaftPositionAndRotation(); } private bool MoveAlongPath() { SkipMissingPathPoints(); int lastPointIndex = GetLastValidPathPointIndex(); if (lastPointIndex < 0 || currentPointIndex >= pathPoints.Length) { FinishRaftRide(); return true; } Transform targetPoint = pathPoints[currentPointIndex]; if (targetPoint == null) return false; Vector3 toTarget = GetFlatVectorTo(targetPoint.position); float distance = toTarget.magnitude; bool isLastTarget = currentPointIndex == lastPointIndex; while (!isLastTarget && ShouldAdvancePathPoint(currentPointIndex, distance)) { currentPointIndex++; SkipMissingPathPoints(); if (currentPointIndex >= pathPoints.Length) { FinishAtFinalPoint(); return true; } targetPoint = pathPoints[currentPointIndex]; if (targetPoint == null) return false; toTarget = GetFlatVectorTo(targetPoint.position); distance = toTarget.magnitude; isLastTarget = currentPointIndex == lastPointIndex; } if (isLastTarget && IsCloseEnoughToFinalPoint(distance)) { FinishAtFinalPoint(); return true; } if (toTarget.sqrMagnitude < 0.001f) return false; Vector3 pathForward = toTarget.normalized; currentForward = GetTravelForward(pathForward); float currentSpeed = GetCurrentForwardSpeed(distance); float moveDistance = currentSpeed * Time.deltaTime; if (isLastTarget && moveDistance >= distance) { FinishAtFinalPoint(); return true; } currentCenterPosition += currentForward * moveDistance; if (currentForward.sqrMagnitude > 0.001f) currentRight = Vector3.Cross(Vector3.up, currentForward).normalized; if (TryFinishAtFinalPointByGuards()) return true; return false; } private void HandleSideControl() { float input = 0f; ResolveSteeringKey(); if (steeringKey != null) { input = steeringKey.SteeringValue; } else { input = ReadLegacyHorizontalInput(); if (!warnedMissingSteeringKey) { warnedMissingSteeringKey = true; Debug.LogWarning("[RaftRiverController] SteeringKeyXR reference is missing. Falling back to legacy horizontal input.", this); } } if (reverseControl) input *= -1f; currentSteeringInput = input; float targetSideVelocity = input * sideMoveSpeed; sideVelocity = Mathf.MoveTowards( sideVelocity, targetSideVelocity, sideAcceleration * Time.deltaTime ); sideOffset += sideVelocity * Time.deltaTime; sideOffset = Mathf.Clamp(sideOffset, -maxSideOffset, maxSideOffset); if (Mathf.Approximately(Mathf.Abs(sideOffset), maxSideOffset) && Mathf.Sign(sideVelocity) == Mathf.Sign(sideOffset)) { sideVelocity = 0f; } } private void ApplyRaftPositionAndRotation() { previousPosition = transform.position; Vector3 targetPosition = currentCenterPosition + currentRight * sideOffset; targetPosition.y = transform.position.y; float smoothTime = Mathf.Max(0.01f, pathFollowSmoothTime); transform.position = Vector3.SmoothDamp( transform.position, targetPosition, ref positionSmoothVelocity, smoothTime ); if (currentForward != Vector3.zero) { Vector3 frameVelocity = transform.position - previousPosition; frameVelocity.y = 0f; Vector3 lookDirection = currentForward; if (frameVelocity.sqrMagnitude > 0.0001f) { lookDirection = Vector3.Slerp( currentForward, frameVelocity.normalized, rotationVelocityBlend ); } if (lookDirection.sqrMagnitude < 0.001f) return; Quaternion targetRotation = Quaternion.LookRotation(lookDirection, Vector3.up) * Quaternion.Euler(0f, currentSteeringInput * steeringYawAngle, 0f); transform.rotation = Quaternion.Slerp( transform.rotation, targetRotation, turnSpeed * Time.deltaTime ); } } 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() { if (isFinished) return; isFinished = true; SetSpeedMultiplier(0f); sideVelocity = 0f; currentSteeringInput = 0f; positionSmoothVelocity = Vector3.zero; Debug.Log("[RaftRiverController] Arrived at destination. Raft stopped."); onArrived?.Invoke(); } public void StopRaft() { isFinished = true; sideVelocity = 0f; currentSteeringInput = 0f; positionSmoothVelocity = Vector3.zero; } public void ResumeRaft() { isFinished = false; ResetFinalStopGuard(); } public void SetSpeedMultiplier(float value) { speedMultiplier = Mathf.Clamp01(value); } public void SetSteeringKey(SteeringKeyXR newSteeringKey) { steeringKey = newSteeringKey; warnedMissingSteeringKey = false; } private void ResolveSteeringKey() { if (steeringKey != null) return; steeringKey = GetComponentInChildren(true); } private Vector3 GetTravelForward(Vector3 fallbackForward) { Vector3 travelForward = currentForward; travelForward.y = 0f; if (travelForward.sqrMagnitude < 0.001f) travelForward = transform.forward; travelForward.y = 0f; if (travelForward.sqrMagnitude < 0.001f) travelForward = fallbackForward; if (steeringKey != null && steeringKey.IsGrabbed) { float turnAmount = currentSteeringInput * Mathf.Max(0f, grabbedSteeringTurnSpeed) * Time.deltaTime; travelForward = Quaternion.Euler(0f, turnAmount, 0f) * travelForward.normalized; } return travelForward.normalized; } private Vector3 GetFlatVectorTo(Vector3 worldPosition) { Vector3 delta = worldPosition - currentCenterPosition; delta.y = 0f; return delta; } private bool ShouldAdvancePathPoint(int targetIndex, float distanceToTarget) { return distanceToTarget < pointReachDistance || HasPassedPathPoint(targetIndex); } private bool HasPassedPathPoint(int targetIndex) { if (pathPoints == null || targetIndex < 0 || targetIndex >= pathPoints.Length || pathPoints[targetIndex] == null) { return true; } Vector3 anchorPosition = GetPreviousPathAnchorPosition(targetIndex); Vector3 targetPosition = pathPoints[targetIndex].position; anchorPosition.y = currentCenterPosition.y; targetPosition.y = currentCenterPosition.y; Vector3 segment = targetPosition - anchorPosition; if (segment.sqrMagnitude < 0.001f) return false; Vector3 fromAnchor = currentCenterPosition - anchorPosition; fromAnchor.y = 0f; return Vector3.Dot(fromAnchor, segment.normalized) >= segment.magnitude; } private Vector3 GetPreviousPathAnchorPosition(int targetIndex) { if (pathPoints != null) { for (int i = targetIndex - 1; i >= 0; i--) { if (pathPoints[i] != null) return pathPoints[i].position; } } return startCenterPosition; } private void SnapToPathPoint(Transform point) { if (point == null) return; Vector3 finalPosition = point.position; finalPosition.y = transform.position.y; currentCenterPosition = finalPosition; sideOffset = 0f; sideVelocity = 0f; currentSteeringInput = 0f; positionSmoothVelocity = Vector3.zero; previousPosition = finalPosition; transform.position = finalPosition; ResetFinalStopGuard(); } private int GetPenultimateValidPathPointIndex() { if (pathPoints == null) return -1; int lastValidIndex = -1; for (int i = pathPoints.Length - 1; i >= 0; i--) { if (pathPoints[i] == null) continue; if (lastValidIndex < 0) { lastValidIndex = i; continue; } return i; } return lastValidIndex; } private float GetArrivalSlowDownDistance() { float fallbackDistance = Mathf.Max(pointReachDistance + 0.01f, arrivalSlowDownDistance); int penultimateIndex = GetPenultimateValidPathPointIndex(); int lastPointIndex = GetLastValidPathPointIndex(); if (penultimateIndex < 0 || lastPointIndex < 0 || penultimateIndex == lastPointIndex) { return fallbackDistance; } Vector3 penultimatePosition = pathPoints[penultimateIndex].position; Vector3 finalPosition = pathPoints[lastPointIndex].position; penultimatePosition.y = 0f; finalPosition.y = 0f; float finalSegmentDistance = Vector3.Distance(penultimatePosition, finalPosition); if (finalSegmentDistance <= pointReachDistance) return fallbackDistance; return Mathf.Max(pointReachDistance + 0.01f, finalSegmentDistance); } private float GetCurrentForwardSpeed(float distanceToTarget) { float baseSpeed; if (currentPointIndex != GetLastValidPathPointIndex()) { baseSpeed = forwardSpeed; } else { float maxSpeed = Mathf.Max(0f, forwardSpeed); if (maxSpeed <= 0.01f) { baseSpeed = maxSpeed; } else { float slowDownDistance = GetArrivalSlowDownDistance(); if (slowDownDistance <= pointReachDistance) { baseSpeed = maxSpeed; } else { float minSpeed = Mathf.Clamp(arrivalMinSpeed, 0.01f, maxSpeed); float speedRatio = Mathf.InverseLerp( pointReachDistance, slowDownDistance, distanceToTarget ); speedRatio = speedRatio * speedRatio * (3f - 2f * speedRatio); baseSpeed = Mathf.Lerp(minSpeed, maxSpeed, speedRatio); } } } return baseSpeed * speedMultiplier; } private int GetLastValidPathPointIndex() { if (pathPoints == null) return -1; for (int i = pathPoints.Length - 1; i >= 0; i--) { if (pathPoints[i] != null) return i; } return -1; } private void SkipMissingPathPoints() { while (currentPointIndex < pathPoints.Length && pathPoints[currentPointIndex] == null) { currentPointIndex++; } } private void ResetFinalStopGuard() { previousFinalDelta = Vector3.zero; hasPreviousFinalDelta = false; } private float ReadLegacyHorizontalInput() { #if ENABLE_LEGACY_INPUT_MANAGER return Input.GetAxis("Horizontal"); #else return 0f; #endif } }