2026.06.25 Fishing scene
This commit is contained in:
350
Assets/My project/Fishing Scripts/UI/FishingModelLineFollower.cs
Normal file
350
Assets/My project/Fishing Scripts/UI/FishingModelLineFollower.cs
Normal file
@@ -0,0 +1,350 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user