351 lines
11 KiB
C#
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);
|
|
}
|
|
}
|