Files
F2RopeLine/Assets/Scripts/FishingLineSolver.cs
2026-04-06 19:27:55 +08:00

442 lines
15 KiB
C#

using System;
using System.Collections.Generic;
using UnityEngine;
public class FishingLineSolver : MonoBehaviour
{
private const float MinSegmentLength = 0.01f;
[Header("Rig")]
[SerializeField] private FishingRigDefinition rigDefinition;
[SerializeField] private List<FishingLineLogicalNodeDefinition> inlineLogicalNodes = new List<FishingLineLogicalNodeDefinition>();
[SerializeField] private Transform startAnchor;
[SerializeField] private Vector3 initialDirection = Vector3.down;
[Header("Simulation")]
[SerializeField] [Min(1)] private int solverIterations = 8;
[SerializeField] [Min(0f)] private float gravity = 9.81f;
[SerializeField] [Range(0f, 1f)] private float globalDamping = 0.01f;
[SerializeField] [Range(0f, 1f)] private float tensionSmoothing = 0.18f;
[SerializeField] private bool simulateInFixedUpdate = false;
[Header("Line Length")]
[SerializeField] [Min(0.1f)] private float lengthScale = 1f;
[SerializeField] [Min(0.1f)] private float minLengthScale = 0.35f;
[SerializeField] [Min(0.1f)] private float maxLengthScale = 2.5f;
[Header("Debug")]
[SerializeField] private bool drawDebug = true;
[SerializeField] [Min(0.005f)] private float debugNodeRadius = 0.03f;
[SerializeField] private Color virtualNodeColor = new Color(0.4f, 0.75f, 1f, 1f);
[SerializeField] private Color segmentColor = new Color(0.85f, 0.9f, 1f, 1f);
private readonly List<RuntimePoint> points = new List<RuntimePoint>();
private readonly List<RuntimeLogicalNode> logicalNodes = new List<RuntimeLogicalNode>();
private readonly List<float> baseSegmentLengths = new List<float>();
private readonly List<Vector3> linePositions = new List<Vector3>();
private bool runtimeBuilt;
private float currentTensionNormalized;
public IReadOnlyList<Vector3> LinePositions => linePositions;
public int PointCount => points.Count;
public int LogicalNodeCount => logicalNodes.Count;
public float CurrentTensionNormalized => currentTensionNormalized;
public IReadOnlyList<FishingLineLogicalNodeDefinition> LogicalNodeDefinitions => GetActiveDefinitions();
private void OnEnable()
{
Rebuild();
}
private void Reset()
{
if (inlineLogicalNodes.Count > 0)
{
return;
}
FishingLineLogicalNodeDefinition startNode = new FishingLineLogicalNodeDefinition();
startNode.ConfigurePrototype("Start", FishingLineNodeType.Start, 0f, 0, 0f, 0f, new Color(0.4f, 1f, 0.6f, 1f));
FishingLineLogicalNodeDefinition floatNode = new FishingLineLogicalNodeDefinition();
floatNode.ConfigurePrototype("Float", FishingLineNodeType.Float, 1.2f, 5, 0.15f, 0.08f, new Color(1f, 0.75f, 0.2f, 1f));
FishingLineLogicalNodeDefinition sinkerNode = new FishingLineLogicalNodeDefinition();
sinkerNode.ConfigurePrototype("Sinker", FishingLineNodeType.Sinker, 1.4f, 4, 1.6f, 0.02f, new Color(0.7f, 0.85f, 1f, 1f));
FishingLineLogicalNodeDefinition hookNode = new FishingLineLogicalNodeDefinition();
hookNode.ConfigurePrototype("Hook", FishingLineNodeType.Hook, 0.8f, 2, 1.2f, 0.03f, new Color(1f, 0.35f, 0.35f, 1f));
inlineLogicalNodes = new List<FishingLineLogicalNodeDefinition>
{
startNode,
floatNode,
sinkerNode,
hookNode
};
}
private void OnValidate()
{
solverIterations = Mathf.Max(1, solverIterations);
minLengthScale = Mathf.Max(0.1f, minLengthScale);
maxLengthScale = Mathf.Max(minLengthScale, maxLengthScale);
lengthScale = Mathf.Clamp(lengthScale, minLengthScale, maxLengthScale);
}
private void Update()
{
if (!simulateInFixedUpdate)
{
Simulate(Time.deltaTime);
}
}
private void FixedUpdate()
{
if (simulateInFixedUpdate)
{
Simulate(Time.fixedDeltaTime);
}
}
[ContextMenu("Rebuild Solver")]
public void Rebuild()
{
BuildRuntime();
SnapToAnchors();
RefreshLinePositions();
}
public void SetLengthScale(float newScale)
{
lengthScale = Mathf.Clamp(newScale, minLengthScale, maxLengthScale);
}
public void AdjustLengthScale(float delta)
{
SetLengthScale(lengthScale + delta);
}
public bool TryGetLogicalNodePosition(int logicalNodeIndex, out Vector3 position)
{
if (logicalNodeIndex < 0 || logicalNodeIndex >= logicalNodes.Count)
{
position = default;
return false;
}
position = logicalNodes[logicalNodeIndex].Point.Position;
return true;
}
public bool TryGetLogicalNodeTangent(int logicalNodeIndex, out Vector3 tangent)
{
if (logicalNodeIndex < 0 || logicalNodeIndex >= logicalNodes.Count)
{
tangent = Vector3.forward;
return false;
}
int pointIndex = logicalNodes[logicalNodeIndex].PointIndex;
Vector3 previous = pointIndex > 0 ? points[pointIndex - 1].Position : points[pointIndex].Position;
Vector3 next = pointIndex < points.Count - 1 ? points[pointIndex + 1].Position : points[pointIndex].Position;
tangent = (next - previous).normalized;
return tangent.sqrMagnitude > 0f;
}
private void Simulate(float deltaTime)
{
if (!runtimeBuilt)
{
Rebuild();
}
if (points.Count == 0 || deltaTime <= 0f)
{
return;
}
SyncAnchors();
Integrate(deltaTime);
SolveConstraints();
SyncAnchors();
RefreshLinePositions();
UpdateTension();
}
private void BuildRuntime()
{
points.Clear();
logicalNodes.Clear();
baseSegmentLengths.Clear();
linePositions.Clear();
IReadOnlyList<FishingLineLogicalNodeDefinition> definitions = GetActiveDefinitions();
if (definitions.Count == 0)
{
runtimeBuilt = false;
return;
}
if (definitions[0].NodeType != FishingLineNodeType.Start)
{
Debug.LogWarning("FishingLineSolver expects the first logical node to be Start. The first node is still treated as the anchor.", this);
}
Vector3 cursor = startAnchor != null ? startAnchor.position : transform.position;
Vector3 direction = initialDirection.sqrMagnitude > 0f ? initialDirection.normalized : Vector3.down;
for (int logicalIndex = 0; logicalIndex < definitions.Count; logicalIndex++)
{
FishingLineLogicalNodeDefinition definition = definitions[logicalIndex];
bool anchored = logicalIndex == 0;
if (logicalIndex > 0)
{
FishingLineLogicalNodeDefinition previousDefinition = definitions[logicalIndex - 1];
float logicalDistance = Mathf.Max(MinSegmentLength, definition.DistanceFromPrevious);
int virtualNodeCount = Mathf.Max(0, definition.VirtualNodeCount);
float subSegmentLength = logicalDistance / (virtualNodeCount + 1);
for (int virtualIndex = 0; virtualIndex < virtualNodeCount; virtualIndex++)
{
cursor += direction * subSegmentLength;
RuntimePoint virtualPoint = new RuntimePoint(
cursor,
false,
-1,
Mathf.Lerp(previousDefinition.GravityScale, definition.GravityScale, (virtualIndex + 1f) / (virtualNodeCount + 1f)),
Mathf.Lerp(previousDefinition.Damping, definition.Damping, (virtualIndex + 1f) / (virtualNodeCount + 1f)));
points.Add(virtualPoint);
baseSegmentLengths.Add(subSegmentLength);
linePositions.Add(cursor);
}
cursor += direction * subSegmentLength;
baseSegmentLengths.Add(subSegmentLength);
}
RuntimePoint logicalPoint = new RuntimePoint(cursor, anchored, logicalIndex, definition.GravityScale, definition.Damping);
if (anchored)
{
logicalPoint.Anchor = startAnchor;
}
points.Add(logicalPoint);
logicalNodes.Add(new RuntimeLogicalNode(logicalIndex, points.Count - 1, logicalPoint));
linePositions.Add(cursor);
}
runtimeBuilt = points.Count > 0;
}
private void SnapToAnchors()
{
for (int index = 0; index < points.Count; index++)
{
RuntimePoint point = points[index];
if (!point.IsAnchored)
{
continue;
}
Vector3 anchorPosition = point.Anchor != null ? point.Anchor.position : transform.position;
point.Position = anchorPosition;
point.PreviousPosition = anchorPosition;
}
}
private void SyncAnchors()
{
for (int index = 0; index < points.Count; index++)
{
RuntimePoint point = points[index];
if (!point.IsAnchored)
{
continue;
}
Vector3 anchorPosition = point.Anchor != null ? point.Anchor.position : transform.position;
point.Position = anchorPosition;
}
}
private void Integrate(float deltaTime)
{
float deltaTimeSqr = deltaTime * deltaTime;
for (int index = 0; index < points.Count; index++)
{
RuntimePoint point = points[index];
if (point.IsAnchored)
{
point.PreviousPosition = point.Position;
continue;
}
Vector3 velocity = point.Position - point.PreviousPosition;
float dampingFactor = Mathf.Clamp01(1f - globalDamping - point.Damping);
Vector3 nextPosition = point.Position + velocity * dampingFactor + Vector3.down * (gravity * point.GravityScale * deltaTimeSqr);
point.PreviousPosition = point.Position;
point.Position = nextPosition;
}
}
private void SolveConstraints()
{
for (int iteration = 0; iteration < solverIterations; iteration++)
{
for (int segmentIndex = 0; segmentIndex < baseSegmentLengths.Count; segmentIndex++)
{
RuntimePoint pointA = points[segmentIndex];
RuntimePoint pointB = points[segmentIndex + 1];
Vector3 delta = pointB.Position - pointA.Position;
float distance = delta.magnitude;
float restLength = baseSegmentLengths[segmentIndex] * lengthScale;
if (distance <= restLength || distance <= Mathf.Epsilon)
{
continue;
}
Vector3 correction = delta * ((distance - restLength) / distance);
if (pointA.IsAnchored && pointB.IsAnchored)
{
continue;
}
if (pointA.IsAnchored)
{
pointB.Position -= correction;
continue;
}
if (pointB.IsAnchored)
{
pointA.Position += correction;
continue;
}
Vector3 halfCorrection = correction * 0.5f;
pointA.Position += halfCorrection;
pointB.Position -= halfCorrection;
}
}
}
private void RefreshLinePositions()
{
linePositions.Clear();
for (int index = 0; index < points.Count; index++)
{
linePositions.Add(points[index].Position);
}
}
private void UpdateTension()
{
if (baseSegmentLengths.Count == 0)
{
currentTensionNormalized = 0f;
return;
}
float maxStretch = 0f;
for (int segmentIndex = 0; segmentIndex < baseSegmentLengths.Count; segmentIndex++)
{
float currentLength = Vector3.Distance(points[segmentIndex].Position, points[segmentIndex + 1].Position);
float restLength = Mathf.Max(MinSegmentLength, baseSegmentLengths[segmentIndex] * lengthScale);
maxStretch = Mathf.Max(maxStretch, Mathf.Clamp01((currentLength - restLength) / restLength));
}
currentTensionNormalized = Mathf.Lerp(currentTensionNormalized, maxStretch, tensionSmoothing);
}
private IReadOnlyList<FishingLineLogicalNodeDefinition> GetActiveDefinitions()
{
if (rigDefinition != null && rigDefinition.LogicalNodes.Count > 0)
{
return rigDefinition.LogicalNodes;
}
return inlineLogicalNodes;
}
private void OnDrawGizmos()
{
if (!drawDebug || points.Count == 0)
{
return;
}
IReadOnlyList<FishingLineLogicalNodeDefinition> definitions = GetActiveDefinitions();
Gizmos.color = segmentColor;
for (int segmentIndex = 0; segmentIndex < points.Count - 1; segmentIndex++)
{
Gizmos.DrawLine(points[segmentIndex].Position, points[segmentIndex + 1].Position);
}
for (int pointIndex = 0; pointIndex < points.Count; pointIndex++)
{
RuntimePoint point = points[pointIndex];
if (point.IsLogicalNode)
{
FishingLineLogicalNodeDefinition definition = definitions[point.LogicalNodeIndex];
Gizmos.color = definition.DebugColor;
Gizmos.DrawSphere(point.Position, debugNodeRadius * 1.35f);
}
else
{
Gizmos.color = virtualNodeColor;
Gizmos.DrawSphere(point.Position, debugNodeRadius);
}
}
}
[Serializable]
private class RuntimeLogicalNode
{
public RuntimeLogicalNode(int logicalIndex, int pointIndex, RuntimePoint point)
{
LogicalIndex = logicalIndex;
PointIndex = pointIndex;
Point = point;
}
public int LogicalIndex { get; }
public int PointIndex { get; }
public RuntimePoint Point { get; }
}
[Serializable]
private class RuntimePoint
{
public RuntimePoint(Vector3 position, bool isAnchored, int logicalNodeIndex, float gravityScale, float damping)
{
Position = position;
PreviousPosition = position;
IsAnchored = isAnchored;
LogicalNodeIndex = logicalNodeIndex;
GravityScale = gravityScale;
Damping = damping;
}
public Vector3 Position;
public Vector3 PreviousPosition;
public bool IsAnchored;
public Transform Anchor;
public int LogicalNodeIndex;
public float GravityScale;
public float Damping;
public bool IsLogicalNode => LogicalNodeIndex >= 0;
}
}