454 lines
13 KiB
C#
454 lines
13 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("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("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 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;
|
|
currentRight = transform.right;
|
|
previousPosition = transform.position;
|
|
currentPointIndex = 0;
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
if (isFinished)
|
|
return;
|
|
|
|
HandleSideControl();
|
|
MoveAlongPath();
|
|
ApplyRaftPositionAndRotation();
|
|
}
|
|
|
|
private void MoveAlongPath()
|
|
{
|
|
SkipMissingPathPoints();
|
|
|
|
int lastPointIndex = GetLastValidPathPointIndex();
|
|
if (lastPointIndex < 0 || currentPointIndex >= pathPoints.Length)
|
|
{
|
|
FinishRaftRide();
|
|
return;
|
|
}
|
|
|
|
Transform targetPoint = pathPoints[currentPointIndex];
|
|
Vector3 toTarget = GetFlatVectorTo(targetPoint.position);
|
|
float distance = toTarget.magnitude;
|
|
bool isLastTarget = currentPointIndex == lastPointIndex;
|
|
|
|
while (!isLastTarget && ShouldAdvancePathPoint(currentPointIndex, distance))
|
|
{
|
|
currentPointIndex++;
|
|
SkipMissingPathPoints();
|
|
|
|
if (currentPointIndex >= pathPoints.Length)
|
|
{
|
|
SnapToPathPoint(targetPoint);
|
|
FinishRaftRide();
|
|
return;
|
|
}
|
|
|
|
targetPoint = pathPoints[currentPointIndex];
|
|
toTarget = GetFlatVectorTo(targetPoint.position);
|
|
distance = toTarget.magnitude;
|
|
isLastTarget = currentPointIndex == lastPointIndex;
|
|
}
|
|
|
|
if (isLastTarget && distance <= pointReachDistance)
|
|
{
|
|
SnapToPathPoint(targetPoint);
|
|
FinishRaftRide();
|
|
return;
|
|
}
|
|
|
|
if (toTarget.sqrMagnitude < 0.001f)
|
|
return;
|
|
|
|
Vector3 pathForward = toTarget.normalized;
|
|
currentForward = GetTravelForward(pathForward);
|
|
float currentSpeed = GetCurrentForwardSpeed(distance);
|
|
float moveDistance = currentSpeed * Time.deltaTime;
|
|
|
|
if (isLastTarget && moveDistance >= distance)
|
|
{
|
|
SnapToPathPoint(targetPoint);
|
|
FinishRaftRide();
|
|
return;
|
|
}
|
|
|
|
currentCenterPosition += currentForward * moveDistance;
|
|
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 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
|
|
);
|
|
}
|
|
|
|
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] Arrived at destination. Raft stopped.");
|
|
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<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;
|
|
}
|
|
|
|
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)
|
|
{
|
|
if (currentPointIndex != GetLastValidPathPointIndex())
|
|
return forwardSpeed;
|
|
|
|
float maxSpeed = Mathf.Max(0f, forwardSpeed);
|
|
if (maxSpeed <= 0.01f)
|
|
return maxSpeed;
|
|
|
|
float slowDownDistance = GetArrivalSlowDownDistance();
|
|
if (slowDownDistance <= pointReachDistance)
|
|
return maxSpeed;
|
|
|
|
float minSpeed = Mathf.Clamp(arrivalMinSpeed, 0.01f, maxSpeed);
|
|
float speedRatio = Mathf.InverseLerp(pointReachDistance, slowDownDistance, distanceToTarget);
|
|
speedRatio = speedRatio * speedRatio * (3f - 2f * speedRatio);
|
|
|
|
return Mathf.Lerp(minSpeed, maxSpeed, speedRatio);
|
|
}
|
|
|
|
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 float ReadLegacyHorizontalInput()
|
|
{
|
|
#if ENABLE_LEGACY_INPUT_MANAGER
|
|
return Input.GetAxis("Horizontal");
|
|
#else
|
|
return 0f;
|
|
#endif
|
|
}
|
|
} |