using UnityEngine; /// /// 모델 낚시줄을 두 포인트 사이에 맞춰 배치/회전/길이 스케일하는 스크립트입니다. /// 권장 사용 방식: /// - StretchLineRoot 빈 오브젝트에 이 스크립트를 붙입니다. /// - StretchLineRoot 자식으로 '늘어나는 중간 줄 모델'만 넣습니다. /// - RodFixedLinePart, BobberFixedLinePart, Bobber/Hook은 StretchLineRoot 안에 넣지 않습니다. /// - 낚시찌는 BobberLineStartPoint의 자식으로 넣어 줄 끝점에 고정합니다. /// 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(true); for (int i = 0; i < renderers.Length; i++) renderers[i].enabled = visible; } private void TryAutoFindVisualRoot() { if (visualRoot != null) return; Renderer directRenderer = GetComponent(); if (directRenderer != null) { visualRoot = transform; return; } Renderer childRenderer = GetComponentInChildren(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(true) : GetComponentInChildren(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); } }