442 lines
15 KiB
C#
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;
|
|
}
|
|
}
|