using UnityEngine; using UnityEngine.Events; public class RaftRiverController : MonoBehaviour { [Header("Path")] [SerializeField] private Transform[] pathPoints; [Header("Steering Input")] [SerializeField] private SteeringKeyXR steeringKey; [Tooltip("체크하면 키 움직임과 반대로 뗏목이 좌우 이동합니다.")] [SerializeField] private bool reverseControl = true; [Header("Move Speed")] [SerializeField] private float forwardSpeed = 5f; [SerializeField] private float turnSpeed = 4f; [Header("Side Control")] [SerializeField] private float sideMoveSpeed = 16f; [SerializeField] private float sideAcceleration = 40f; [Tooltip("강 중앙선 기준 좌우 이동 가능 범위입니다. 동굴 폭 46이면 14~16 추천.")] [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("Arrival")] [SerializeField] private float pointReachDistance = 1.5f; [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 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가 비어 있습니다.", this); enabled = false; return; } currentCenterPosition = transform.position; currentForward = transform.forward; currentRight = transform.right; previousPosition = transform.position; // 첫 목표 지점은 0번이 아니라 1번부터 가는 것이 자연스러울 때가 많음. // 단, 0번이 현재 위치와 다르면 그대로 0번부터 이동. currentPointIndex = 0; } private void Update() { if (isFinished) return; MoveAlongPath(); HandleSideControl(); ApplyRaftPositionAndRotation(); } private void MoveAlongPath() { SkipMissingPathPoints(); if (currentPointIndex >= pathPoints.Length) { FinishRaftRide(); return; } Transform targetPoint = pathPoints[currentPointIndex]; Vector3 toTarget = targetPoint.position - currentCenterPosition; toTarget.y = 0f; float distance = toTarget.magnitude; if (distance < pointReachDistance) { currentPointIndex++; if (currentPointIndex >= pathPoints.Length) { FinishRaftRide(); return; } targetPoint = pathPoints[currentPointIndex]; toTarget = targetPoint.position - currentCenterPosition; toTarget.y = 0f; } if (toTarget.sqrMagnitude < 0.001f) return; currentForward = toTarget.normalized; currentCenterPosition += currentForward * forwardSpeed * Time.deltaTime; // 진행 방향 기준 오른쪽 벡터 currentRight = Vector3.Cross(Vector3.up, currentForward).normalized; } private void HandleSideControl() { float input = 0f; ResolveSteeringKey(); if (steeringKey != null) { input = steeringKey.SteeringValue; } else { input = ReadLegacyHorizontalInput(); if (!warnedMissingSteeringKey) { warnedMissingSteeringKey = true; Debug.LogWarning("[RaftRiverController] SteeringKeyXR 참조가 없어 좌우 조향 없이 전진합니다.", 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 ); } Quaternion targetRotation = Quaternion.LookRotation(lookDirection, Vector3.up) * Quaternion.Euler(0f, currentSteeringInput * steeringYawAngle, 0f); transform.rotation = Quaternion.Slerp( transform.rotation, targetRotation, turnSpeed * Time.deltaTime ); } } private void FinishRaftRide() { if (isFinished) return; isFinished = true; Debug.Log("[RaftRiverController] 목적지 도착. 뗏목 정지."); onArrived?.Invoke(); } public void StopRaft() { isFinished = true; } public void ResumeRaft() { isFinished = false; } public void SetSteeringKey(SteeringKeyXR newSteeringKey) { steeringKey = newSteeringKey; warnedMissingSteeringKey = false; } private void ResolveSteeringKey() { if (steeringKey != null) return; steeringKey = GetComponentInChildren(true); } private void SkipMissingPathPoints() { while (currentPointIndex < pathPoints.Length && pathPoints[currentPointIndex] == null) { currentPointIndex++; } } private float ReadLegacyHorizontalInput() { #if ENABLE_LEGACY_INPUT_MANAGER return Input.GetAxis("Horizontal"); #else return 0f; #endif } }