Files
WhaleAdventure_VR/Assets/My project/Fishing Scripts/UI/FishingModelLineFollower.cs
2026-06-25 17:39:42 +09:00

351 lines
11 KiB
C#

using UnityEngine;
/// <summary>
/// 모델 낚시줄을 두 포인트 사이에 맞춰 배치/회전/길이 스케일하는 스크립트입니다.
/// 권장 사용 방식:
/// - StretchLineRoot 빈 오브젝트에 이 스크립트를 붙입니다.
/// - StretchLineRoot 자식으로 '늘어나는 중간 줄 모델'만 넣습니다.
/// - RodFixedLinePart, BobberFixedLinePart, Bobber/Hook은 StretchLineRoot 안에 넣지 않습니다.
/// - 낚시찌는 BobberLineStartPoint의 자식으로 넣어 줄 끝점에 고정합니다.
/// </summary>
public class FishingModelLineFollower : MonoBehaviour
{
public enum LineUseMode
{
WholeModelLine,
StretchSegmentOnly
}
public enum LineAxis
{
LocalX,
LocalY,
LocalZ
}
public enum PivotMode
{
Center,
Start,
End
}
[Header("Mode")]
[Tooltip("WholeModelLine: 전체 줄 모델을 두 포인트 사이에 맞춤 / StretchSegmentOnly: 분리된 중간 줄 조각만 늘리는 권장 모드")]
[SerializeField] private LineUseMode lineUseMode = LineUseMode.StretchSegmentOnly;
[Header("Line Targets")]
[Tooltip("늘어나는 줄의 시작점입니다. StretchSegmentOnly에서는 RodLineEndPoint를 연결하세요.")]
[SerializeField] private Transform rodTipPoint;
[Tooltip("늘어나는 줄의 끝점입니다. StretchSegmentOnly에서는 BobberLineStartPoint를 연결하세요.")]
[SerializeField] private Transform bobberPoint;
[Header("Stretch Settings")]
[Tooltip("모델 줄의 길이 방향 축입니다. 보통 LocalY부터 테스트하세요.")]
[SerializeField] private LineAxis lineAxis = LineAxis.LocalY;
[Tooltip("모델 줄의 원본 길이입니다. 줄이 너무 길게 늘어나면 값을 키우고, 너무 짧으면 값을 줄이세요.")]
[Min(0.0001f)]
[SerializeField] private float originalLength = 1f;
[Tooltip("두 포인트가 너무 가까울 때 사용할 최소 길이입니다.")]
[Min(0.0001f)]
[SerializeField] private float minLength = 0.03f;
[Tooltip("줄 두께 배율입니다. 1이면 원본 두께를 유지합니다.")]
[Min(0.0001f)]
[SerializeField] private float thicknessScale = 1f;
[Tooltip("모델 줄의 앞뒤 방향이 거꾸로 보일 때 켜세요. 포인트 연결은 바꾸지 않는 것을 추천합니다.")]
[SerializeField] private bool reverseVisualDirection;
[Tooltip("대부분 Center를 권장합니다. 모델 Pivot이 시작점/끝점에 있으면 Start 또는 End를 테스트하세요.")]
[SerializeField] private PivotMode pivotMode = PivotMode.Center;
[Header("Visual Mesh Correction")]
[Tooltip("실제 Mesh 자식입니다. 비워두면 첫 번째 Renderer가 있는 자식을 자동으로 찾습니다. 위치 보정은 Root가 아니라 이 Visual에 적용하세요.")]
[SerializeField] private Transform visualRoot;
[Tooltip("Visual Mesh의 로컬 위치 보정값입니다. 줄이 살짝 위/옆으로 떠 있을 때 작은 값으로만 보정하세요.")]
[SerializeField] private Vector3 visualLocalPositionOffset;
[Tooltip("Visual Mesh의 로컬 회전 보정값입니다. 모델 자체가 90도 틀어져 있을 때 사용하세요.")]
[SerializeField] private Vector3 visualLocalRotationOffsetEuler;
[Header("Root Rotation Correction")]
[Tooltip("루트 회전 보정값입니다. 가능하면 0,0,0으로 두고 Line Axis / Reverse Visual Direction / Visual Rotation Offset으로 먼저 맞추세요.")]
[SerializeField] private Vector3 rootRotationOffsetEuler;
[Header("Visibility / Update")]
[SerializeField] private bool hideWhenMissingTarget = true;
[SerializeField] private bool hideWhenTooShort = false;
[SerializeField] private bool updateInLateUpdate = true;
[Header("Debug")]
[SerializeField] private bool drawDebugLine = true;
[SerializeField] private Color debugLineColor = Color.cyan;
private Vector3 initialLocalScale = Vector3.one;
private Vector3 visualInitialLocalPosition;
private Quaternion visualInitialLocalRotation = Quaternion.identity;
private bool initialized;
public LineUseMode CurrentLineUseMode => lineUseMode;
public Transform RodTipPoint => rodTipPoint;
public Transform BobberPoint => bobberPoint;
private void Awake()
{
InitializeIfNeeded();
}
private void Reset()
{
lineUseMode = LineUseMode.StretchSegmentOnly;
lineAxis = LineAxis.LocalY;
originalLength = 1f;
minLength = 0.03f;
thicknessScale = 1f;
pivotMode = PivotMode.Center;
updateInLateUpdate = true;
drawDebugLine = true;
TryAutoFindVisualRoot();
}
private void Update()
{
if (!updateInLateUpdate)
UpdateLine();
}
private void LateUpdate()
{
if (updateInLateUpdate)
UpdateLine();
}
private void OnValidate()
{
originalLength = Mathf.Max(0.0001f, originalLength);
minLength = Mathf.Max(0.0001f, minLength);
thicknessScale = Mathf.Max(0.0001f, thicknessScale);
if (!Application.isPlaying)
TryAutoFindVisualRoot();
}
private void InitializeIfNeeded()
{
if (initialized)
return;
initialLocalScale = transform.localScale;
if (visualRoot == null)
TryAutoFindVisualRoot();
if (visualRoot != null)
{
visualInitialLocalPosition = visualRoot.localPosition;
visualInitialLocalRotation = visualRoot.localRotation;
}
initialized = true;
}
private void UpdateLine()
{
InitializeIfNeeded();
if (rodTipPoint == null || bobberPoint == null)
{
SetVisible(!hideWhenMissingTarget);
return;
}
Vector3 start = rodTipPoint.position;
Vector3 end = bobberPoint.position;
Vector3 segment = end - start;
float distance = segment.magnitude;
if (distance < minLength)
{
if (hideWhenTooShort)
{
SetVisible(false);
return;
}
distance = minLength;
}
Vector3 direction = segment.sqrMagnitude > 0.000001f ? segment.normalized : transform.forward;
Vector3 visualDirection = reverseVisualDirection ? -direction : direction;
SetVisible(true);
transform.position = GetRootPosition(start, end);
transform.rotation = Quaternion.FromToRotation(GetAxisVector(lineAxis), visualDirection) * Quaternion.Euler(rootRotationOffsetEuler);
transform.localScale = GetScaledVector(distance);
ApplyVisualCorrection();
if (drawDebugLine)
Debug.DrawLine(start, end, debugLineColor);
}
private Vector3 GetRootPosition(Vector3 start, Vector3 end)
{
switch (pivotMode)
{
case PivotMode.Start:
return start;
case PivotMode.End:
return end;
case PivotMode.Center:
default:
return (start + end) * 0.5f;
}
}
private Vector3 GetScaledVector(float targetLength)
{
float lengthScale = targetLength / Mathf.Max(0.0001f, originalLength);
Vector3 scale = initialLocalScale;
switch (lineAxis)
{
case LineAxis.LocalX:
scale.x = initialLocalScale.x * lengthScale;
scale.y = initialLocalScale.y * thicknessScale;
scale.z = initialLocalScale.z * thicknessScale;
break;
case LineAxis.LocalY:
scale.x = initialLocalScale.x * thicknessScale;
scale.y = initialLocalScale.y * lengthScale;
scale.z = initialLocalScale.z * thicknessScale;
break;
case LineAxis.LocalZ:
scale.x = initialLocalScale.x * thicknessScale;
scale.y = initialLocalScale.y * thicknessScale;
scale.z = initialLocalScale.z * lengthScale;
break;
}
return scale;
}
private static Vector3 GetAxisVector(LineAxis axis)
{
switch (axis)
{
case LineAxis.LocalX:
return Vector3.right;
case LineAxis.LocalY:
return Vector3.up;
case LineAxis.LocalZ:
return Vector3.forward;
default:
return Vector3.up;
}
}
private void ApplyVisualCorrection()
{
if (visualRoot == null)
return;
visualRoot.localPosition = visualInitialLocalPosition + visualLocalPositionOffset;
visualRoot.localRotation = visualInitialLocalRotation * Quaternion.Euler(visualLocalRotationOffsetEuler);
}
private void SetVisible(bool visible)
{
if (visualRoot != null)
{
SetRenderersVisible(visualRoot, visible);
return;
}
SetRenderersVisible(transform, visible);
}
private void SetRenderersVisible(Transform target, bool visible)
{
if (target == null)
return;
Renderer[] renderers = target.GetComponentsInChildren<Renderer>(true);
for (int i = 0; i < renderers.Length; i++)
renderers[i].enabled = visible;
}
private void TryAutoFindVisualRoot()
{
if (visualRoot != null)
return;
Renderer directRenderer = GetComponent<Renderer>();
if (directRenderer != null)
{
visualRoot = transform;
return;
}
Renderer childRenderer = GetComponentInChildren<Renderer>(true);
if (childRenderer != null)
visualRoot = childRenderer.transform;
}
[ContextMenu("Set Mode: Stretch Segment Only")]
private void SetStretchSegmentOnlyMode()
{
lineUseMode = LineUseMode.StretchSegmentOnly;
}
[ContextMenu("Reset Corrections")]
private void ResetCorrections()
{
rootRotationOffsetEuler = Vector3.zero;
visualLocalPositionOffset = Vector3.zero;
visualLocalRotationOffsetEuler = Vector3.zero;
}
[ContextMenu("Calibrate Original Length From Renderer Bounds")]
private void CalibrateOriginalLengthFromRendererBounds()
{
Renderer renderer = visualRoot != null ? visualRoot.GetComponentInChildren<Renderer>(true) : GetComponentInChildren<Renderer>(true);
if (renderer == null)
{
Debug.LogWarning("FishingModelLineFollower: Renderer를 찾지 못해서 Original Length를 자동 계산할 수 없습니다.", this);
return;
}
Vector3 size = renderer.bounds.size;
float measured = Mathf.Max(size.x, Mathf.Max(size.y, size.z));
originalLength = Mathf.Max(0.0001f, measured);
Debug.Log($"FishingModelLineFollower: Original Length를 {originalLength:F4}로 설정했습니다.", this);
}
public void SetTargets(Transform startPoint, Transform endPoint)
{
rodTipPoint = startPoint;
bobberPoint = endPoint;
}
public void SetReverseVisualDirection(bool value)
{
reverseVisualDirection = value;
}
public void SetOriginalLength(float value)
{
originalLength = Mathf.Max(0.0001f, value);
}
}