동글 운항

This commit is contained in:
2026-06-19 18:16:40 +09:00
parent 790e958b0a
commit 5b14d51884
22 changed files with 1225 additions and 33 deletions

View File

@@ -9,7 +9,7 @@ public class RaftRiverController : MonoBehaviour
[Header("Steering Input")]
[SerializeField] private SteeringKeyXR steeringKey;
[Tooltip("체크하면 키 움직임과 반대로 뗏목이 좌우 이동합니다.")]
[Tooltip("Invert left/right steering input.")]
[SerializeField] private bool reverseControl = true;
[Header("Move Speed")]
@@ -20,7 +20,7 @@ public class RaftRiverController : MonoBehaviour
[SerializeField] private float sideMoveSpeed = 16f;
[SerializeField] private float sideAcceleration = 40f;
[Tooltip("강 중앙선 기준 좌우 이동 가능 범위입니다. 동굴 폭 46이면 14~16 추천.")]
[Tooltip("Maximum side movement from the raft center line.")]
[SerializeField] private float maxSideOffset = 16f;
[Header("Path Follow Feel")]
@@ -29,8 +29,14 @@ public class RaftRiverController : MonoBehaviour
[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;
@@ -45,6 +51,7 @@ public class RaftRiverController : MonoBehaviour
private Vector3 currentRight;
private Vector3 positionSmoothVelocity;
private Vector3 previousPosition;
private Vector3 startCenterPosition;
private bool isFinished = false;
private bool warnedMissingSteeringKey;
@@ -62,18 +69,16 @@ private void Start()
if (pathPoints == null || pathPoints.Length == 0)
{
Debug.LogWarning("[RaftRiverController] Path Points가 비어 있습니다.", this);
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;
// 첫 목표 지점은 0번이 아니라 1번부터 가는 것이 자연스러울 때가 많음.
// 단, 0번이 현재 위치와 다르면 그대로 0번부터 이동.
currentPointIndex = 0;
}
@@ -82,8 +87,8 @@ private void Update()
if (isFinished)
return;
MoveAlongPath();
HandleSideControl();
MoveAlongPath();
ApplyRaftPositionAndRotation();
}
@@ -91,41 +96,59 @@ private void MoveAlongPath()
{
SkipMissingPathPoints();
if (currentPointIndex >= pathPoints.Length)
int lastPointIndex = GetLastValidPathPointIndex();
if (lastPointIndex < 0 || currentPointIndex >= pathPoints.Length)
{
FinishRaftRide();
return;
}
Transform targetPoint = pathPoints[currentPointIndex];
Vector3 toTarget = targetPoint.position - currentCenterPosition;
toTarget.y = 0f;
Vector3 toTarget = GetFlatVectorTo(targetPoint.position);
float distance = toTarget.magnitude;
bool isLastTarget = currentPointIndex == lastPointIndex;
if (distance < pointReachDistance)
while (!isLastTarget && ShouldAdvancePathPoint(currentPointIndex, distance))
{
currentPointIndex++;
SkipMissingPathPoints();
if (currentPointIndex >= pathPoints.Length)
{
SnapToPathPoint(targetPoint);
FinishRaftRide();
return;
}
targetPoint = pathPoints[currentPointIndex];
toTarget = targetPoint.position - currentCenterPosition;
toTarget.y = 0f;
toTarget = GetFlatVectorTo(targetPoint.position);
distance = toTarget.magnitude;
isLastTarget = currentPointIndex == lastPointIndex;
}
if (isLastTarget && distance <= pointReachDistance)
{
SnapToPathPoint(targetPoint);
FinishRaftRide();
return;
}
if (toTarget.sqrMagnitude < 0.001f)
return;
currentForward = toTarget.normalized;
currentCenterPosition += currentForward * forwardSpeed * Time.deltaTime;
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;
}
@@ -145,15 +168,12 @@ private void HandleSideControl()
if (!warnedMissingSteeringKey)
{
warnedMissingSteeringKey = true;
Debug.LogWarning("[RaftRiverController] SteeringKeyXR 참조가 없어 좌우 조향 없이 전진합니다.", this);
Debug.LogWarning("[RaftRiverController] SteeringKeyXR reference is missing. Falling back to legacy horizontal input.", this);
}
}
// 반전은 여기서만 처리
if (reverseControl)
{
input *= -1f;
}
currentSteeringInput = input;
@@ -179,8 +199,6 @@ private void ApplyRaftPositionAndRotation()
previousPosition = transform.position;
Vector3 targetPosition = currentCenterPosition + currentRight * sideOffset;
// 현재 뗏목 높이 유지
targetPosition.y = transform.position.y;
float smoothTime = Mathf.Max(0.01f, pathFollowSmoothTime);
@@ -225,8 +243,7 @@ private void FinishRaftRide()
isFinished = true;
Debug.Log("[RaftRiverController] 목적지 도착. 뗏목 정지.");
Debug.Log("[RaftRiverController] Arrived at destination. Raft stopped.");
onArrived?.Invoke();
}
@@ -254,6 +271,170 @@ private void ResolveSteeringKey()
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)
@@ -270,4 +451,4 @@ private float ReadLegacyHorizontalInput()
return 0f;
#endif
}
}
}