Files
WhaleAdventure_VR/Assets/02_Scripts/Cave/RaftRiverController.cs
2026-06-22 16:58:32 +09:00

724 lines
20 KiB
C#

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<SteeringKeyXR>(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
}
}